diff --git a/.cargo/config.toml b/.cargo/config.toml index d7db3025da8..d409506399e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,13 +1,15 @@ +# Note: keep in mind that this file is completely ignored in several use-cases +# like e.g. out-of-tree builds ( https://github.com/rust-lang/cargo/issues/2930 ). +# For this reason this file should be avoided as much as possible when there are alternatives. + [target.x86_64-unknown-redox] linker = "x86_64-unknown-redox-gcc" -[target.'cfg(clippy)'] -rustflags = [ - "-Wclippy::use_self", - "-Wclippy::needless_pass_by_value", - "-Wclippy::semicolon_if_nothing_returned", - "-Wclippy::single_char_pattern", - "-Wclippy::explicit_iter_loop", - "-Wclippy::if_not_else", -] +[env] +# See feat_external_libstdbuf in src/uu/stdbuf/Cargo.toml +LIBSTDBUF_DIR = "/usr/local/libexec/coreutils" +# libstdbuf must be a shared library, so musl libc can't be linked statically +# https://github.com/rust-lang/rust/issues/82193 +[build] +rustflags = ["-C", "target-feature=-crt-static"] diff --git a/.clippy.toml b/.clippy.toml index b1552463edd..b6f9a360f69 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1,2 +1,5 @@ -msrv = "1.70.0" +msrv = "1.85.0" +avoid-breaking-exported-api = false +check-private-items = true cognitive-complexity-threshold = 24 +missing-docs-in-crate-items = true diff --git a/.config/nextest.toml b/.config/nextest.toml index 3ba8bb393a4..473c461402a 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -4,3 +4,10 @@ status-level = "all" final-status-level = "skip" failure-output = "immediate-final" fail-fast = false + +[profile.coverage] +retries = 0 +status-level = "all" +final-status-level = "skip" +failure-output = "immediate-final" +fail-fast = false diff --git a/.editorconfig b/.editorconfig index 53ccc4f9a15..9df8cbbbf98 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,5 @@ # EditorConfig (is awesome!; ref: http://EditorConfig.org; v2022.02.11 [rivy]) +# spell-checker:ignore akefile shellcheck vcproj # * top-most EditorConfig file root = true @@ -52,7 +53,7 @@ indent_style = space switch_case_indent = true [*.{sln,vc{,x}proj{,.*},[Ss][Ln][Nn],[Vv][Cc]{,[Xx]}[Pp][Rr][Oo][Jj]{,.*}}] -# MSVC sln/vcproj/vcxproj files, when used, will persistantly revert to CRLF EOLNs and eat final EOLs +# MSVC sln/vcproj/vcxproj files, when used, will persistently revert to CRLF EOLNs and eat final EOLs end_of_line = crlf insert_final_newline = false diff --git a/.envrc b/.envrc new file mode 100644 index 00000000000..cbf4a76e2de --- /dev/null +++ b/.envrc @@ -0,0 +1,7 @@ +# spell-checker:ignore direnv + +if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM=" +fi + +use flake diff --git a/.github/fluent_linter_config.yml b/.github/fluent_linter_config.yml new file mode 100644 index 00000000000..1b33942f73a --- /dev/null +++ b/.github/fluent_linter_config.yml @@ -0,0 +1,28 @@ +--- +ID01: + enabled: true + exclusions: + messages: [] + files: [] +ID02: + enabled: true + min_length: 7 +VC: + disabled: true +# Disable: # TE01: single quote instead of apostrophe for genitive (foo's) +TE01: + enabled: false +# TE03: single quotes ('foo') +TE03: + enabled: false +# TE04: Double-quoted strings should use Unicode " instead of "foo". +TE04: + enabled: false +# Disable: TE05: 3 dots for ellipsis ("...") +TE05: + enabled: false +# Should be fixed +VC01: + disabled: true +ID03: + enabled: true diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index f61b49ea27e..8bdbc6c3f10 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -1,17 +1,17 @@ name: CICD -# spell-checker:ignore (abbrev/names) CICD CodeCOV MacOS MinGW MSVC musl taiki +# 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 -# spell-checker:ignore (jargon) SHAs deps dequote softprops subshell toolchain fuzzers -# spell-checker:ignore (people) Peltoche rivy dtolnay -# spell-checker:ignore (shell/tools) choco clippy dmake dpkg esac fakeroot fdesc fdescfs gmake grcov halium lcov libssl mkdir popd printf pushd rsync rustc rustfmt rustup shopt utmpdump xargs -# spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils defconfig DESTDIR gecos gnueabihf issuecomment maint multisize nullglob onexitbegin onexitend pell runtest Swatinem tempfile testsuite toybox uutils +# spell-checker:ignore (jargon) SHAs deps dequote softprops subshell toolchain fuzzers dedupe devel profdata +# spell-checker:ignore (people) Peltoche rivy dtolnay Anson dawidd +# spell-checker:ignore (shell/tools) binutils choco clippy dmake dpkg esac fakeroot fdesc fdescfs gmake grcov halium lcov libclang libfuse libssl limactl mkdir nextest nocross pacman popd printf pushd redoxer rsync rustc rustfmt rustup shopt sccache utmpdump xargs +# spell-checker:ignore (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 env: PROJECT_NAME: coreutils PROJECT_DESC: "Core universal (cross-platform) utilities" PROJECT_AUTH: "uutils" - RUST_MIN_SRV: "1.70.0" + RUST_MIN_SRV: "1.85.0" # * style job configuration STYLE_FAIL_ON_FAULT: true ## (bool) fail the build if a style job contains a fault (error or warning); may be overridden on a per-job basis @@ -19,8 +19,9 @@ on: pull_request: push: tags: + - '*' branches: - - main + - '*' permissions: contents: read # to fetch code (actions/checkout) @@ -35,8 +36,10 @@ jobs: name: Style/cargo-deny runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: EmbarkStudios/cargo-deny-action@v1 + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: EmbarkStudios/cargo-deny-action@v2 style_deps: ## ToDO: [2021-11-10; rivy] 'Style/deps' needs more informative output and better integration of results into the GHA dashboard @@ -52,7 +55,9 @@ jobs: - { os: macos-latest , features: "feat_Tier1,feat_require_unix,feat_require_unix_utmpx" } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@nightly ## note: requires 'nightly' toolchain b/c `cargo-udeps` uses the `rustc` '-Z save-analysis' option ## * ... ref: @@ -104,14 +109,20 @@ jobs: # - { os: macos-latest , features: feat_os_macos } # - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@master with: toolchain: stable components: clippy - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.4 + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install/setup prerequisites + shell: bash + run: | + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev - name: Initialize workflow variables id: vars shell: bash @@ -138,7 +149,7 @@ jobs: shell: bash run: | RUSTDOCFLAGS="-Dwarnings" cargo doc ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-deps --workspace --document-private-items - - uses: DavidAnson/markdownlint-cli2-action@v15 + - uses: DavidAnson/markdownlint-cli2-action@v20 with: fix: "true" globs: | @@ -157,7 +168,9 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.RUST_MIN_SRV }} @@ -165,7 +178,7 @@ jobs: - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.4 + uses: mozilla-actions/sccache-action@v0.0.9 - name: Initialize workflow variables id: vars shell: bash @@ -177,19 +190,19 @@ jobs: unset CARGO_FEATURES_OPTION if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features "${{ matrix.job.features }}"' ; fi outputs CARGO_FEATURES_OPTION - - name: Confirm MinSRV compatible 'Cargo.lock' + - name: Confirm MinSRV compatible '*/Cargo.lock' shell: bash run: | - ## Confirm MinSRV compatible 'Cargo.lock' - # * 'Cargo.lock' is required to be in a format that `cargo` of MinSRV can interpret (eg, v1-format for MinSRV < v1.38) - cargo fetch --locked --quiet || { echo "::error file=Cargo.lock::Incompatible (or out-of-date) 'Cargo.lock' file; update using \`cargo +${{ env.RUST_MIN_SRV }} update\`" ; exit 1 ; } - - name: Confirm MinSRV equivalence for '.clippy.toml' + ## Confirm MinSRV compatible '*/Cargo.lock' + # * '*/Cargo.lock' is required to be in a format that `cargo` of MinSRV can interpret (eg, v1-format for MinSRV < v1.38) + for dir in "." "fuzz"; do + ( cd "$dir" && cargo fetch --locked --quiet ) || { echo "::error file=$dir/Cargo.lock::Incompatible (or out-of-date) '$dir/Cargo.lock' file; update using \`cd '$dir' && cargo +${{ env.RUST_MIN_SRV }} update\`" ; exit 1 ; } + done + - name: Install/setup prerequisites shell: bash run: | - ## Confirm MinSRV equivalence for '.clippy.toml' - # * ensure '.clippy.toml' MSRV configuration setting is equal to ${{ env.RUST_MIN_SRV }} - CLIPPY_MSRV=$(grep -P "(?i)^\s*msrv\s*=\s*" .clippy.toml | grep -oP "\d+([.]\d+)+") - if [ "${CLIPPY_MSRV}" != "${{ env.RUST_MIN_SRV }}" ]; then { echo "::error file=.clippy.toml::Incorrect MSRV configuration for clippy (found '${CLIPPY_MSRV}'; should be '${{ env.RUST_MIN_SRV }}'); update '.clippy.toml' with 'msrv = \"${{ env.RUST_MIN_SRV }}\"'" ; exit 1 ; } ; fi + # Install a package for one of the tests + sudo apt-get -y update ; sudo apt-get -y install attr - name: Info shell: bash run: | @@ -225,7 +238,9 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: "`cargo update` testing" @@ -233,7 +248,9 @@ jobs: run: | ## `cargo update` testing # * convert any errors/warnings to GHA UI annotations; ref: - cargo fetch --locked --quiet || { echo "::error file=Cargo.lock::'Cargo.lock' file requires update (use \`cargo +${{ env.RUST_MIN_SRV }} update\`)" ; exit 1 ; } + for dir in "." "fuzz"; do + ( cd "$dir" && cargo fetch --locked --quiet ) || { echo "::error file=$dir/Cargo.lock::'$dir/Cargo.lock' file requires update (use \`cd '$dir' && cargo +${{ env.RUST_MIN_SRV }} update\`)" ; exit 1 ; } + done build_makefile: name: Build/Makefile @@ -248,25 +265,61 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 + - name: Install/setup prerequisites + shell: bash + run: | + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.4 + uses: mozilla-actions/sccache-action@v0.0.9 - name: "`make build`" + # Also check that target/CACHEDIR.TAG is created on a fresh checkout shell: bash run: | + set -x + # Target directory must not exist to start with, otherwise cargo + # will not create target/CACHEDIR.TAG. + if [[ -d target ]]; then + mv -T target target.cache + fi + # Actually do the build make build + echo "Check that target directory will be ignored by backup tools" + test -f target/CACHEDIR.TAG + # Restore cache for target/release (we only did a debug build) + mv -t target/ target.cache/release 2>/dev/null || true - name: "`make nextest`" shell: bash run: make nextest CARGOFLAGS="--profile ci --hide-progress-bar" env: RUST_BACKTRACE: "1" + - name: "`make install COMPLETIONS=n MANPAGES=n`" + shell: bash + run: | + set -x + DESTDIR=/tmp/ make PROFILE=release COMPLETIONS=n MANPAGES=n install + # Check that the utils are present + test -f /tmp/usr/local/bin/tty + # Check that the manpage is not present + ! test -f /tmp/usr/local/share/man/man1/whoami.1 + # Check that the completion is not present + ! test -f /tmp/usr/local/share/zsh/site-functions/_install + ! test -f /tmp/usr/local/share/bash-completion/completions/head + ! test -f /tmp/usr/local/share/fish/vendor_completions.d/cat.fish + env: + RUST_BACKTRACE: "1" - name: "`make install`" shell: bash run: | + set -x DESTDIR=/tmp/ make PROFILE=release install + # Check that the utils are present + test -f /tmp/usr/local/bin/tty # Check that the manpage is present test -f /tmp/usr/local/share/man/man1/whoami.1 # Check that the completion is present @@ -278,7 +331,10 @@ jobs: - name: "`make uninstall`" shell: bash run: | + set -x DESTDIR=/tmp/ make uninstall + # Check that the utils are not present + ! test -f /tmp/usr/local/bin/tty # Check that the manpage is not present ! test -f /tmp/usr/local/share/man/man1/whoami.1 # Check that the completion is not present @@ -302,14 +358,16 @@ jobs: - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.4 + uses: mozilla-actions/sccache-action@v0.0.9 - name: Test - run: cargo nextest run --hide-progress-bar --profile ci ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} + run: cargo nextest run --hide-progress-bar --profile ci --features ${{ matrix.job.features }} env: RUST_BACKTRACE: "1" @@ -329,14 +387,16 @@ jobs: - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@nightly - uses: taiki-e/install-action@nextest - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.4 + uses: mozilla-actions/sccache-action@v0.0.9 - name: Test - run: cargo nextest run --hide-progress-bar --profile ci ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} + run: cargo nextest run --hide-progress-bar --profile ci --features ${{ matrix.job.features }} env: RUST_BACKTRACE: "1" @@ -353,17 +413,19 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.4 + uses: mozilla-actions/sccache-action@v0.0.9 - name: Install dependencies shell: bash run: | ## Install dependencies sudo apt-get update - sudo apt-get install jq + sudo apt-get install jq libselinux1-dev - name: "`make install`" shell: bash run: | @@ -396,14 +458,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@v3 + uses: dawidd6/action-download-artifact@v11 with: workflow: CICD.yml name: individual-size-result repo: uutils/coreutils path: dl - name: Download the previous size result - uses: dawidd6/action-download-artifact@v3 + uses: dawidd6/action-download-artifact@v11 with: workflow: CICD.yml name: size-result @@ -466,23 +528,30 @@ jobs: fail-fast: false matrix: job: - # - { os , target , cargo-options , features , use-cross , toolchain, skip-tests } - - { os: ubuntu-latest , target: arm-unknown-linux-gnueabihf, features: feat_os_unix_gnueabihf, use-cross: use-cross, skip-tests: true } - - { os: ubuntu-latest , target: aarch64-unknown-linux-gnu , features: feat_os_unix_gnueabihf , use-cross: use-cross , skip-tests: true } - - { os: ubuntu-latest , target: aarch64-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross , skip-tests: true } + # - { os , target , cargo-options , default-features, features , use-cross , toolchain, skip-tests, workspace-tests, skip-package, skip-publish } + - { os: ubuntu-latest , target: arm-unknown-linux-gnueabihf , features: feat_os_unix_gnueabihf , use-cross: use-cross , skip-tests: true } + - { os: ubuntu-24.04-arm , target: aarch64-unknown-linux-gnu , features: feat_os_unix_gnueabihf } + - { os: ubuntu-latest , target: aarch64-unknown-linux-musl , features: feat_os_unix , use-cross: use-cross , skip-tests: true } # - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: feat_selinux , use-cross: use-cross } - - { os: ubuntu-latest , target: i686-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } - - { os: ubuntu-latest , target: i686-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } - - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: feat_os_unix , use-cross: use-cross } - - { os: ubuntu-latest , target: x86_64-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } + - { os: ubuntu-latest , target: i686-unknown-linux-gnu , features: "feat_os_unix,test_risky_names", use-cross: use-cross } + - { os: ubuntu-latest , target: i686-unknown-linux-musl , features: feat_os_unix , use-cross: use-cross } + - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,test_risky_names", use-cross: use-cross } + - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,uudoc" , use-cross: no, workspace-tests: true } + - { os: ubuntu-latest , target: x86_64-unknown-linux-musl , features: feat_os_unix , use-cross: use-cross } - { os: ubuntu-latest , target: x86_64-unknown-redox , features: feat_os_unix_redox , use-cross: redoxer , skip-tests: true } - - { os: macos-14 , target: aarch64-apple-darwin , features: feat_os_macos } # M1 CPU - - { os: macos-latest , target: x86_64-apple-darwin , features: feat_os_macos } + - { os: ubuntu-latest , target: wasm32-unknown-unknown , default-features: false, features: uucore/format, skip-tests: true, skip-package: true, skip-publish: true } + - { os: macos-latest , target: aarch64-apple-darwin , features: feat_os_macos, workspace-tests: true } # M1 CPU + # PR #7964: Mac should still build even if the feature is not enabled + - { os: macos-latest , target: aarch64-apple-darwin , workspace-tests: true } # M1 CPU + - { os: macos-latest , target: x86_64-apple-darwin , features: feat_os_macos, workspace-tests: true } - { os: windows-latest , target: i686-pc-windows-msvc , features: feat_os_windows } - { os: windows-latest , target: x86_64-pc-windows-gnu , features: feat_os_windows } - { os: windows-latest , target: x86_64-pc-windows-msvc , features: feat_os_windows } + - { os: windows-latest , target: aarch64-pc-windows-msvc , features: feat_os_windows, use-cross: use-cross , skip-tests: true } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.RUST_MIN_SRV }} @@ -491,7 +560,7 @@ jobs: with: key: "${{ matrix.job.os }}_${{ matrix.job.target }}" - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.4 + uses: mozilla-actions/sccache-action@v0.0.9 - name: Initialize workflow variables id: vars shell: bash @@ -568,24 +637,41 @@ jobs: CARGO_FEATURES_OPTION='' ; if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features=${{ matrix.job.features }}' ; fi outputs CARGO_FEATURES_OPTION + # * CARGO_DEFAULT_FEATURES_OPTION + CARGO_DEFAULT_FEATURES_OPTION='' ; + if [ "${{ matrix.job.default-features }}" == "false" ]; then CARGO_DEFAULT_FEATURES_OPTION='--no-default-features' ; fi + outputs CARGO_DEFAULT_FEATURES_OPTION # * CARGO_CMD CARGO_CMD='cross' CARGO_CMD_OPTIONS='+${{ env.RUST_MIN_SRV }}' + # Added suffix for artifacts, needed when multiple jobs use the same target. + ARTIFACTS_SUFFIX='' case '${{ matrix.job.use-cross }}' in ''|0|f|false|n|no) CARGO_CMD='cargo' + ARTIFACTS_SUFFIX='-nocross' ;; redoxer) CARGO_CMD='redoxer' CARGO_CMD_OPTIONS='' ;; esac + # needed for target "aarch64-apple-darwin". There are two jobs, and the difference between them is whether "features" is set + if [ -z "${{ matrix.job.features }}" ]; then ARTIFACTS_SUFFIX='-nofeatures' ; fi outputs CARGO_CMD outputs CARGO_CMD_OPTIONS - # ** pass needed environment into `cross` container (iff `cross` not already configured via "Cross.toml") - if [ "${CARGO_CMD}" = 'cross' ] && [ ! -e "Cross.toml" ] ; then - printf "[build.env]\npassthrough = [\"CI\", \"RUST_BACKTRACE\", \"CARGO_TERM_COLOR\"]\n" > Cross.toml - fi + outputs ARTIFACTS_SUFFIX + CARGO_TEST_OPTIONS='' + case '${{ matrix.job.workspace-tests }}' in + 1|t|true|y|yes) + # This also runs tests in other packages in the source directory (e.g. uucore). + # We cannot enable this everywhere as some platforms are currently broken, and + # we cannot use `cross` as its Docker image is ancient (Ubuntu 16.04) and is + # missing required system dependencies (e.g. recent libclang-dev). + CARGO_TEST_OPTIONS='--workspace' + ;; + esac + outputs CARGO_TEST_OPTIONS # * executable for `strip`? STRIP="strip" case ${{ matrix.job.target }} in @@ -622,23 +708,23 @@ jobs: sudo apt-get -y update sudo apt-get -y install fuse3 libfuse-dev ;; - # Update binutils if MinGW due to https://github.com/rust-lang/rust/issues/112368 - x86_64-pc-windows-gnu) - C:/msys64/usr/bin/pacman.exe -Syu --needed mingw-w64-x86_64-gcc --noconfirm - echo "C:\msys64\mingw64\bin" >> $GITHUB_PATH - ;; esac case '${{ matrix.job.os }}' in macos-latest) brew install coreutils ;; # needed for testing esac case '${{ matrix.job.os }}' in ubuntu-*) + # selinux headers needed to build tests + sudo apt-get -y update + sudo apt-get -y install libselinux1-dev # pinky is a tool to show logged-in users from utmp, and gecos fields from /etc/passwd. - # In GitHub Action *nix VMs, no accounts log in, even the "runner" account that runs the commands. The account also has empty gecos fields. - # To work around this for pinky tests, we create a fake login entry for the GH runner account... - FAKE_UTMP='[7] [999999] [tty2] [runner] [tty2] [] [0.0.0.0] [2022-02-22T22:22:22,222222+00:00]' - # ... by dumping the login records, adding our fake line, then reverse dumping ... - (utmpdump /var/run/utmp ; echo $FAKE_UTMP) | sudo utmpdump -r -o /var/run/utmp + # In GitHub Action *nix VMs, no accounts log in, even the "runner" account that runs the commands, and "system boot" entry is missing. + # The account also has empty gecos fields. + # To work around these issues for pinky (and who) tests, we create a fake utmp file with a + # system boot entry and a login entry for the GH runner account. + FAKE_UTMP_2='[2] [00000] [~~ ] [reboot] [~ ] [6.0.0-test] [0.0.0.0] [2022-02-22T22:11:22,222222+00:00]' + FAKE_UTMP_7='[7] [999999] [tty2] [runner] [tty2] [ ] [0.0.0.0] [2022-02-22T22:22:22,222222+00:00]' + (echo "$FAKE_UTMP_2" ; echo "$FAKE_UTMP_7") | sudo utmpdump -r -o /var/run/utmp # ... and add a full name to each account with a gecos field but no full name. sudo sed -i 's/:,/:runner name,/' /etc/passwd # We also create a couple optional files pinky looks for @@ -683,20 +769,20 @@ jobs: # dependencies echo "## dependency list" cargo fetch --locked --quiet - cargo tree --locked --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-dedupe -e=no-dev --prefix=none | grep -vE "$PWD" | sort --unique + cargo tree --locked --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} ${{ steps.vars.outputs.CARGO_DEFAULT_FEATURES_OPTION }} --no-dedupe -e=no-dev --prefix=none | grep -vE "$PWD" | sort --unique - name: Build shell: bash run: | ## Build ${{ steps.vars.outputs.CARGO_CMD }} ${{ steps.vars.outputs.CARGO_CMD_OPTIONS }} build --release \ - --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} + --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} ${{ steps.vars.outputs.CARGO_DEFAULT_FEATURES_OPTION }} - name: Test if: matrix.job.skip-tests != true shell: bash run: | ## Test ${{ steps.vars.outputs.CARGO_CMD }} ${{ steps.vars.outputs.CARGO_CMD_OPTIONS }} test --target=${{ matrix.job.target }} \ - ${{ steps.vars.outputs.CARGO_TEST_OPTIONS}} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} + ${{ steps.vars.outputs.CARGO_TEST_OPTIONS}} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} ${{ steps.vars.outputs.CARGO_DEFAULT_FEATURES_OPTION }} env: RUST_BACKTRACE: "1" - name: Test individual utilities @@ -711,9 +797,10 @@ jobs: - name: Archive executable artifacts uses: actions/upload-artifact@v4 with: - name: ${{ env.PROJECT_NAME }}-${{ matrix.job.target }} + name: ${{ env.PROJECT_NAME }}-${{ matrix.job.target }}${{ steps.vars.outputs.ARTIFACTS_SUFFIX }} path: target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }} - name: Package + if: matrix.job.skip-package != true shell: bash run: | ## Package artifact(s) @@ -749,8 +836,9 @@ jobs: fi - name: Publish uses: softprops/action-gh-release@v2 - if: steps.vars.outputs.DEPLOY + if: steps.vars.outputs.DEPLOY && matrix.job.skip-publish != true with: + draft: true files: | ${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_NAME }} ${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.DPKG_NAME }} @@ -776,13 +864,16 @@ jobs: run: | ## VARs setup echo "TEST_SUMMARY_FILE=busybox-result.json" >> $GITHUB_OUTPUT - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.4 + uses: mozilla-actions/sccache-action@v0.0.9 - name: Install/setup prerequisites shell: bash run: | + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev ## Install/setup prerequisites make prepare-busytest - name: Run BusyBox test suite @@ -856,24 +947,29 @@ jobs: outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } TEST_SUMMARY_FILE="toybox-result.json" outputs TEST_SUMMARY_FILE - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.RUST_MIN_SRV }} components: rustfmt - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.4 + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install/setup prerequisites + shell: bash + run: | + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev - name: Build coreutils as multiple binaries shell: bash run: | ## Build individual uutil binaries set -v make - - name: Install/setup prerequisites + - name: Run toybox src shell: bash run: | - ## Install/setup prerequisites make toybox-src - name: Run Toybox test suite id: summary @@ -935,45 +1031,55 @@ jobs: matrix: job: - { os: ubuntu-latest , features: unix, toolchain: nightly } - - { os: macos-latest , features: macos, toolchain: nightly } - - { os: windows-latest , features: windows, toolchain: nightly-x86_64-pc-windows-gnu } + # FIXME: Re-enable macos code coverage + # - { os: macos-latest , features: macos, toolchain: nightly } + # FIXME: Re-enable Code Coverage on windows, which currently fails due to "profiler_builtins". See #6686. + # - { os: windows-latest , features: windows, toolchain: nightly-x86_64-pc-windows-gnu } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.job.toolchain }} components: rustfmt - - uses: taiki-e/install-action@nextest - - uses: taiki-e/install-action@grcov + - uses: taiki-e/install-action@v2 + with: + tool: nextest,grcov@0.8.24 - uses: Swatinem/rust-cache@v2 + - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.4 + uses: mozilla-actions/sccache-action@v0.0.9 + # - name: Reattach HEAD ## may be needed for accurate code coverage info # run: git checkout ${{ github.head_ref }} + - name: Initialize workflow variables id: vars shell: bash run: | ## VARs setup outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } + # toolchain TOOLCHAIN="nightly" ## default to "nightly" toolchain (required for certain required unstable compiler flags) ## !maint: refactor when stable channel has needed support + # * specify gnu-type TOOLCHAIN for windows; `grcov` requires gnu-style code coverage data files case ${{ matrix.job.os }} in windows-*) TOOLCHAIN="$TOOLCHAIN-x86_64-pc-windows-gnu" ;; esac; + # * use requested TOOLCHAIN if specified if [ -n "${{ matrix.job.toolchain }}" ]; then TOOLCHAIN="${{ matrix.job.toolchain }}" ; fi outputs TOOLCHAIN - # staging directory - STAGING='_staging' - outputs STAGING + # target-specific options + # * CARGO_FEATURES_OPTION CARGO_FEATURES_OPTION='--all-features' ; ## default to '--all-features' for code coverage if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features=${{ matrix.job.features }}' ; fi outputs CARGO_FEATURES_OPTION + # * CODECOV_FLAGS CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' ) outputs CODECOV_FLAGS + - name: Install/setup prerequisites shell: bash run: | @@ -981,14 +1087,18 @@ jobs: case '${{ matrix.job.os }}' in macos-latest) brew install coreutils ;; # needed for testing esac + case '${{ matrix.job.os }}' in ubuntu-latest) + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev # pinky is a tool to show logged-in users from utmp, and gecos fields from /etc/passwd. - # In GitHub Action *nix VMs, no accounts log in, even the "runner" account that runs the commands. The account also has empty gecos fields. - # To work around this for pinky tests, we create a fake login entry for the GH runner account... - FAKE_UTMP='[7] [999999] [tty2] [runner] [tty2] [] [0.0.0.0] [2022-02-22T22:22:22,222222+00:00]' - # ... by dumping the login records, adding our fake line, then reverse dumping ... - (utmpdump /var/run/utmp ; echo $FAKE_UTMP) | sudo utmpdump -r -o /var/run/utmp + # In GitHub Action *nix VMs, no accounts log in, even the "runner" account that runs the commands, and "system boot" entry is missing. + # The account also has empty gecos fields. + # To work around these issues for pinky (and who) tests, we create a fake utmp file with a + # system boot entry and a login entry for the GH runner account. + FAKE_UTMP_2='[2] [00000] [~~ ] [reboot] [~ ] [6.0.0-test] [0.0.0.0] [2022-02-22T22:11:22,222222+00:00]' + FAKE_UTMP_7='[7] [999999] [tty2] [runner] [tty2] [ ] [0.0.0.0] [2022-02-22T22:22:22,222222+00:00]' + (echo "$FAKE_UTMP_2" ; echo "$FAKE_UTMP_7") | sudo utmpdump -r -o /var/run/utmp # ... and add a full name to each account with a gecos field but no full name. sudo sed -i 's/:,/:runner name,/' /etc/passwd # We also create a couple optional files pinky looks for @@ -996,57 +1106,118 @@ jobs: echo "foo" > /home/runner/.plan ;; esac + case '${{ matrix.job.os }}' in # Update binutils if MinGW due to https://github.com/rust-lang/rust/issues/112368 - windows-latest) C:/msys64/usr/bin/pacman.exe -Syu --needed mingw-w64-x86_64-gcc --noconfirm ; echo "C:\msys64\mingw64\bin" >> $GITHUB_PATH ;; + windows-latest) C:/msys64/usr/bin/pacman.exe -Sy --needed mingw-w64-x86_64-gcc --noconfirm ; echo "C:\msys64\mingw64\bin" >> $GITHUB_PATH ;; esac - - name: Initialize toolchain-dependent workflow variables - id: dep_vars - shell: bash + + ## Install the llvm-tools component to get access to `llvm-profdata` + rustup component add llvm-tools + + - name: Run test and coverage + id: run_test_cov run: | - ## Dependent VARs setup outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } - # * determine sub-crate utility list - UTILITY_LIST="$(./util/show-utils.sh ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }})" - CARGO_UTILITY_LIST_OPTIONS="$(for u in ${UTILITY_LIST}; do echo -n "-puu_${u} "; done;)" - outputs CARGO_UTILITY_LIST_OPTIONS - - name: Test - run: cargo nextest run --profile ci --hide-progress-bar ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} -p uucore -p coreutils - env: - RUSTC_WRAPPER: "" - RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" - RUSTDOCFLAGS: "-Cpanic=abort" - RUST_BACKTRACE: "1" - # RUSTUP_TOOLCHAIN: ${{ steps.vars.outputs.TOOLCHAIN }} - - name: Test individual utilities - run: cargo nextest run --profile ci --hide-progress-bar ${{ steps.dep_vars.outputs.CARGO_UTILITY_LIST_OPTIONS }} + + # Run the coverage script + ./util/build-run-test-coverage-linux.sh + + outputs REPORT_FILE env: - RUSTC_WRAPPER: "" - RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" - RUSTDOCFLAGS: "-Cpanic=abort" - RUST_BACKTRACE: "1" + COVERAGE_DIR: ${{ github.workspace }}/coverage + FEATURES_OPTION: ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} # RUSTUP_TOOLCHAIN: ${{ steps.vars.outputs.TOOLCHAIN }} - - name: Generate coverage data (via `grcov`) - id: coverage - shell: bash - run: | - ## Generate coverage data - COVERAGE_REPORT_DIR="target/debug" - COVERAGE_REPORT_FILE="${COVERAGE_REPORT_DIR}/lcov.info" - # GRCOV_IGNORE_OPTION='--ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*"' ## `grcov` ignores these params when passed as an environment variable (why?) - # GRCOV_EXCLUDE_OPTION='--excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()"' ## `grcov` ignores these params when passed as an environment variable (why?) - mkdir -p "${COVERAGE_REPORT_DIR}" - # display coverage files - ~/.cargo/bin/grcov . --output-type files --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" | sort --unique - # generate coverage report - ~/.cargo/bin/grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" - echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT + - name: Upload coverage results (to Codecov.io) - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} - file: ${{ steps.coverage.outputs.report }} + files: ${{ steps.run_test_cov.outputs.report }} ## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }} flags: ${{ steps.vars.outputs.CODECOV_FLAGS }} name: codecov-umbrella fail_ci_if_error: false + + test_separately: + name: Separate Builds + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: build and test all programs individually + shell: bash + run: | + for f in $(util/show-utils.sh) + do + echo "Building and testing $f" + cargo test -p "uu_$f" || exit 1 + done + + test_all_features: + name: Test all features separately + needs: [ min_version, deps ] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + # windows-latest - https://github.com/uutils/coreutils/issues/7044 + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: build and test all features individually + shell: bash + run: | + for f in $(util/show-utils.sh) + do + echo "Running tests with --features=$f and --no-default-features" + cargo test --features=$f --no-default-features + done + + test_selinux: + name: Build/SELinux + needs: [ min_version, deps ] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - name: Setup Lima + uses: lima-vm/lima-actions/setup@v1 + id: lima-actions-setup + - name: Cache ~/.cache/lima + uses: actions/cache@v4 + with: + path: ~/.cache/lima + key: lima-${{ steps.lima-actions-setup.outputs.version }} + - name: Start Fedora VM with SELinux + run: limactl start --plain --name=default --cpus=4 --disk=30 --memory=4 --network=lima:user-v2 template://fedora + - name: Setup SSH + uses: lima-vm/lima-actions/ssh@v1 + - run: rsync -v -a -e ssh . lima-default:~/work/ + - name: Setup Rust and other build deps in VM + run: | + lima sudo dnf install gcc g++ git rustup libselinux-devel clang-devel attr -y + lima rustup-init -y --default-toolchain stable + - name: Verify SELinux Status + run: | + lima getenforce + lima ls -laZ /etc/selinux + - name: Build and Test with SELinux + run: | + lima ls + lima bash -c "cd work && cargo test --features 'feat_selinux'" + - name: Lint with SELinux + run: lima bash -c "cd work && cargo clippy --all-targets --features 'feat_selinux' -- -D warnings" diff --git a/.github/workflows/CheckScripts.yml b/.github/workflows/CheckScripts.yml index c18c4733cbe..f6f13bbe6c4 100644 --- a/.github/workflows/CheckScripts.yml +++ b/.github/workflows/CheckScripts.yml @@ -1,6 +1,6 @@ name: CheckScripts -# spell-checker:ignore ludeeus mfinelli +# spell-checker:ignore ludeeus mfinelli shellcheck scandir shfmt env: SCRIPT_DIR: 'util' @@ -8,7 +8,7 @@ env: on: push: branches: - - main + - '*' paths: - 'util/**/*.sh' pull_request: @@ -29,7 +29,9 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Run ShellCheck uses: ludeeus/action-shellcheck@master env: @@ -45,7 +47,9 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Setup shfmt uses: mfinelli/setup-shfmt@v3 - name: Run shfmt diff --git a/.github/workflows/FixPR.yml b/.github/workflows/FixPR.yml index e837b354687..d9ac2dbdaf4 100644 --- a/.github/workflows/FixPR.yml +++ b/.github/workflows/FixPR.yml @@ -1,6 +1,6 @@ name: FixPR -# spell-checker:ignore Swatinem dtolnay +# spell-checker:ignore Swatinem dtolnay dedupe # Trigger automated fixes for PRs being merged (with associated commits) @@ -26,7 +26,9 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Initialize job variables id: vars shell: bash @@ -41,9 +43,11 @@ jobs: - name: Ensure updated 'Cargo.lock' shell: bash run: | - # Ensure updated 'Cargo.lock' - # * 'Cargo.lock' is required to be in a format that `cargo` of MinSRV can interpret (eg, v1-format for MinSRV < v1.38) - cargo fetch --locked --quiet || cargo +${{ steps.vars.outputs.RUST_MIN_SRV }} update + # Ensure updated '*/Cargo.lock' + # * '*/Cargo.lock' is required to be in a format that `cargo` of MinSRV can interpret (eg, v1-format for MinSRV < v1.38) + for dir in "." "fuzz"; do + ( cd "$dir" && (cargo fetch --locked --quiet || cargo +${{ steps.vars.outputs.RUST_MIN_SRV }} update) ) + done - name: Info shell: bash run: | @@ -69,8 +73,8 @@ jobs: with: new_branch: ${{ env.BRANCH_TARGET }} default_author: github_actions - message: "maint ~ refresh 'Cargo.lock'" - add: Cargo.lock + message: "maint ~ refresh 'Cargo.lock' 'fuzz/Cargo.lock'" + add: Cargo.lock fuzz/Cargo.lock env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -85,7 +89,9 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Initialize job variables id: vars shell: bash diff --git a/.github/workflows/GnuComment.yml b/.github/workflows/GnuComment.yml index 36c54490ce9..7fe42070e82 100644 --- a/.github/workflows/GnuComment.yml +++ b/.github/workflows/GnuComment.yml @@ -1,10 +1,12 @@ name: GnuComment +# spell-checker:ignore zizmor backquote + on: workflow_run: workflows: ["GnuTests"] types: - - completed + - completed # zizmor: ignore[dangerous-triggers] permissions: {} jobs: diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 6a4676a7913..0a98a36914e 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -1,8 +1,8 @@ name: GnuTests # spell-checker:ignore (abbrev/names) CodeCov gnulib GnuTests Swatinem -# spell-checker:ignore (jargon) submodules -# spell-checker:ignore (libs/utils) autopoint chksum gperf lcov libexpect pyinotify shopt texinfo valgrind libattr libcap taiki-e +# spell-checker:ignore (jargon) submodules devel +# spell-checker:ignore (libs/utils) autopoint chksum getenforce gperf lcov libexpect limactl pyinotify setenforce shopt texinfo valgrind libattr libcap taiki-e # spell-checker:ignore (options) Ccodegen Coverflow Cpanic Zpanic # spell-checker:ignore (people) Dawid Dziurla * dawidd dtolnay # spell-checker:ignore (vars) FILESET SUBDIRS XPASS @@ -13,7 +13,7 @@ on: pull_request: push: branches: - - main + - '*' permissions: contents: read @@ -23,75 +23,55 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} +env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + TEST_FULL_SUMMARY_FILE: 'gnu-full-result.json' + TEST_ROOT_FULL_SUMMARY_FILE: 'gnu-root-full-result.json' + TEST_SELINUX_FULL_SUMMARY_FILE: 'selinux-gnu-full-result.json' + TEST_SELINUX_ROOT_FULL_SUMMARY_FILE: 'selinux-root-gnu-full-result.json' + REPO_GNU_REF: "v9.7" + jobs: - gnu: - permissions: - actions: read # for dawidd6/action-download-artifact to query and download artifacts - contents: read # for actions/checkout to fetch code - pull-requests: read # for dawidd6/action-download-artifact to query commit hash - name: Run GNU tests - runs-on: ubuntu-22.04 + native: + name: Run GNU tests (native) + runs-on: ubuntu-24.04 steps: - - name: Initialize workflow variables - id: vars - shell: bash - run: | - ## VARs setup - outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } - # * config - path_GNU="gnu" - path_GNU_tests="${path_GNU}/tests" - path_UUTILS="uutils" - path_reference="reference" - outputs path_GNU path_GNU_tests path_reference path_UUTILS - # - repo_default_branch="${{ github.event.repository.default_branch }}" - repo_GNU_ref="v9.4" - repo_reference_branch="${{ github.event.repository.default_branch }}" - outputs repo_default_branch repo_GNU_ref repo_reference_branch - # - SUITE_LOG_FILE="${path_GNU_tests}/test-suite.log" - ROOT_SUITE_LOG_FILE="${path_GNU_tests}/test-suite-root.log" - TEST_LOGS_GLOB="${path_GNU_tests}/**/*.log" ## note: not usable at bash CLI; [why] double globstar not enabled by default b/c MacOS includes only bash v3 which doesn't have double globstar support - TEST_FILESET_PREFIX='test-fileset-IDs.sha1#' - TEST_FILESET_SUFFIX='.txt' - TEST_SUMMARY_FILE='gnu-result.json' - TEST_FULL_SUMMARY_FILE='gnu-full-result.json' - outputs SUITE_LOG_FILE ROOT_SUITE_LOG_FILE TEST_FILESET_PREFIX TEST_FILESET_SUFFIX TEST_LOGS_GLOB TEST_SUMMARY_FILE TEST_FULL_SUMMARY_FILE - - name: Checkout code (uutil) - uses: actions/checkout@v4 + #### Get the code, setup cache + - name: Checkout code (uutils) + uses: actions/checkout@v5 with: - path: '${{ steps.vars.outputs.path_UUTILS }}' + path: 'uutils' + persist-credentials: false - uses: dtolnay/rust-toolchain@master with: toolchain: stable components: rustfmt - uses: Swatinem/rust-cache@v2 with: - workspaces: "./${{ steps.vars.outputs.path_UUTILS }} -> target" + workspaces: "./uutils -> target" - name: Checkout code (GNU coreutils) - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'coreutils/coreutils' - path: '${{ steps.vars.outputs.path_GNU }}' - ref: ${{ steps.vars.outputs.repo_GNU_ref }} - submodules: recursive - - name: Retrieve reference artifacts - uses: dawidd6/action-download-artifact@v3 - # ref: - continue-on-error: true ## don't break the build for missing reference artifacts (may be expired or just not generated yet) - with: - workflow: GnuTests.yml - branch: "${{ steps.vars.outputs.repo_reference_branch }}" - # workflow_conclusion: success ## (default); * but, if commit with failed GnuTests is merged into the default branch, future commits will all show regression errors in GnuTests CI until o/w fixed - workflow_conclusion: completed ## continually recalibrates to last commit of default branch with a successful GnuTests (ie, "self-heals" from GnuTest regressions, but needs more supervision for/of regressions) - path: "${{ steps.vars.outputs.path_reference }}" + path: 'gnu' + ref: ${{ env.REPO_GNU_REF }} + submodules: false + persist-credentials: false + - name: Override submodule URL and initialize submodules + # Use github instead of upstream git server + run: | + git submodule sync --recursive + git config submodule.gnulib.url https://github.com/coreutils/gnulib.git + git submodule update --init --recursive --depth 1 + working-directory: gnu + + #### Build environment setup - name: Install dependencies shell: bash run: | ## Install dependencies sudo apt-get update - sudo apt-get install -y autoconf autopoint bison texinfo gperf gcc g++ gdb python3-pyinotify jq valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev + sudo apt-get install -y autoconf autopoint bison texinfo gperf gcc g++ gdb python3-pyinotify jq valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev libselinux1-dev attr quilt - name: Add various locales shell: bash run: | @@ -103,226 +83,388 @@ jobs: sudo locale-gen 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 sv_SE sudo locale-gen --keep-existing sv_SE.UTF-8 sudo locale-gen --keep-existing en_US + sudo locale-gen --keep-existing en_US.UTF-8 sudo locale-gen --keep-existing ru_RU.KOI8-R sudo update-locale echo "After:" locale -a + + ### Build - name: Build binaries shell: bash run: | ## Build binaries - cd '${{ steps.vars.outputs.path_UUTILS }}' + cd 'uutils' bash util/build-gnu.sh --release-build + + ### Run tests as user - name: Run GNU tests shell: bash run: | ## Run GNU tests - path_GNU='${{ steps.vars.outputs.path_GNU }}' - path_UUTILS='${{ steps.vars.outputs.path_UUTILS }}' - bash "${path_UUTILS}/util/run-gnu-test.sh" + path_GNU='gnu' + path_UUTILS='uutils' + bash "uutils/util/run-gnu-test.sh" + - name: Extract testing info from individual logs into JSON + shell: bash + run : | + path_UUTILS='uutils' + python uutils/util/gnu-json-result.py gnu/tests > ${{ env.TEST_FULL_SUMMARY_FILE }} + + ### Run tests as root - name: Run GNU root tests shell: bash run: | ## Run GNU root tests - path_GNU='${{ steps.vars.outputs.path_GNU }}' - path_UUTILS='${{ steps.vars.outputs.path_UUTILS }}' - bash "${path_UUTILS}/util/run-gnu-test.sh" run-root - - name: Extract testing info into JSON + path_GNU='gnu' + path_UUTILS='uutils' + bash "uutils/util/run-gnu-test.sh" run-root + - name: Extract testing info from individual logs (run as root) into JSON shell: bash run : | - ## Extract testing info into JSON - path_UUTILS='${{ steps.vars.outputs.path_UUTILS }}' - python ${path_UUTILS}/util/gnu-json-result.py ${{ steps.vars.outputs.path_GNU_tests }} > ${{ steps.vars.outputs.TEST_FULL_SUMMARY_FILE }} + path_UUTILS='uutils' + python uutils/util/gnu-json-result.py gnu/tests > ${{ env.TEST_ROOT_FULL_SUMMARY_FILE }} + + ### Upload artifacts + - name: Upload full json results + uses: actions/upload-artifact@v4 + with: + name: gnu-full-result + path: ${{ env.TEST_FULL_SUMMARY_FILE }} + - name: Upload root json results + uses: actions/upload-artifact@v4 + with: + name: gnu-root-full-result + path: ${{ env.TEST_ROOT_FULL_SUMMARY_FILE }} + - name: Compress test logs + shell: bash + run : | + # Compress logs before upload (fails otherwise) + gzip gnu/tests/*/*.log + - name: Upload test logs + uses: actions/upload-artifact@v4 + with: + name: test-logs + path: | + gnu/tests/*.log + gnu/tests/*/*.log.gz + + selinux: + name: Run GNU tests (SELinux) + runs-on: ubuntu-24.04 + steps: + #### Get the code, setup cache + - name: Checkout code (uutils) + uses: actions/checkout@v5 + with: + path: 'uutils' + persist-credentials: false + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: rustfmt + - uses: Swatinem/rust-cache@v2 + with: + workspaces: "./uutils -> target" + - name: Checkout code (GNU coreutils) + uses: actions/checkout@v5 + with: + repository: 'coreutils/coreutils' + path: 'gnu' + ref: ${{ env.REPO_GNU_REF }} + submodules: false + persist-credentials: false + - name: Override submodule URL and initialize submodules + # Use github instead of upstream git server + run: | + git submodule sync --recursive + git config submodule.gnulib.url https://github.com/coreutils/gnulib.git + git submodule update --init --recursive --depth 1 + working-directory: gnu + + #### Lima build environment setup + - name: Setup Lima + uses: lima-vm/lima-actions/setup@v1 + id: lima-actions-setup + - name: Cache ~/.cache/lima + uses: actions/cache@v4 + with: + path: ~/.cache/lima + key: lima-${{ steps.lima-actions-setup.outputs.version }} + - name: Start Fedora VM with SELinux + run: limactl start --plain --name=default --cpus=4 --disk=40 --memory=8 --network=lima:user-v2 template://fedora + - name: Setup SSH + uses: lima-vm/lima-actions/ssh@v1 + - name: Verify SELinux Status and Configuration + run: | + lima getenforce + lima ls -laZ /etc/selinux + lima sudo sestatus + + # Ensure we're running in enforcing mode + lima sudo setenforce 1 + lima getenforce + + # Create test files with SELinux contexts for testing + lima sudo mkdir -p /var/test_selinux + lima sudo touch /var/test_selinux/test_file + lima sudo chcon -t etc_t /var/test_selinux/test_file + lima ls -Z /var/test_selinux/test_file # Verify context + - name: Install dependencies in VM + run: | + lima sudo dnf -y update + lima sudo dnf -y install git autoconf autopoint bison texinfo gperf gcc g++ gdb jq libacl-devel libattr-devel libcap-devel libselinux-devel attr rustup clang-devel texinfo-tex wget automake patch quilt + lima rustup-init -y --default-toolchain stable + - name: Copy the sources to VM + run: | + rsync -a -e ssh . lima-default:~/work/ + + ### Build + - name: Build binaries + run: | + lima bash -c "cd ~/work/uutils/ && bash util/build-gnu.sh --release-build" + + ### Run tests as user + - name: Generate SELinux tests list + run: | + # Find and list all tests that require SELinux + lima bash -c "cd ~/work/gnu/ && grep -l 'require_selinux_' -r tests/ > ~/work/uutils/selinux-tests.txt" + lima bash -c "cd ~/work/uutils/ && cat selinux-tests.txt" + + # Count the tests + lima bash -c "cd ~/work/uutils/ && echo 'Found SELinux tests:'; wc -l selinux-tests.txt" + - name: Run GNU SELinux tests + run: | + lima sudo setenforce 1 + lima getenforce + lima cat /proc/filesystems + lima bash -c "cd ~/work/uutils/ && bash util/run-gnu-test.sh \$(cat selinux-tests.txt)" + - name: Extract testing info from individual logs into JSON + shell: bash + run : | + lima bash -c "cd ~/work/gnu/ && python3 ../uutils/util/gnu-json-result.py tests > ~/work/${{ env.TEST_SELINUX_FULL_SUMMARY_FILE }}" + + ### Run tests as root + - name: Run GNU SELinux root tests + run: | + lima bash -c "cd ~/work/uutils/ && CI=1 bash util/run-gnu-test.sh run-root \$(cat selinux-tests.txt)" + - name: Extract testing info from individual logs (run as root) into JSON + shell: bash + run : | + lima bash -c "cd ~/work/gnu/ && python3 ../uutils/util/gnu-json-result.py tests > ~/work/${{ env.TEST_SELINUX_ROOT_FULL_SUMMARY_FILE }}" + + ### Upload artifacts + - name: Collect test logs and test results from VM + run: | + mkdir -p gnu/tests-selinux + + # Copy the json output back from the Lima VM to the host + rsync -v -a -e ssh lima-default:~/work/*.json ./ + # Copy the test directory now + rsync -v -a -e ssh lima-default:~/work/gnu/tests/ ./gnu/tests-selinux/ + - name: Upload SELinux json results + uses: actions/upload-artifact@v4 + with: + name: selinux-gnu-full-result + path: ${{ env.TEST_SELINUX_FULL_SUMMARY_FILE }} + - name: Upload SELinux root json results + uses: actions/upload-artifact@v4 + with: + name: selinux-root-gnu-full-result + path: ${{ env.TEST_SELINUX_ROOT_FULL_SUMMARY_FILE }} + - name: Compress SELinux test logs + shell: bash + run : | + # Compress logs before upload (fails otherwise) + gzip gnu/tests-selinux/*/*.log + - name: Upload SELinux test logs + uses: actions/upload-artifact@v4 + with: + name: selinux-test-logs + path: | + gnu/tests-selinux/*.log + gnu/tests-selinux/*/*.log.gz + + aggregate: + needs: [native, selinux] + permissions: + actions: read # for dawidd6/action-download-artifact to query and download artifacts + contents: read # for actions/checkout to fetch code + pull-requests: read # for dawidd6/action-download-artifact to query commit hash + name: Aggregate GNU test results + runs-on: ubuntu-24.04 + steps: + - name: Initialize workflow variables + id: vars + shell: bash + run: | + ## VARs setup + outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } + # + TEST_SUMMARY_FILE='gnu-result.json' + AGGREGATED_SUMMARY_FILE='aggregated-result.json' + + outputs TEST_SUMMARY_FILE AGGREGATED_SUMMARY_FILE + - name: Checkout code (uutils) + uses: actions/checkout@v5 + with: + path: 'uutils' + persist-credentials: false + - name: Retrieve reference artifacts + uses: dawidd6/action-download-artifact@v11 + # ref: + continue-on-error: true ## don't break the build for missing reference artifacts (may be expired or just not generated yet) + with: + workflow: GnuTests.yml + branch: "${{ env.DEFAULT_BRANCH }}" + # workflow_conclusion: success ## (default); * but, if commit with failed GnuTests is merged into the default branch, future commits will all show regression errors in GnuTests CI until o/w fixed + workflow_conclusion: completed ## continually recalibrates to last commit of default branch with a successful GnuTests (ie, "self-heals" from GnuTest regressions, but needs more supervision for/of regressions) + path: "reference" + - name: Download full json results + uses: actions/download-artifact@v5 + with: + name: gnu-full-result + path: results + merge-multiple: true + - name: Download root json results + uses: actions/download-artifact@v5 + with: + name: gnu-root-full-result + path: results + merge-multiple: true + - name: Download selinux json results + uses: actions/download-artifact@v5 + with: + name: selinux-gnu-full-result + path: results + merge-multiple: true + - name: Download selinux root json results + uses: actions/download-artifact@v5 + with: + name: selinux-root-gnu-full-result + path: results + merge-multiple: true - name: Extract/summarize testing info id: summary shell: bash run: | ## Extract/summarize testing info outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } - # - path_UUTILS='${{ steps.vars.outputs.path_UUTILS }}' - # - SUITE_LOG_FILE='${{ steps.vars.outputs.SUITE_LOG_FILE }}' - ROOT_SUITE_LOG_FILE='${{ steps.vars.outputs.ROOT_SUITE_LOG_FILE }}' - ls -al ${SUITE_LOG_FILE} ${ROOT_SUITE_LOG_FILE} - - if test -f "${SUITE_LOG_FILE}" - then - source ${path_UUTILS}/util/analyze-gnu-results.sh ${SUITE_LOG_FILE} ${ROOT_SUITE_LOG_FILE} - if [[ "$TOTAL" -eq 0 || "$TOTAL" -eq 1 ]]; then - echo "::error ::Failed to parse test results from '${SUITE_LOG_FILE}'; failing early" - exit 1 - fi - output="GNU tests summary = TOTAL: $TOTAL / PASS: $PASS / FAIL: $FAIL / ERROR: $ERROR / SKIP: $SKIP" - echo "${output}" - if [[ "$FAIL" -gt 0 || "$ERROR" -gt 0 ]]; then echo "::warning ::${output}" ; fi - jq -n \ - --arg date "$(date --rfc-email)" \ - --arg sha "$GITHUB_SHA" \ - --arg total "$TOTAL" \ - --arg pass "$PASS" \ - --arg skip "$SKIP" \ - --arg fail "$FAIL" \ - --arg xpass "$XPASS" \ - --arg error "$ERROR" \ - '{($date): { sha: $sha, total: $total, pass: $pass, skip: $skip, fail: $fail, xpass: $xpass, error: $error, }}' > '${{ steps.vars.outputs.TEST_SUMMARY_FILE }}' - HASH=$(sha1sum '${{ steps.vars.outputs.TEST_SUMMARY_FILE }}' | cut --delim=" " -f 1) - outputs HASH - else - echo "::error ::Failed to find summary of test results (missing '${SUITE_LOG_FILE}'); failing early" - exit 1 - fi - # Compress logs before upload (fails otherwise) - gzip ${{ steps.vars.outputs.TEST_LOGS_GLOB }} - - name: Reserve SHA1/ID of 'test-summary' + + path_UUTILS='uutils' + + json_count=$(ls -l results/*.json | wc -l) + if [[ "$json_count" -ne 4 ]]; then + echo "::error ::Failed to download all results json files (expected 4 files, found $json_count); failing early" + ls -lR results || true + exit 1 + fi + + # Look at all individual results and summarize + eval $(python3 uutils/util/analyze-gnu-results.py -o=${{ steps.vars.outputs.AGGREGATED_SUMMARY_FILE }} results/*.json) + + if [[ "$TOTAL" -eq 0 || "$TOTAL" -eq 1 ]]; then + echo "::error ::Failed to parse test results from '${{ env.TEST_FULL_SUMMARY_FILE }}'; failing early" + exit 1 + fi + + output="GNU tests summary = TOTAL: $TOTAL / PASS: $PASS / FAIL: $FAIL / ERROR: $ERROR / SKIP: $SKIP" + echo "${output}" + + if [[ "$FAIL" -gt 0 || "$ERROR" -gt 0 ]]; then + echo "::warning ::${output}" + fi + + jq -n \ + --arg date "$(date --rfc-email)" \ + --arg sha "$GITHUB_SHA" \ + --arg total "$TOTAL" \ + --arg pass "$PASS" \ + --arg skip "$SKIP" \ + --arg fail "$FAIL" \ + --arg xpass "$XPASS" \ + --arg error "$ERROR" \ + '{($date): { sha: $sha, total: $total, pass: $pass, skip: $skip, fail: $fail, xpass: $xpass, error: $error, }}' > '${{ steps.vars.outputs.TEST_SUMMARY_FILE }}' + HASH=$(sha1sum '${{ steps.vars.outputs.TEST_SUMMARY_FILE }}' | cut --delim=" " -f 1) + outputs HASH + - name: Upload SHA1/ID of 'test-summary' uses: actions/upload-artifact@v4 with: name: "${{ steps.summary.outputs.HASH }}" path: "${{ steps.vars.outputs.TEST_SUMMARY_FILE }}" - - name: Reserve test results summary + - name: Upload test results summary uses: actions/upload-artifact@v4 with: name: test-summary path: "${{ steps.vars.outputs.TEST_SUMMARY_FILE }}" - - name: Reserve test logs + - name: Upload aggregated json results uses: actions/upload-artifact@v4 with: - name: test-logs - path: "${{ steps.vars.outputs.TEST_LOGS_GLOB }}" - - name: Upload full json results - uses: actions/upload-artifact@v4 - with: - name: gnu-full-result.json - path: ${{ steps.vars.outputs.TEST_FULL_SUMMARY_FILE }} + name: aggregated-result + path: ${{ steps.vars.outputs.AGGREGATED_SUMMARY_FILE }} - name: Compare test failures VS reference shell: bash run: | - ## Compare test failures VS reference - have_new_failures="" - REF_LOG_FILE='${{ steps.vars.outputs.path_reference }}/test-logs/test-suite.log' - ROOT_REF_LOG_FILE='${{ steps.vars.outputs.path_reference }}/test-logs/test-suite-root.log' - REF_SUMMARY_FILE='${{ steps.vars.outputs.path_reference }}/test-summary/gnu-result.json' - REPO_DEFAULT_BRANCH='${{ steps.vars.outputs.repo_default_branch }}' - path_UUTILS='${{ steps.vars.outputs.path_UUTILS }}' - # https://github.com/uutils/coreutils/issues/4294 - # https://github.com/uutils/coreutils/issues/4295 - IGNORE_INTERMITTENT="${path_UUTILS}/.github/workflows/ignore-intermittent.txt" - - mkdir -p ${{ steps.vars.outputs.path_reference }} - - COMMENT_DIR="${{ steps.vars.outputs.path_reference }}/comment" + ## Compare test failures VS reference using JSON files + REF_SUMMARY_FILE='reference/aggregated-result/aggregated-result.json' + CURRENT_SUMMARY_FILE='${{ steps.vars.outputs.AGGREGATED_SUMMARY_FILE }}' + REPO_DEFAULT_BRANCH='${{ env.DEFAULT_BRANCH }}' + path_UUTILS='uutils' + + # Path to ignore file for intermittent issues + IGNORE_INTERMITTENT="uutils/.github/workflows/ignore-intermittent.txt" + + # Set up comment directory + COMMENT_DIR="reference/comment" mkdir -p ${COMMENT_DIR} echo ${{ github.event.number }} > ${COMMENT_DIR}/NR COMMENT_LOG="${COMMENT_DIR}/result.txt" - # The comment log might be downloaded from a previous run - # We only want the new changes, so remove it if it exists. - rm -f ${COMMENT_LOG} - touch ${COMMENT_LOG} - - compare_tests() { - local new_log_file=$1 - local ref_log_file=$2 - local test_type=$3 # "standard" or "root" - - if test -f "${ref_log_file}"; then - echo "Reference ${test_type} test log SHA1/ID: $(sha1sum -- "${ref_log_file}") - ${test_type}" - REF_ERROR=$(sed -n "s/^ERROR: \([[:print:]]\+\).*/\1/p" "${ref_log_file}"| sort) - CURRENT_RUN_ERROR=$(sed -n "s/^ERROR: \([[:print:]]\+\).*/\1/p" "${new_log_file}" | sort) - REF_FAILING=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" "${ref_log_file}"| sort) - CURRENT_RUN_FAILING=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" "${new_log_file}" | sort) - echo "Detailled information:" - echo "REF_ERROR = ${REF_ERROR}" - echo "CURRENT_RUN_ERROR = ${CURRENT_RUN_ERROR}" - echo "REF_FAILING = ${REF_FAILING}" - echo "CURRENT_RUN_FAILING = ${CURRENT_RUN_FAILING}" - - # Compare failing and error tests - for LINE in ${CURRENT_RUN_FAILING} - do - if ! grep -Fxq ${LINE}<<<"${REF_FAILING}" - then - if ! grep ${LINE} ${IGNORE_INTERMITTENT} - then - MSG="GNU test failed: ${LINE}. ${LINE} is passing on '${REPO_DEFAULT_BRANCH}'. Maybe you have to rebase?" - echo "::error ::$MSG" - echo $MSG >> ${COMMENT_LOG} - have_new_failures="true" - else - MSG="Skip an intermittent issue ${LINE} (fails in this run but passes in the 'main' branch)" - echo "::warning ::$MSG" - echo $MSG >> ${COMMENT_LOG} - echo "" - fi - fi - done - - for LINE in ${REF_FAILING} - do - if ! grep -Fxq ${LINE}<<<"${CURRENT_RUN_FAILING}" - then - if ! grep ${LINE} ${IGNORE_INTERMITTENT} - then - MSG="Congrats! The gnu test ${LINE} is no longer failing!" - echo "::warning ::$MSG" - echo $MSG >> ${COMMENT_LOG} - else - MSG="Skipping an intermittent issue ${LINE} (passes in this run but fails in the 'main' branch)" - echo "::warning ::$MSG" - echo $MSG >> ${COMMENT_LOG} - echo "" - fi - fi - done - - for LINE in ${CURRENT_RUN_ERROR} - do - if ! grep -Fxq ${LINE}<<<"${REF_ERROR}" - then - MSG="GNU test error: ${LINE}. ${LINE} is passing on '${REPO_DEFAULT_BRANCH}'. Maybe you have to rebase?" - echo "::error ::$MSG" - echo $MSG >> ${COMMENT_LOG} - have_new_failures="true" - fi - done - - for LINE in ${REF_ERROR} - do - if ! grep -Fxq ${LINE}<<<"${CURRENT_RUN_ERROR}" - then - MSG="Congrats! The gnu test ${LINE} is no longer ERROR!" - echo "::warning ::$MSG" - echo $MSG >> ${COMMENT_LOG} - fi - done - else - echo "::warning ::Skipping ${test_type} test failure comparison; no prior reference test logs are available." - fi - } + COMPARISON_RESULT=0 + if test -f "${CURRENT_SUMMARY_FILE}"; then + if test -f "${REF_SUMMARY_FILE}"; then + echo "Reference summary SHA1/ID: $(sha1sum -- "${REF_SUMMARY_FILE}")" + echo "Current summary SHA1/ID: $(sha1sum -- "${CURRENT_SUMMARY_FILE}")" - # Compare standard tests - compare_tests '${{ steps.vars.outputs.path_GNU_tests }}/test-suite.log' "${REF_LOG_FILE}" "standard" + python3 uutils/util/compare_test_results.py \ + --ignore-file "${IGNORE_INTERMITTENT}" \ + --output "${COMMENT_LOG}" \ + "${CURRENT_SUMMARY_FILE}" "${REF_SUMMARY_FILE}" - # Compare root tests - compare_tests '${{ steps.vars.outputs.path_GNU_tests }}/test-suite-root.log' "${ROOT_REF_LOG_FILE}" "root" + COMPARISON_RESULT=$? + else + echo "::warning ::Skipping test comparison; no prior reference summary is available at '${REF_SUMMARY_FILE}'." + fi + else + echo "::error ::Failed to find summary of test results (missing '${CURRENT_SUMMARY_FILE}'); failing early" + exit 1 + fi - if test -n "${have_new_failures}" ; then exit -1 ; fi + if [ ${COMPARISON_RESULT} -eq 1 ]; then + echo "ONLY_INTERMITTENT=false" >> $GITHUB_ENV + echo "::error ::Found new non-intermittent test failures" + exit 1 + else + echo "ONLY_INTERMITTENT=true" >> $GITHUB_ENV + echo "::notice ::No new test failures detected" + fi - name: Upload comparison log (for GnuComment workflow) if: success() || failure() # run regardless of prior step success/failure uses: actions/upload-artifact@v4 with: name: comment - path: ${{ steps.vars.outputs.path_reference }}/comment/ + path: reference/comment/ - name: Compare test summary VS reference if: success() || failure() # run regardless of prior step success/failure shell: bash run: | ## Compare test summary VS reference - REF_SUMMARY_FILE='${{ steps.vars.outputs.path_reference }}/test-summary/gnu-result.json' + REF_SUMMARY_FILE='reference/test-summary/gnu-result.json' if test -f "${REF_SUMMARY_FILE}"; then echo "Reference SHA1/ID: $(sha1sum -- "${REF_SUMMARY_FILE}")" mv "${REF_SUMMARY_FILE}" main-gnu-result.json @@ -330,77 +472,3 @@ jobs: else echo "::warning ::Skipping test summary comparison; no prior reference summary is available." fi - - gnu_coverage: - name: Run GNU tests with coverage - runs-on: ubuntu-22.04 - steps: - - name: Checkout code uutil - uses: actions/checkout@v4 - with: - path: 'uutils' - - name: Checkout GNU coreutils - uses: actions/checkout@v4 - with: - repository: 'coreutils/coreutils' - path: 'gnu' - ref: 'v9.4' - submodules: recursive - - uses: dtolnay/rust-toolchain@master - with: - toolchain: nightly - components: rustfmt - - uses: taiki-e/install-action@grcov - - uses: Swatinem/rust-cache@v2 - with: - workspaces: "./uutils -> target" - - name: Install dependencies - run: | - ## Install dependencies - sudo apt-get update - sudo apt-get install -y autoconf autopoint bison texinfo gperf gcc g++ gdb python3-pyinotify jq valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev - - name: Add various locales - run: | - ## Add various locales - echo "Before:" - locale -a - ## Some tests fail with 'cannot change locale (en_US.ISO-8859-1): No such file or directory' - ## Some others need a French locale - sudo locale-gen - sudo locale-gen --keep-existing fr_FR - sudo locale-gen --keep-existing fr_FR.UTF-8 - sudo update-locale - echo "After:" - locale -a - - name: Build binaries - env: - RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" - RUSTDOCFLAGS: "-Cpanic=abort" - run: | - ## Build binaries - cd uutils - bash util/build-gnu.sh - - name: Run GNU tests - run: bash uutils/util/run-gnu-test.sh - - name: Generate coverage data (via `grcov`) - id: coverage - run: | - ## Generate coverage data - cd uutils - COVERAGE_REPORT_DIR="target/debug" - COVERAGE_REPORT_FILE="${COVERAGE_REPORT_DIR}/lcov.info" - mkdir -p "${COVERAGE_REPORT_DIR}" - sudo chown -R "$(whoami)" "${COVERAGE_REPORT_DIR}" - # display coverage files - grcov . --output-type files --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" | sort --unique - # generate coverage report - grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" - echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT - - name: Upload coverage results (to Codecov.io) - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: ${{ steps.coverage.outputs.report }} - flags: gnutests - name: gnutests - working-directory: uutils diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 528c2ad496c..5856d0fdda7 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -1,12 +1,14 @@ name: Android -# spell-checker:ignore TERMUX reactivecircus Swatinem noaudio pkill swiftshader dtolnay juliangruber +# spell-checker:ignore (people) reactivecircus Swatinem dtolnay juliangruber +# spell-checker:ignore (shell/tools) TERMUX nextest udevadm pkill +# spell-checker:ignore (misc) swiftshader playstore DATALOSS noaudio on: pull_request: push: branches: - - main + - '*' permissions: @@ -20,7 +22,7 @@ concurrency: env: TERMUX: v0.118.0 KEY_POSTFIX: nextest+rustc-hash+adb+sshd+upgrade+XGB+inc18 - COMMON_EMULATOR_OPTIONS: -no-window -noaudio -no-boot-anim -camera-back none -gpu swiftshader_indirect + COMMON_EMULATOR_OPTIONS: -no-window -noaudio -no-boot-anim -camera-back none -gpu swiftshader_indirect -metrics-collection EMULATOR_DISK_SIZE: 12GB EMULATOR_HEAP_SIZE: 2048M EMULATOR_BOOT_TIMEOUT: 1200 # 20min @@ -78,7 +80,9 @@ jobs: echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Collect information about runner if: always() continue-on-error: true @@ -112,7 +116,7 @@ jobs: ~/.android/avd/*/*.lock - name: Create and cache emulator image if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@v2.30.1 + uses: reactivecircus/android-emulator-runner@v2.34.0 with: api-level: ${{ matrix.api-level }} target: ${{ matrix.target }} @@ -150,14 +154,14 @@ jobs: # The version vX at the end of the key is just a development version to avoid conflicts in # the github cache during the development of this workflow key: ${{ matrix.arch }}_${{ matrix.target}}_${{ steps.read_rustc_hash.outputs.content }}_${{ hashFiles('**/Cargo.toml', '**/Cargo.lock') }}_v3 - - name: Collect information about runner ressources + - name: Collect information about runner resources if: always() continue-on-error: true run: | free -mh df -Th - name: Build and Test - uses: reactivecircus/android-emulator-runner@v2.30.1 + uses: reactivecircus/android-emulator-runner@v2.34.0 with: api-level: ${{ matrix.api-level }} target: ${{ matrix.target }} @@ -176,8 +180,8 @@ jobs: util/android-commands.sh sync_host util/android-commands.sh build util/android-commands.sh tests - if [[ "${{ steps.rust-cache.outputs.cache-hit }}" != 'true' ]]; then util/android-commands.sh sync_image; fi; exit 0 - - name: Collect information about runner ressources + if [ "${{ steps.rust-cache.outputs.cache-hit }}" != 'true' ]; then util/android-commands.sh sync_image; fi; exit 0 + - name: Collect information about runner resources if: always() continue-on-error: true run: | @@ -195,7 +199,7 @@ jobs: with: name: test_output_${{ env.AVD_CACHE_KEY }} path: output - - name: Collect information about runner ressources + - name: Collect information about runner resources if: always() continue-on-error: true run: | diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 1879bfc78f6..b06ad4e6056 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -1,12 +1,13 @@ name: Code Quality -# spell-checker:ignore TERMUX reactivecircus Swatinem noaudio pkill swiftshader dtolnay juliangruber +# spell-checker:ignore (people) reactivecircus Swatinem dtolnay juliangruber pell taplo +# spell-checker:ignore (misc) TERMUX noaudio pkill swiftshader esac sccache pcoreutils shopt subshell dequote on: pull_request: push: branches: - - main + - '*' env: # * style job configuration @@ -31,7 +32,9 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -44,16 +47,11 @@ jobs: ## VARs setup outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } # failure mode - unset FAIL_ON_FAULT ; case '${{ env.STYLE_FAIL_ON_FAULT }}' in + unset FAIL_ON_FAULT ; case "$STYLE_FAIL_ON_FAULT" in ''|0|f|false|n|no|off) FAULT_TYPE=warning ;; *) FAIL_ON_FAULT=true ; FAULT_TYPE=error ;; esac; outputs FAIL_ON_FAULT FAULT_TYPE - # target-specific options - # * CARGO_FEATURES_OPTION - CARGO_FEATURES_OPTION='' ; - if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features "${{ matrix.job.features }}"' ; fi - outputs CARGO_FEATURES_OPTION - name: "`cargo fmt` testing" shell: bash run: | @@ -64,6 +62,11 @@ jobs: # * convert any errors/warnings to GHA UI annotations; ref: S=$(cargo fmt -- --check) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s\n" "$S" | sed -E -n -e "s/^Diff[[:space:]]+in[[:space:]]+${PWD//\//\\/}\/(.*)[[:space:]]+at[[:space:]]+[^0-9]+([0-9]+).*$/::${fault_type} file=\1,line=\2::${fault_prefix}: \`cargo fmt\`: style violation (file:'\1', line:\2; use \`cargo fmt -- \"\1\"\`)/p" ; fault=true ; } if [ -n "${{ steps.vars.outputs.FAIL_ON_FAULT }}" ] && [ -n "$fault" ]; then exit 1 ; fi + - name: "cargo fmt on fuzz dir" + shell: bash + run: | + cd fuzz + cargo fmt --check style_lint: name: Style/lint @@ -75,18 +78,20 @@ jobs: fail-fast: false matrix: job: - - { os: ubuntu-latest , features: feat_os_unix } + - { os: ubuntu-latest , features: all , workspace: true } - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@master with: toolchain: stable components: clippy - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.4 + uses: mozilla-actions/sccache-action@v0.0.9 - name: Initialize workflow variables id: vars shell: bash @@ -94,39 +99,46 @@ jobs: ## VARs setup outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } # failure mode - unset FAIL_ON_FAULT ; case '${{ env.STYLE_FAIL_ON_FAULT }}' in + unset FAIL_ON_FAULT ; case "$STYLE_FAIL_ON_FAULT" in ''|0|f|false|n|no|off) FAULT_TYPE=warning ;; *) FAIL_ON_FAULT=true ; FAULT_TYPE=error ;; esac; outputs FAIL_ON_FAULT FAULT_TYPE - # target-specific options - # * CARGO_FEATURES_OPTION - CARGO_FEATURES_OPTION='--all-features' ; - if [ -n "${{ matrix.job.features }}" ]; then CARGO_FEATURES_OPTION='--features ${{ matrix.job.features }}' ; fi - outputs CARGO_FEATURES_OPTION - # * determine sub-crate utility list - UTILITY_LIST="$(./util/show-utils.sh ${CARGO_FEATURES_OPTION})" - echo UTILITY_LIST=${UTILITY_LIST} - CARGO_UTILITY_LIST_OPTIONS="$(for u in ${UTILITY_LIST}; do echo -n "-puu_${u} "; done;)" - outputs CARGO_UTILITY_LIST_OPTIONS - name: Install/setup prerequisites shell: bash run: | ## Install/setup prerequisites case '${{ matrix.job.os }}' in - macos-latest) brew install coreutils ;; # needed for show-utils.sh + ubuntu-*) + # selinux headers needed to enable all features + sudo apt-get -y install libselinux1-dev + ;; esac - name: "`cargo clippy` lint testing" - shell: bash - run: | - ## `cargo clippy` lint testing - unset fault - CLIPPY_FLAGS="-W clippy::default_trait_access -W clippy::manual_string_new -W clippy::cognitive_complexity -W clippy::implicit_clone -W clippy::range-plus-one -W clippy::redundant-clone" - fault_type="${{ steps.vars.outputs.FAULT_TYPE }}" - fault_prefix=$(echo "$fault_type" | tr '[:lower:]' '[:upper:]') - # * convert any warnings to GHA UI annotations; ref: - S=$(cargo clippy --all-targets ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_UTILITY_LIST_OPTIONS }} -- ${CLIPPY_FLAGS} -D warnings 2>&1) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*$/::${fault_type} file=\2,line=\3,col=\4::${fault_prefix}: \`cargo clippy\`: \1 (file:'\2', line:\3)/p;" -e '}' ; fault=true ; } - if [ -n "${{ steps.vars.outputs.FAIL_ON_FAULT }}" ] && [ -n "$fault" ]; then exit 1 ; fi + uses: nick-fields/retry@v3 + with: + max_attempts: 3 + retry_on: error + timeout_minutes: 90 + 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 }}" + fi + case '${{ matrix.job.workspace-tests }}' in + 1|t|true|y|yes) + extra="${extra} --workspace" + ;; + esac + S=$(cargo clippy --all-targets $extra --tests -pcoreutils -- -D warnings 2>&1) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*$/::${fault_type} file=\2,line=\3,col=\4::${fault_prefix}: \`cargo clippy\`: \1 (file:'\2', line:\3)/p;" -e '}' ; fault=true ; } + if [ -n "${{ steps.vars.outputs.FAIL_ON_FAULT }}" ] && [ -n "$fault" ]; then exit 1 ; fi style_spellcheck: name: Style/spelling @@ -136,7 +148,9 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Initialize workflow variables id: vars shell: bash @@ -144,7 +158,7 @@ jobs: ## VARs setup outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } # failure mode - unset FAIL_ON_FAULT ; case '${{ env.STYLE_FAIL_ON_FAULT }}' in + unset FAIL_ON_FAULT ; case "$STYLE_FAIL_ON_FAULT" in ''|0|f|false|n|no|off) FAULT_TYPE=warning ;; *) FAIL_ON_FAULT=true ; FAULT_TYPE=error ;; esac; @@ -164,7 +178,7 @@ jobs: cfg_files=($(shopt -s nullglob ; echo {.vscode,.}/{,.}c[sS]pell{.json,.config{.js,.cjs,.json,.yaml,.yml},.yaml,.yml} ;)) cfg_file=${cfg_files[0]} unset CSPELL_CFG_OPTION ; if [ -n "$cfg_file" ]; then CSPELL_CFG_OPTION="--config $cfg_file" ; fi - S=$(cspell ${CSPELL_CFG_OPTION} --no-summary --no-progress "**/*") && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n "s/${PWD//\//\\/}\/(.*):(.*):(.*) - (.*)/::${fault_type} file=\1,line=\2,col=\3::${fault_type^^}: \4 (file:'\1', line:\2)/p" ; fault=true ; true ; } + S=$(cspell ${CSPELL_CFG_OPTION} --no-summary --no-progress .) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n "s/${PWD//\//\\/}\/(.*):(.*):(.*) - (.*)/::${fault_type} file=\1,line=\2,col=\3::${fault_type^^}: \4 (file:'\1', line:\2)/p" ; fault=true ; true ; } if [ -n "${{ steps.vars.outputs.FAIL_ON_FAULT }}" ] && [ -n "$fault" ]; then exit 1 ; fi toml_format: @@ -172,7 +186,73 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 + with: + persist-credentials: false - name: Check run: npx --yes @taplo/cli fmt --check + + python: + name: Style/Python + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: ruff + uses: astral-sh/ruff-action@v3 + with: + src: "./util" + + - name: ruff - format + uses: astral-sh/ruff-action@v3 + with: + src: "./util" + args: format --check + - name: Run Python unit tests + shell: bash + run: | + python3 -m unittest util/test_compare_test_results.py + + pre_commit: + name: Pre-commit hooks + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: rustfmt, clippy + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install pre-commit + run: pip install pre-commit + + - name: Install cspell + run: npm install -g cspell + + - name: Cache pre-commit environments + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + pre-commit-${{ runner.os }}- + + - name: Run pre-commit + run: pre-commit run diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index a2b3562ca3a..aec17355365 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 +# spell-checker:ignore sshfs usesh vmactions taiki Swatinem esac fdescfs fdesc sccache nextest copyback env: # * style job configuration @@ -10,7 +10,7 @@ on: pull_request: push: branches: - - main + - '*' permissions: contents: read # to fetch code (actions/checkout) @@ -24,22 +24,24 @@ jobs: style: name: Style and Lint runs-on: ${{ matrix.job.os }} - timeout-minutes: 90 + timeout-minutes: 45 strategy: fail-fast: false matrix: job: - - { os: ubuntu-22.04 , features: unix } + - { os: ubuntu-24.04 , features: unix } env: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.4 + uses: mozilla-actions/sccache-action@v0.0.9 - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.0.6 + uses: vmactions/freebsd-vm@v1.2.3 with: usesh: true sync: rsync @@ -105,7 +107,7 @@ jobs: 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} -- -W clippy::manual_string_new -D warnings 2>&1) && printf "%s\n" "\$S" || { printf "%s\n" "\$S" ; printf "%s" "\$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*\$/::\${FAULT_TYPE} file=\2,line=\3,col=\4::\${FAULT_PREFIX}: \\\`cargo clippy\\\`: \1 (file:'\2', line:\3)/p;" -e '}' ; FAULT=true ; } + S=\$(cargo clippy --all-targets \${CARGO_UTILITY_LIST_OPTIONS} -- -D warnings 2>&1) && printf "%s\n" "\$S" || { printf "%s\n" "\$S" ; printf "%s" "\$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*\$/::\${FAULT_TYPE} file=\2,line=\3,col=\4::\${FAULT_PREFIX}: \\\`cargo clippy\\\`: \1 (file:'\2', line:\3)/p;" -e '}' ; FAULT=true ; } fi # Clean to avoid to rsync back the files cargo clean @@ -115,23 +117,25 @@ jobs: test: name: Tests runs-on: ${{ matrix.job.os }} - timeout-minutes: 90 + timeout-minutes: 45 strategy: fail-fast: false matrix: job: - - { os: ubuntu-22.04 , features: unix } + - { os: ubuntu-24.04 , features: unix } env: mem: 4096 SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.4 + uses: mozilla-actions/sccache-action@v0.0.9 - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.0.6 + uses: vmactions/freebsd-vm@v1.2.3 with: usesh: true sync: rsync @@ -191,6 +195,8 @@ jobs: export CARGO_TERM_COLOR=always if (test -z "\$FAULT"); then cargo nextest run --hide-progress-bar --profile ci --features '${{ matrix.job.features }}' || FAULT=1 ; fi if (test -z "\$FAULT"); then cargo nextest run --hide-progress-bar --profile ci --all-features -p uucore || FAULT=1 ; fi + # Test building with make + if (test -z "\$FAULT"); then make PROFILE=ci || FAULT=1 ; fi # Clean to avoid to rsync back the files cargo clean if (test -n "\$FAULT"); then exit 1 ; fi diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index c60c01ff4be..cf8d943c3a8 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -1,12 +1,12 @@ name: Fuzzing -# spell-checker:ignore fuzzer +# spell-checker:ignore fuzzer dtolnay Swatinem on: pull_request: push: branches: - - main + - '*' permissions: contents: read # to fetch code (actions/checkout) @@ -21,7 +21,9 @@ jobs: name: Build the fuzzers runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@nightly - name: Install `cargo-fuzz` run: cargo install cargo-fuzz @@ -34,7 +36,7 @@ jobs: fuzz-run: needs: fuzz-build - name: Run the fuzzers + name: Fuzz runs-on: ubuntu-latest timeout-minutes: 5 env: @@ -46,18 +48,26 @@ jobs: # https://github.com/uutils/coreutils/issues/5311 - { name: fuzz_date, should_pass: false } - { name: fuzz_expr, should_pass: true } - - { name: fuzz_printf, should_pass: false } + - { name: fuzz_printf, should_pass: true } - { name: fuzz_echo, should_pass: true } - { name: fuzz_seq, should_pass: false } - { name: fuzz_sort, should_pass: false } - { name: fuzz_wc, should_pass: false } - { name: fuzz_cut, should_pass: false } - { name: fuzz_split, should_pass: false } + - { name: fuzz_tr, should_pass: false } + - { name: fuzz_env, should_pass: false } + - { name: fuzz_cksum, should_pass: false } - { name: fuzz_parse_glob, should_pass: true } - { name: fuzz_parse_size, should_pass: true } - { name: fuzz_parse_time, should_pass: true } + - { name: fuzz_seq_parse_number, should_pass: true } + - { name: fuzz_non_utf8_paths, should_pass: true } + steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@nightly - name: Install `cargo-fuzz` run: cargo install cargo-fuzz @@ -72,13 +82,201 @@ jobs: path: | fuzz/corpus/${{ matrix.test-target.name }} - name: Run ${{ matrix.test-target.name }} for XX seconds + id: run_fuzzer shell: bash continue-on-error: ${{ !matrix.test-target.name.should_pass }} run: | - cargo +nightly fuzz run ${{ matrix.test-target.name }} -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0 + mkdir -p fuzz/stats + STATS_FILE="fuzz/stats/${{ matrix.test-target.name }}.txt" + cargo +nightly fuzz run ${{ matrix.test-target.name }} -- -max_total_time=${{ env.RUN_FOR }} -timeout=${{ env.RUN_FOR }} -detect_leaks=0 -print_final_stats=1 2>&1 | tee "$STATS_FILE" + + # Extract key stats from the output + if grep -q "stat::number_of_executed_units" "$STATS_FILE"; then + RUNS=$(grep "stat::number_of_executed_units" "$STATS_FILE" | awk '{print $2}') + echo "runs=$RUNS" >> "$GITHUB_OUTPUT" + else + echo "runs=unknown" >> "$GITHUB_OUTPUT" + fi + + if grep -q "stat::average_exec_per_sec" "$STATS_FILE"; then + EXEC_RATE=$(grep "stat::average_exec_per_sec" "$STATS_FILE" | awk '{print $2}') + echo "exec_rate=$EXEC_RATE" >> "$GITHUB_OUTPUT" + else + echo "exec_rate=unknown" >> "$GITHUB_OUTPUT" + fi + + if grep -q "stat::new_units_added" "$STATS_FILE"; then + NEW_UNITS=$(grep "stat::new_units_added" "$STATS_FILE" | awk '{print $2}') + echo "new_units=$NEW_UNITS" >> "$GITHUB_OUTPUT" + else + echo "new_units=unknown" >> "$GITHUB_OUTPUT" + fi + + # Save should_pass value to file for summary job to use + echo "${{ matrix.test-target.should_pass }}" > "fuzz/stats/${{ matrix.test-target.name }}.should_pass" + + # Print stats to job output for immediate visibility + echo "----------------------------------------" + echo "FUZZING STATISTICS FOR ${{ matrix.test-target.name }}" + echo "----------------------------------------" + echo "Runs: $(grep -q "stat::number_of_executed_units" "$STATS_FILE" && grep "stat::number_of_executed_units" "$STATS_FILE" | awk '{print $2}' || echo "unknown")" + echo "Execution Rate: $(grep -q "stat::average_exec_per_sec" "$STATS_FILE" && grep "stat::average_exec_per_sec" "$STATS_FILE" | awk '{print $2}' || echo "unknown") execs/sec" + echo "New Units: $(grep -q "stat::new_units_added" "$STATS_FILE" && grep "stat::new_units_added" "$STATS_FILE" | awk '{print $2}' || echo "unknown")" + echo "Expected: ${{ matrix.test-target.name.should_pass }}" + if grep -q "SUMMARY: " "$STATS_FILE"; then + echo "Status: $(grep "SUMMARY: " "$STATS_FILE" | head -1)" + else + echo "Status: Completed" + fi + echo "----------------------------------------" + + # Add summary to GitHub step summary + echo "### Fuzzing Results for ${{ matrix.test-target.name }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY + + if grep -q "stat::number_of_executed_units" "$STATS_FILE"; then + echo "| Runs | $(grep "stat::number_of_executed_units" "$STATS_FILE" | awk '{print $2}') |" >> $GITHUB_STEP_SUMMARY + fi + + if grep -q "stat::average_exec_per_sec" "$STATS_FILE"; then + echo "| Execution Rate | $(grep "stat::average_exec_per_sec" "$STATS_FILE" | awk '{print $2}') execs/sec |" >> $GITHUB_STEP_SUMMARY + fi + + if grep -q "stat::new_units_added" "$STATS_FILE"; then + echo "| New Units | $(grep "stat::new_units_added" "$STATS_FILE" | awk '{print $2}') |" >> $GITHUB_STEP_SUMMARY + fi + + echo "| Should pass | ${{ matrix.test-target.should_pass }} |" >> $GITHUB_STEP_SUMMARY + + if grep -q "SUMMARY: " "$STATS_FILE"; then + echo "| Status | $(grep "SUMMARY: " "$STATS_FILE" | head -1) |" >> $GITHUB_STEP_SUMMARY + else + echo "| Status | Completed |" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY - name: Save Corpus Cache uses: actions/cache/save@v4 with: key: corpus-cache-${{ matrix.test-target.name }} path: | fuzz/corpus/${{ matrix.test-target.name }} + - name: Upload Stats + uses: actions/upload-artifact@v4 + with: + name: fuzz-stats-${{ matrix.test-target.name }} + path: | + fuzz/stats/${{ matrix.test-target.name }}.txt + fuzz/stats/${{ matrix.test-target.name }}.should_pass + retention-days: 5 + fuzz-summary: + needs: fuzz-run + name: Fuzzing Summary + runs-on: ubuntu-latest + if: always() + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Download all stats + uses: actions/download-artifact@v5 + with: + path: fuzz/stats-artifacts + pattern: fuzz-stats-* + merge-multiple: true + - name: Prepare stats directory + run: | + mkdir -p fuzz/stats + # Debug: List content of stats-artifacts directory + echo "Contents of stats-artifacts directory:" + find fuzz/stats-artifacts -type f | sort + + # Extract files from the artifact directories - handle nested directories + find fuzz/stats-artifacts -type f -name "*.txt" -exec cp {} fuzz/stats/ \; + find fuzz/stats-artifacts -type f -name "*.should_pass" -exec cp {} fuzz/stats/ \; + + # Debug information + echo "Contents of stats directory after extraction:" + ls -la fuzz/stats/ + echo "Contents of should_pass files (if any):" + cat fuzz/stats/*.should_pass 2>/dev/null || echo "No should_pass files found" + - name: Generate Summary + run: | + echo "# Fuzzing Summary" > fuzzing_summary.md + echo "" >> fuzzing_summary.md + echo "| Target | Runs | Exec/sec | New Units | Should pass | Status |" >> fuzzing_summary.md + echo "|--------|------|----------|-----------|-------------|--------|" >> fuzzing_summary.md + + TOTAL_RUNS=0 + TOTAL_NEW_UNITS=0 + + for stat_file in fuzz/stats/*.txt; do + TARGET=$(basename "$stat_file" .txt) + SHOULD_PASS_FILE="${stat_file%.*}.should_pass" + + # Get expected status + if [ -f "$SHOULD_PASS_FILE" ]; then + EXPECTED=$(cat "$SHOULD_PASS_FILE") + else + EXPECTED="unknown" + fi + + # Extract runs + if grep -q "stat::number_of_executed_units" "$stat_file"; then + RUNS=$(grep "stat::number_of_executed_units" "$stat_file" | awk '{print $2}') + TOTAL_RUNS=$((TOTAL_RUNS + RUNS)) + else + RUNS="unknown" + fi + + # Extract execution rate + if grep -q "stat::average_exec_per_sec" "$stat_file"; then + EXEC_RATE=$(grep "stat::average_exec_per_sec" "$stat_file" | awk '{print $2}') + else + EXEC_RATE="unknown" + fi + + # Extract new units added + if grep -q "stat::new_units_added" "$stat_file"; then + NEW_UNITS=$(grep "stat::new_units_added" "$stat_file" | awk '{print $2}') + if [[ "$NEW_UNITS" =~ ^[0-9]+$ ]]; then + TOTAL_NEW_UNITS=$((TOTAL_NEW_UNITS + NEW_UNITS)) + fi + else + NEW_UNITS="unknown" + fi + + # Extract status + if grep -q "SUMMARY: " "$stat_file"; then + STATUS=$(grep "SUMMARY: " "$stat_file" | head -1) + else + STATUS="Completed" + fi + + echo "| $TARGET | $RUNS | $EXEC_RATE | $NEW_UNITS | $EXPECTED | $STATUS |" >> fuzzing_summary.md + done + + echo "" >> fuzzing_summary.md + echo "## Overall Statistics" >> fuzzing_summary.md + echo "" >> fuzzing_summary.md + echo "- **Total runs:** $TOTAL_RUNS" >> fuzzing_summary.md + echo "- **Total new units discovered:** $TOTAL_NEW_UNITS" >> fuzzing_summary.md + echo "- **Average execution rate:** $(grep -h "stat::average_exec_per_sec" fuzz/stats/*.txt | awk '{sum += $2; count++} END {if (count > 0) print sum/count " execs/sec"; else print "unknown"}')" >> fuzzing_summary.md + + # Add count by expected status + echo "- **Tests expected to pass:** $(find fuzz/stats -name "*.should_pass" -exec cat {} \; | grep -c "true")" >> fuzzing_summary.md + echo "- **Tests expected to fail:** $(find fuzz/stats -name "*.should_pass" -exec cat {} \; | grep -c "false")" >> fuzzing_summary.md + + # Write to GitHub step summary + cat fuzzing_summary.md >> $GITHUB_STEP_SUMMARY + - name: Show Summary + run: | + cat fuzzing_summary.md + - name: Upload Summary + uses: actions/upload-artifact@v4 + with: + name: fuzzing-summary + path: fuzzing_summary.md + retention-days: 5 diff --git a/.github/workflows/ignore-intermittent.txt b/.github/workflows/ignore-intermittent.txt index e163202a2ca..9e0e2ab0df6 100644 --- a/.github/workflows/ignore-intermittent.txt +++ b/.github/workflows/ignore-intermittent.txt @@ -1,3 +1,6 @@ tests/tail/inotify-dir-recreate -tests/misc/timeout +tests/timeout/timeout tests/rm/rm1 +tests/misc/stdbuf +tests/misc/usage_vs_getopt +tests/misc/tee diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml new file mode 100644 index 00000000000..d6909eb5ca5 --- /dev/null +++ b/.github/workflows/l10n.yml @@ -0,0 +1,1268 @@ +name: L10n (Localization) + +# spell-checker: disable + +on: + pull_request: + push: + branches: + - '*' + +env: + # * style job configuration + STYLE_FAIL_ON_FAULT: true ## (bool) fail the build if a style job contains a fault (error or warning); may be overridden on a per-job basis + +permissions: + contents: read # to fetch code (actions/checkout) + +# End the current execution if there is a new changeset in the PR. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + + l10n_build_test: + name: L10n/Build and Test + runs-on: ${{ matrix.job.os }} + env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + strategy: + fail-fast: false + matrix: + job: + - { os: ubuntu-latest , features: "feat_os_unix" } + - { os: macos-latest , features: "feat_os_macos" } + - { os: windows-latest , features: "feat_os_windows" } + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: taiki-e/install-action@nextest + - uses: Swatinem/rust-cache@v2 + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install/setup prerequisites + shell: bash + run: | + ## Install/setup prerequisites + case '${{ matrix.job.os }}' in + ubuntu-*) + # selinux headers needed for testing + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev + ;; + 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: | + ## Test l10n functionality + cargo test -p uucore locale + cargo test + env: + RUST_BACKTRACE: "1" + + l10n_fluent_syntax: + name: L10n/Fluent Syntax Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Install Mozilla Fluent Linter + shell: bash + run: | + ## Install Mozilla Fluent Linter + pip install moz-fluent-linter + - name: Find and validate Fluent files + shell: bash + run: | + ## Find and validate Fluent files with Mozilla Fluent Linter + + # Check if any .ftl files exist + fluent_files=$(find . -name "*.ftl" -type f 2>/dev/null || true) + + if [ -n "$fluent_files" ]; then + echo "Found Fluent files:" + echo "$fluent_files" + else + echo "::notice::No Fluent (.ftl) files found in the repository" + exit 0 + fi + + # Use Mozilla Fluent Linter for comprehensive validation + echo "Running Mozilla Fluent Linter..." + + has_errors=false + + while IFS= read -r file; do + echo "Checking $file with Mozilla Fluent Linter..." + + # Run fluent-linter on each file + if ! moz-fluent-lint "$file"; then + echo "::error file=$file::Fluent syntax errors found in $file" + has_errors=true + else + echo "✓ Fluent syntax check passed for $file" + fi + + done <<< "$fluent_files" + + if [ "$has_errors" = true ]; then + echo "::error::Fluent linting failed - please fix syntax errors" + exit 1 + fi + + echo "::notice::All Fluent files passed Mozilla Fluent Linter validation" + + l10n_clap_error_localization: + name: L10n/Clap Error Localization Test + runs-on: ubuntu-latest + env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install/setup prerequisites + shell: bash + run: | + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev locales + 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: | + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + + # Test invalid argument error - should show colored error message + echo "Testing invalid argument error..." + error_output=$(cargo run --features feat_os_unix --bin coreutils -- cp --invalid-arg 2>&1 || echo "Expected error occurred") + echo "Error output: $error_output" + + # Check for expected English clap error patterns + english_errors_found=0 + + if echo "$error_output" | grep -q "error.*unexpected argument"; then + echo "✓ Found English clap error message pattern" + english_errors_found=$((english_errors_found + 1)) + fi + + if echo "$error_output" | grep -q "Usage:"; then + echo "✓ Found English usage pattern" + english_errors_found=$((english_errors_found + 1)) + fi + + if echo "$error_output" | grep -q "For more information.*--help"; then + echo "✓ Found English help suggestion" + english_errors_found=$((english_errors_found + 1)) + fi + + # Test typo suggestion + echo "Testing typo suggestion..." + typo_output=$(cargo run --features feat_os_unix --bin coreutils -- ls --verbos 2>&1 || echo "Expected error occurred") + echo "Typo output: $typo_output" + + if echo "$typo_output" | grep -q "similar.*verbose"; then + echo "✓ Found English typo suggestion" + english_errors_found=$((english_errors_found + 1)) + fi + + echo "English clap errors found: $english_errors_found" + if [ "$english_errors_found" -ge 2 ]; then + echo "✓ SUCCESS: English clap error localization working" + else + echo "✗ ERROR: English clap error localization not working properly" + exit 1 + fi + env: + RUST_BACKTRACE: "1" + + - name: Test French clap error localization + shell: bash + run: | + export LANG=fr_FR.UTF-8 + export LC_ALL=fr_FR.UTF-8 + + # Test invalid argument error - should show French colored error message + echo "Testing invalid argument error in French..." + error_output=$(cargo run --features feat_os_unix --bin coreutils -- cp --invalid-arg 2>&1 || echo "Expected error occurred") + echo "French error output: $error_output" + + # Check for expected French clap error patterns + french_errors_found=0 + + if echo "$error_output" | grep -q "erreur.*argument inattendu"; then + echo "✓ Found French clap error message: 'erreur: argument inattendu'" + french_errors_found=$((french_errors_found + 1)) + fi + + if echo "$error_output" | grep -q "conseil.*pour passer.*comme valeur"; then + echo "✓ Found French tip message: 'conseil: pour passer ... comme valeur'" + french_errors_found=$((french_errors_found + 1)) + fi + + if echo "$error_output" | grep -q "Utilisation:"; then + echo "✓ Found French usage pattern: 'Utilisation:'" + french_errors_found=$((french_errors_found + 1)) + fi + + if echo "$error_output" | grep -q "Pour plus d'informations.*--help"; then + echo "✓ Found French help suggestion: 'Pour plus d'informations'" + french_errors_found=$((french_errors_found + 1)) + fi + + # Test typo suggestion in French + echo "Testing typo suggestion in French..." + typo_output=$(cargo run --features feat_os_unix --bin coreutils -- ls --verbos 2>&1 || echo "Expected error occurred") + echo "French typo output: $typo_output" + + if echo "$typo_output" | grep -q "conseil.*similaire.*verbose"; then + echo "✓ Found French typo suggestion with 'conseil'" + french_errors_found=$((french_errors_found + 1)) + fi + + echo "French clap errors found: $french_errors_found" + if [ "$french_errors_found" -ge 2 ]; then + echo "✓ SUCCESS: French clap error localization working - found $french_errors_found French patterns" + else + echo "✗ ERROR: French clap error localization not working properly" + echo "Note: This might be expected if French common locale files are not available" + # Don't fail the build - French clap localization might not be fully set up yet + echo "::warning::French clap error localization not working, but continuing" + fi + + # Test that colors are working (ANSI escape codes) + echo "Testing ANSI color codes in error output..." + if echo "$error_output" | grep -q $'\x1b\[3[0-7]m'; then + echo "✓ Found ANSI color codes in error output" + else + echo "✗ No ANSI color codes found - colors may not be working" + echo "::warning::ANSI color codes not detected in clap error output" + fi + env: + RUST_BACKTRACE: "1" + + - name: Test clap localization with multiple utilities + shell: bash + run: | + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + + utilities_to_test=("ls" "cat" "touch" "cp" "mv") + utilities_passed=0 + + for util in "${utilities_to_test[@]}"; do + echo "Testing $util with invalid argument..." + util_error=$(cargo run --features feat_os_unix --bin coreutils -- "$util" --nonexistent-flag 2>&1 || echo "Expected error occurred") + + if echo "$util_error" | grep -q "error.*unexpected argument"; then + echo "✓ $util: clap localization working" + utilities_passed=$((utilities_passed + 1)) + else + echo "✗ $util: clap localization not working" + echo "Output: $util_error" + fi + done + + echo "Utilities with working clap localization: $utilities_passed/${#utilities_to_test[@]}" + if [ "$utilities_passed" -ge 3 ]; then + echo "✓ SUCCESS: Clap localization working across multiple utilities" + else + echo "✗ ERROR: Clap localization not working for enough utilities" + exit 1 + fi + env: + RUST_BACKTRACE: "1" + + l10n_french_integration: + name: L10n/French Integration Test + runs-on: ubuntu-latest + env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install/setup prerequisites + shell: bash + run: | + ## Install/setup prerequisites + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev locales + - name: Generate French locale + shell: bash + run: | + ## 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: | + ## Test French localization with various commands + export LANG=fr_FR.UTF-8 + export LC_ALL=fr_FR.UTF-8 + + echo "Testing touch --help with French locale..." + help_output=$(cargo run --features feat_os_unix --bin coreutils -- touch --help 2>&1 || echo "Command failed") + echo "Help output: $help_output" + + # Check for specific French strings from touch fr-FR.ftl + french_strings_found=0 + if echo "$help_output" | grep -q "Mettre à jour les temps d'accès"; then + echo "✓ Found French description: 'Mettre à jour les temps d'accès'" + french_strings_found=$((french_strings_found + 1)) + fi + if echo "$help_output" | grep -q "changer seulement le temps d'accès"; then + echo "✓ Found French help text: 'changer seulement le temps d'accès'" + french_strings_found=$((french_strings_found + 1)) + fi + if echo "$help_output" | grep -q "FICHIER"; then + echo "✓ Found French usage pattern: 'FICHIER'" + french_strings_found=$((french_strings_found + 1)) + fi + + echo "Testing ls --help with French locale..." + ls_help=$(cargo run --features feat_os_unix --bin coreutils -- ls --help 2>&1 || echo "Command failed") + echo "ls help output: $ls_help" + + # Check for specific French strings from ls fr-FR.ftl + if echo "$ls_help" | grep -q "Lister le contenu des répertoires"; then + echo "✓ Found French ls description: 'Lister le contenu des répertoires'" + french_strings_found=$((french_strings_found + 1)) + fi + if echo "$ls_help" | grep -q "Afficher les informations d'aide"; then + echo "✓ Found French ls help text: 'Afficher les informations d'aide'" + french_strings_found=$((french_strings_found + 1)) + fi + + echo "Testing base64 --help with French locale..." + base64_help=$(cargo run --features feat_os_unix --bin coreutils -- base64 --help 2>&1 || echo "Command failed") + echo "base64 help output: $base64_help" + + # Check for specific French strings from base64 fr-FR.ftl + if echo "$base64_help" | grep -q "encoder/décoder les données"; then + echo "✓ Found French base64 description: 'encoder/décoder les données'" + french_strings_found=$((french_strings_found + 1)) + fi + + echo "Testing with error messages..." + error_output=$(cargo run --features feat_os_unix --bin coreutils -- ls /nonexistent 2>&1 || echo "Expected error occurred") + echo "Error output: $error_output" + + # Check for French error messages from ls fr-FR.ftl + if echo "$error_output" | grep -q "impossible d'accéder à"; then + echo "✓ Found French error message: 'impossible d'accéder à'" + french_strings_found=$((french_strings_found + 1)) + fi + if echo "$error_output" | grep -q "Aucun fichier ou répertoire de ce type"; then + echo "✓ Found French error text: 'Aucun fichier ou répertoire de ce type'" + french_strings_found=$((french_strings_found + 1)) + fi + + # Test that the binary works and doesn't crash with French locale + version_output=$(cargo run --features feat_os_unix --bin coreutils -- --version 2>&1 || echo "Version command failed") + echo "Version output: $version_output" + + # Final validation - ensure we found at least some French strings + echo "French strings found: $french_strings_found" + if [ "$french_strings_found" -gt 0 ]; then + echo "✓ SUCCESS: French locale integration test passed - found $french_strings_found French strings" + else + echo "✗ ERROR: No French strings were detected, but commands executed successfully" + exit 1 + fi + env: + RUST_BACKTRACE: "1" + + l10n_multicall_binary_install: + name: L10n/Multi-call Binary Install Test + runs-on: ${{ matrix.job.os }} + env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + strategy: + fail-fast: false + matrix: + job: + - { os: ubuntu-latest , features: "feat_os_unix" } + - { os: macos-latest , features: "feat_os_macos" } + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install/setup prerequisites + shell: bash + run: | + ## Install/setup prerequisites + case '${{ matrix.job.os }}' in + ubuntu-*) + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev build-essential + ;; + macos-*) + brew install coreutils make + ;; + esac + - name: Install via make and test multi-call binary + shell: bash + run: | + ## Install using make and test installed binaries + echo "Installing with make using DESTDIR..." + + # Create installation directory + INSTALL_DIR="$PWD/install-dir" + mkdir -p "$INSTALL_DIR" + mkdir -p "$INSTALL_DIR/usr/bin" + + # Build and install using make with DESTDIR + echo "Building multi-call binary with MULTICALL=y" + echo "Current directory: $PWD" + echo "Installation directory will be: $INSTALL_DIR" + + # First check if binary exists after build + echo "Checking if coreutils was built..." + ls -la target/release/coreutils || echo "No coreutils binary in target/release/" + + make FEATURES="${{ matrix.job.features }}" PROFILE=release MULTICALL=y + + echo "After build, checking target/release/:" + ls -la target/release/ | grep -E "(coreutils|^total)" || echo "Build may have failed" + + echo "Running make install..." + echo "Before install - checking what we have:" + ls -la target/release/coreutils 2>/dev/null || echo "No coreutils in target/release" + + # Run make install with verbose output to see what happens + echo "About to run: make install DESTDIR=\"$INSTALL_DIR\" PREFIX=/usr PROFILE=release MULTICALL=y" + echo "Expected install path: $INSTALL_DIR/usr/bin/coreutils" + + make install DESTDIR="$INSTALL_DIR" PREFIX=/usr PROFILE=release MULTICALL=y || { + echo "Make install failed! Exit code: $?" + echo "Let's see what happened:" + ls -la "$INSTALL_DIR" 2>/dev/null || echo "Install directory doesn't exist" + exit 1 + } + + echo "Make install completed successfully" + + # Debug: Show what was installed + echo "=== Installation Debug ===" + echo "Current directory: $(pwd)" + echo "INSTALL_DIR: $INSTALL_DIR" + echo "Checking if build succeeded..." + if [ -f "target/release/coreutils" ]; then + echo "✓ Build succeeded - coreutils binary exists in target/release/" + ls -la target/release/coreutils + else + echo "✗ Build failed - no coreutils binary in target/release/" + echo "Contents of target/release/:" + ls -la target/release/ | head -20 + exit 1 + fi + + echo "Contents of installation directory:" + find "$INSTALL_DIR" -type f 2>/dev/null | head -20 || echo "No files found in $INSTALL_DIR" + + echo "Checking standard installation paths..." + ls -la "$INSTALL_DIR/usr/bin/" 2>/dev/null || echo "Directory $INSTALL_DIR/usr/bin/ not found" + ls -la "$INSTALL_DIR/usr/local/bin/" 2>/dev/null || echo "Directory $INSTALL_DIR/usr/local/bin/ not found" + ls -la "$INSTALL_DIR/bin/" 2>/dev/null || echo "Directory $INSTALL_DIR/bin/ not found" + + # Find where coreutils was actually installed + echo "Searching for coreutils binary in installation directory..." + COREUTILS_BIN=$(find "$INSTALL_DIR" -name "coreutils" -type f 2>/dev/null | head -1) + if [ -n "$COREUTILS_BIN" ]; then + echo "Found coreutils at: $COREUTILS_BIN" + export COREUTILS_BIN + else + echo "ERROR: coreutils binary not found in installation directory!" + echo "Installation may have failed. Let's check the entire filesystem under install-dir:" + find "$INSTALL_DIR" -type f 2>/dev/null | head -50 + + # As a last resort, check if it's in the build directory + if [ -f "target/release/coreutils" ]; then + echo "Using binary from build directory as fallback" + COREUTILS_BIN="$(pwd)/target/release/coreutils" + export COREUTILS_BIN + else + exit 1 + fi + fi + + echo "Testing installed multi-call binary functionality..." + + # Test calling utilities through coreutils binary + echo "Testing: $COREUTILS_BIN ls --version" + "$COREUTILS_BIN" ls --version + + echo "Testing: $COREUTILS_BIN cat --version" + "$COREUTILS_BIN" cat --version + + echo "Testing: $COREUTILS_BIN touch --version" + "$COREUTILS_BIN" touch --version + + # Test individual binaries (if they exist) + LS_BIN="$INSTALL_DIR/usr/bin/ls" + if [ -f "$LS_BIN" ]; then + echo "Testing individual binary: $LS_BIN --version" + "$LS_BIN" --version + else + echo "Individual ls binary not found (multi-call only mode)" + # Check if symlinks exist + if [ -L "$LS_BIN" ]; then + echo "Found ls as symlink to: $(readlink -f "$LS_BIN")" + fi + fi + + echo "✓ Multi-call binary installation and functionality test passed" + + l10n_installation_test: + name: L10n/Installation Test (Make & Cargo) + runs-on: ${{ matrix.job.os }} + env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + strategy: + fail-fast: false + matrix: + job: + - { os: ubuntu-latest , features: "feat_os_unix" } + - { os: macos-latest , features: "feat_os_macos" } + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install/setup prerequisites + shell: bash + run: | + ## Install/setup prerequisites + case '${{ matrix.job.os }}' in + ubuntu-*) + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev build-essential locales + # Generate French locale for testing + sudo locale-gen --keep-existing fr_FR.UTF-8 + locale -a | grep -i fr || echo "French locale generation may have failed" + ;; + macos-*) + brew install coreutils make + ;; + esac + - name: Test Make installation + shell: bash + run: | + ## Test installation via make with DESTDIR + echo "Testing make install with l10n features..." + + # Create installation directory + MAKE_INSTALL_DIR="$PWD/make-install-dir" + mkdir -p "$MAKE_INSTALL_DIR" + + # Build and install using make with DESTDIR + make FEATURES="${{ matrix.job.features }}" PROFILE=release MULTICALL=y + make install DESTDIR="$MAKE_INSTALL_DIR" PREFIX=/usr PROFILE=release MULTICALL=y + + # Verify installation + echo "Testing make-installed binaries..." + if [ -f "$MAKE_INSTALL_DIR/usr/bin/coreutils" ]; then + echo "✓ coreutils binary installed via make successfully" + "$MAKE_INSTALL_DIR/usr/bin/coreutils" --version + else + echo "✗ coreutils binary not found after make install" + exit 1 + fi + + # Test utilities + echo "Testing make-installed utilities..." + "$MAKE_INSTALL_DIR/usr/bin/coreutils" ls --version + "$MAKE_INSTALL_DIR/usr/bin/coreutils" cat --version + "$MAKE_INSTALL_DIR/usr/bin/coreutils" touch --version + + # Test basic functionality + echo "test content" > test.txt + if "$MAKE_INSTALL_DIR/usr/bin/coreutils" cat test.txt | grep -q "test content"; then + echo "✓ Basic functionality works" + else + echo "✗ Basic functionality failed" + exit 1 + fi + + # Test French localization with make-installed binary (Ubuntu only) + if [ "${{ matrix.job.os }}" = "ubuntu-latest" ]; then + echo "Testing French localization with make-installed binary..." + + # Set French locale + export LANG=fr_FR.UTF-8 + export LC_ALL=fr_FR.UTF-8 + + echo "Testing ls --help with French locale..." + ls_help=$("$MAKE_INSTALL_DIR/usr/bin/coreutils" ls --help 2>&1 || echo "Command failed") + echo "ls help output (first 10 lines):" + echo "$ls_help" | head -10 + + # Check for specific French strings from ls fr-FR.ftl + french_strings_found=0 + + if echo "$ls_help" | grep -q "Lister le contenu des répertoires"; then + echo "✓ Found French ls description: 'Lister le contenu des répertoires'" + french_strings_found=$((french_strings_found + 1)) + fi + + if echo "$ls_help" | grep -q "Afficher les informations d'aide"; then + echo "✓ Found French ls help text: 'Afficher les informations d'aide'" + french_strings_found=$((french_strings_found + 1)) + fi + + if echo "$ls_help" | grep -q "FICHIER"; then + echo "✓ Found French usage pattern: 'FICHIER'" + french_strings_found=$((french_strings_found + 1)) + fi + + echo "Testing cat --help with French locale..." + cat_help=$("$MAKE_INSTALL_DIR/usr/bin/coreutils" cat --help 2>&1 || echo "Command failed") + echo "cat help output (first 5 lines):" + echo "$cat_help" | head -5 + + if echo "$cat_help" | grep -q "Concaténer"; then + echo "✓ Found French cat description containing: 'Concaténer'" + french_strings_found=$((french_strings_found + 1)) + fi + + echo "Testing error messages with French locale..." + error_output=$("$MAKE_INSTALL_DIR/usr/bin/coreutils" ls /nonexistent_test_directory 2>&1 || echo "Expected error occurred") + echo "Error output: $error_output" + + if echo "$error_output" | grep -q "impossible d'accéder à"; then + echo "✓ Found French error message: 'impossible d'accéder à'" + french_strings_found=$((french_strings_found + 1)) + fi + + # Final validation + echo "French strings found: $french_strings_found" + if [ "$french_strings_found" -gt 0 ]; then + echo "✓ SUCCESS: French localization test passed with make-installed binary - found $french_strings_found French strings" + else + echo "✗ ERROR: No French strings detected with make-installed binary" + exit 1 + fi + else + echo "Skipping French localization test on ${{ matrix.job.os }} (no French locale available)" + fi + + echo "✓ Make installation test passed" + - name: Test Cargo installation + shell: bash + run: | + ## Test installation via cargo install with DESTDIR-like approach + echo "Testing cargo install with l10n features..." + + # Create installation directory + CARGO_INSTALL_DIR="$PWD/cargo-install-dir" + mkdir -p "$CARGO_INSTALL_DIR" + + # Install using cargo with l10n features + cargo install --path . --features ${{ matrix.job.features }} --root "$CARGO_INSTALL_DIR" --locked + + # Verify installation + echo "Testing cargo-installed binaries..." + if [ -f "$CARGO_INSTALL_DIR/bin/coreutils" ]; then + echo "✓ coreutils binary installed successfully" + "$CARGO_INSTALL_DIR/bin/coreutils" --version + else + echo "✗ coreutils binary not found after cargo install" + exit 1 + fi + + # Test utilities + echo "Testing installed utilities..." + "$CARGO_INSTALL_DIR/bin/coreutils" ls --version + "$CARGO_INSTALL_DIR/bin/coreutils" cat --version + "$CARGO_INSTALL_DIR/bin/coreutils" touch --version + + # Test basic functionality + echo "test content" > test.txt + if "$CARGO_INSTALL_DIR/bin/coreutils" cat test.txt | grep -q "test content"; then + echo "✓ Basic functionality works" + else + echo "✗ Basic functionality failed" + exit 1 + fi + + echo "✓ Cargo installation test passed" + - name: Download additional locales from coreutils-l10n + shell: bash + run: | + ## Download additional locale files from coreutils-l10n repository + echo "Downloading additional locale files from coreutils-l10n..." + git clone https://github.com/uutils/coreutils-l10n.git coreutils-l10n-repo + + # Create installation directory + CARGO_INSTALL_DIR="$PWD/cargo-install-dir" + + # Create locale directory for cargo install + LOCALE_DIR="$CARGO_INSTALL_DIR/share/locales" + mkdir -p "$LOCALE_DIR" + + # Debug: Check structure of l10n repo + echo "Checking structure of coreutils-l10n-repo:" + ls -la coreutils-l10n-repo/ | head -10 + echo "Looking for locales directory:" + find coreutils-l10n-repo -name "*.ftl" -type f 2>/dev/null | head -10 || true + echo "Checking specific utilities:" + ls -la coreutils-l10n-repo/src/uu/ls/locales/ 2>/dev/null || echo "No ls directory in correct location" + find coreutils-l10n-repo -path "*/ls/*.ftl" 2>/dev/null | head -5 || echo "No ls ftl files found" + + # Copy non-English locale files from l10n repo + for util_dir in src/uu/*/; do + util_name=$(basename "$util_dir") + l10n_util_dir="coreutils-l10n-repo/src/uu/$util_name/locales" + + if [ -d "$l10n_util_dir" ]; then + echo "Installing locales for $util_name..." + mkdir -p "$LOCALE_DIR/$util_name" + + for locale_file in "$l10n_util_dir"/*.ftl; do + if [ -f "$locale_file" ]; then + filename=$(basename "$locale_file") + # Skip English locale files (they are embedded) + if [ "$filename" != "en-US.ftl" ]; then + cp "$locale_file" "$LOCALE_DIR/$util_name/" + echo " Installed $filename to $LOCALE_DIR/$util_name/" + fi + fi + done + else + # Debug: Show what's not found + if [ "$util_name" = "ls" ] || [ "$util_name" = "cat" ]; then + echo "WARNING: No l10n directory found for $util_name at $l10n_util_dir" + fi + fi + done + + # Debug: Show what was actually installed + echo "Files installed in locale directory:" + find "$LOCALE_DIR" -name "*.ftl" 2>/dev/null | head -10 || true + + # Fallback: If no files were installed from l10n repo, try copying from main repo + if [ -z "$(find "$LOCALE_DIR" -name "*.ftl" 2>/dev/null)" ]; then + echo "No files found from l10n repo, trying fallback from main repository..." + for util_dir in src/uu/*/; do + util_name=$(basename "$util_dir") + if [ -d "$util_dir/locales" ]; then + echo "Copying locales for $util_name from main repo..." + mkdir -p "$LOCALE_DIR/$util_name" + cp "$util_dir/locales"/*.ftl "$LOCALE_DIR/$util_name/" 2>/dev/null || true + fi + done + echo "Files after fallback:" + find "$LOCALE_DIR" -name "*.ftl" 2>/dev/null | head -10 || true + fi + + echo "✓ Additional locale files installed" + - name: Test French localization after cargo install + shell: bash + run: | + ## Test French localization with cargo-installed binary and downloaded locales + echo "Testing French localization with cargo-installed binary..." + + # Set installation directories + CARGO_INSTALL_DIR="$PWD/cargo-install-dir" + LOCALE_DIR="$CARGO_INSTALL_DIR/share/locales" + + echo "Checking installed binary..." + if [ ! -f "$CARGO_INSTALL_DIR/bin/coreutils" ]; then + echo "✗ coreutils binary not found" + exit 1 + fi + + echo "Checking locale files..." + echo "LOCALE_DIR is: $LOCALE_DIR" + echo "Checking if locale directory exists:" + ls -la "$LOCALE_DIR" 2>/dev/null || echo "Locale directory not found" + echo "Contents of locale directory:" + find "$LOCALE_DIR" -name "*.ftl" 2>/dev/null | head -10 || echo "No locale files found" + echo "Looking for ls locale files specifically:" + ls -la "$LOCALE_DIR/ls/" 2>/dev/null || echo "No ls locale directory" + + # Test French localization + export LANG=fr_FR.UTF-8 + export LC_ALL=fr_FR.UTF-8 + + echo "Testing ls --help with French locale..." + ls_help=$("$CARGO_INSTALL_DIR/bin/coreutils" ls --help 2>&1 || echo "Command failed") + echo "ls help output (first 10 lines):" + echo "$ls_help" | head -10 + + # Check for specific French strings from ls fr-FR.ftl + french_strings_found=0 + + if echo "$ls_help" | grep -q "Lister le contenu des répertoires"; then + echo "✓ Found French ls description: 'Lister le contenu des répertoires'" + french_strings_found=$((french_strings_found + 1)) + fi + + if echo "$ls_help" | grep -q "Afficher les informations d'aide"; then + echo "✓ Found French ls help text: 'Afficher les informations d'aide'" + french_strings_found=$((french_strings_found + 1)) + fi + + if echo "$ls_help" | grep -q "FICHIER"; then + echo "✓ Found French usage pattern: 'FICHIER'" + french_strings_found=$((french_strings_found + 1)) + fi + + echo "Testing cat --help with French locale..." + cat_help=$("$CARGO_INSTALL_DIR/bin/coreutils" cat --help 2>&1 || echo "Command failed") + echo "cat help output (first 5 lines):" + echo "$cat_help" | head -5 + + if echo "$cat_help" | grep -q "Concaténer"; then + echo "✓ Found French cat description containing: 'Concaténer'" + french_strings_found=$((french_strings_found + 1)) + fi + + echo "Testing error messages with French locale..." + error_output=$("$CARGO_INSTALL_DIR/bin/coreutils" ls /nonexistent_test_directory 2>&1 || echo "Expected error occurred") + echo "Error output: $error_output" + + if echo "$error_output" | grep -q "impossible d'accéder à"; then + echo "✓ Found French error message: 'impossible d'accéder à'" + french_strings_found=$((french_strings_found + 1)) + fi + + # Verify the binary works in French locale + version_output=$("$CARGO_INSTALL_DIR/bin/coreutils" --version 2>&1) + if [ $? -eq 0 ]; then + echo "✓ Binary executes successfully with French locale" + echo "Version output: $version_output" + else + echo "✗ Binary failed to execute with French locale" + exit 1 + fi + + # Final validation + echo "French strings found: $french_strings_found" + if [ "$french_strings_found" -gt 0 ]; then + echo "✓ SUCCESS: French localization test passed after cargo install - found $french_strings_found French strings" + else + echo "✗ ERROR: No French strings detected with cargo-installed binary" + echo "This indicates an issue with locale loading from downloaded files" + exit 1 + fi + + echo "✓ French localization verification completed" + + l10n_locale_support_verification: + name: L10n/Locale Support Verification + runs-on: ubuntu-latest + env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install/setup prerequisites + shell: bash + run: | + ## Install/setup prerequisites including locale support + sudo apt-get -y update + sudo apt-get -y install libselinux1-dev locales build-essential + + # Generate multiple locales for testing + sudo locale-gen --keep-existing en_US.UTF-8 fr_FR.UTF-8 de_DE.UTF-8 es_ES.UTF-8 + locale -a | grep -E "(en_US|fr_FR|de_DE|es_ES)" || echo "Some locales may not be available" + - name: Install binaries with locale support + shell: bash + run: | + ## Install both multi-call and individual binaries using make + echo "Installing binaries with full locale support..." + + # Create installation directory + INSTALL_DIR="$PWD/install-dir" + mkdir -p "$INSTALL_DIR" + + # Build and install using make with DESTDIR + make FEATURES="feat_os_unix" PROFILE=release MULTICALL=y + make install DESTDIR="$INSTALL_DIR" PREFIX=/usr PROFILE=release MULTICALL=y + + # Debug: Show what was installed + echo "Contents of installation directory:" + find "$INSTALL_DIR" -type f -name "coreutils" -o -name "ls" 2>/dev/null | head -20 || true + echo "Looking for binaries in: $INSTALL_DIR/usr/bin/" + ls -la "$INSTALL_DIR/usr/bin/" || echo "Directory not found" + + echo "✓ Installation completed" + - name: Verify locale detection and startup + shell: bash + run: | + ## Test that installed binaries start correctly with different locales + echo "Testing locale detection and startup..." + + # Set installation directory path + INSTALL_DIR="$PWD/install-dir" + + # Test with different locales + locales_to_test=("C" "en_US.UTF-8" "fr_FR.UTF-8") + + for locale in "${locales_to_test[@]}"; do + echo "Testing with locale: $locale" + + # Test multi-call binary startup + if LC_ALL="$locale" "$INSTALL_DIR/usr/bin/coreutils" --version >/dev/null 2>&1; then + echo "✓ Multi-call binary starts successfully with locale: $locale" + else + echo "✗ Multi-call binary failed to start with locale: $locale" + exit 1 + fi + + # Test individual binary startup (if available) + if [ -f "$INSTALL_DIR/usr/bin/ls" ]; then + if LC_ALL="$locale" "$INSTALL_DIR/usr/bin/ls" --version >/dev/null 2>&1; then + echo "✓ Individual binary (ls) starts successfully with locale: $locale" + else + echo "✗ Individual binary (ls) failed to start with locale: $locale" + exit 1 + fi + else + echo "Individual ls binary not found (multi-call only mode)" + fi + + # Test that help text appears (even if not localized) + help_output=$(LC_ALL="$locale" "$INSTALL_DIR/usr/bin/coreutils" ls --help 2>&1) + if echo "$help_output" | grep -q -i "usage\|list"; then + echo "✓ Help text appears correctly with locale: $locale" + else + echo "✗ Help text missing or malformed with locale: $locale" + echo "Help output: $help_output" + exit 1 + fi + done + + echo "✓ All locale startup tests passed" + - name: Test locale-specific functionality + shell: bash + run: | + ## Test locale-specific behavior with installed binaries + echo "Testing locale-specific functionality..." + + # Set installation directory path + INSTALL_DIR="$PWD/install-dir" + + # Test with French locale (if available) + if locale -a | grep -q fr_FR.UTF-8; then + echo "Testing French locale functionality..." + + export LANG=fr_FR.UTF-8 + export LC_ALL=fr_FR.UTF-8 + + # Test that the program runs successfully with French locale + french_version_output=$("$INSTALL_DIR/usr/bin/coreutils" --version 2>&1) + if [ $? -eq 0 ]; then + echo "✓ Program runs successfully with French locale" + echo "Version output: $french_version_output" + else + echo "✗ Program failed with French locale" + echo "Error output: $french_version_output" + exit 1 + fi + + # Test basic functionality with French locale + temp_file=$(mktemp) + echo "test content" > "$temp_file" + + if "$INSTALL_DIR/usr/bin/coreutils" cat "$temp_file" | grep -q "test content"; then + echo "✓ Basic file operations work with French locale" + else + echo "✗ Basic file operations failed with French locale" + exit 1 + fi + + rm -f "$temp_file" + + # Test that French translations are actually working + echo "Testing French translations..." + french_strings_found=0 + + echo "Testing ls --help with French locale..." + ls_help=$("$INSTALL_DIR/usr/bin/coreutils" ls --help 2>&1 || echo "Command failed") + echo "ls help output (first 10 lines):" + echo "$ls_help" | head -10 + + # Check for actual French strings that appear in ls --help output + if echo "$ls_help" | grep -q "Lister le contenu des répertoires"; then + echo "✓ Found French ls description: 'Lister le contenu des répertoires'" + french_strings_found=$((french_strings_found + 1)) + fi + if echo "$ls_help" | grep -q "Ignorer les fichiers et répertoires commençant par"; then + echo "✓ Found French explanation: 'Ignorer les fichiers et répertoires commençant par'" + french_strings_found=$((french_strings_found + 1)) + fi + if echo "$ls_help" | grep -q "Afficher les informations d'aide"; then + echo "✓ Found French help text: 'Afficher les informations d'aide'" + french_strings_found=$((french_strings_found + 1)) + fi + if echo "$ls_help" | grep -q "FICHIER"; then + echo "✓ Found French usage pattern: 'FICHIER'" + french_strings_found=$((french_strings_found + 1)) + fi + if echo "$ls_help" | grep -q "Définir le format d'affichage"; then + echo "✓ Found French option description: 'Définir le format d'affichage'" + french_strings_found=$((french_strings_found + 1)) + fi + + echo "Testing cat --help with French locale..." + cat_help=$("$INSTALL_DIR/usr/bin/coreutils" cat --help 2>&1 || echo "Command failed") + echo "cat help output (first 5 lines):" + echo "$cat_help" | head -5 + + # Check for French strings in cat help + if echo "$cat_help" | grep -q "Concaténer"; then + echo "✓ Found French cat description containing: 'Concaténer'" + french_strings_found=$((french_strings_found + 1)) + fi + + echo "Testing with error messages..." + error_output=$("$INSTALL_DIR/usr/bin/coreutils" ls /nonexistent_directory_for_testing 2>&1 || echo "Expected error occurred") + echo "Error output: $error_output" + + # Check for French error messages + if echo "$error_output" | grep -q "impossible d'accéder à"; then + echo "✓ Found French error message: 'impossible d'accéder à'" + french_strings_found=$((french_strings_found + 1)) + fi + if echo "$error_output" | grep -q "Aucun fichier ou répertoire de ce type"; then + echo "✓ Found French error text: 'Aucun fichier ou répertoire de ce type'" + french_strings_found=$((french_strings_found + 1)) + fi + + # Test version output + echo "Testing --version with French locale..." + version_output=$("$INSTALL_DIR/usr/bin/coreutils" --version 2>&1) + echo "Version output: $version_output" + + # Final validation - ensure we found at least some French strings + echo "French strings found: $french_strings_found" + if [ "$french_strings_found" -gt 0 ]; then + echo "✓ SUCCESS: French locale translation test passed - found $french_strings_found French strings" + else + echo "✗ ERROR: No French strings were detected in installed binaries" + echo "This indicates that French translations are not working properly with installed binaries" + exit 1 + fi + else + echo "French locale not available, skipping French-specific tests" + fi + + # Test with standard build configuration + echo "Testing standard build configuration..." + cd "$GITHUB_WORKSPACE" + + # Create separate installation directory for standard build + STANDARD_BUILD_INSTALL_DIR="$PWD/standard-build-install-dir" + mkdir -p "$STANDARD_BUILD_INSTALL_DIR" + + # Clean and build standard version + make clean + make FEATURES="feat_os_unix" PROFILE=release MULTICALL=y + make install DESTDIR="$STANDARD_BUILD_INSTALL_DIR" PREFIX=/usr PROFILE=release MULTICALL=y + + # Verify standard build binary works + if "$STANDARD_BUILD_INSTALL_DIR/usr/bin/coreutils" --version >/dev/null 2>&1; then + echo "✓ Standard build works correctly" + else + echo "✗ Standard build failed" + exit 1 + fi + + echo "✓ All locale-specific functionality tests passed" + env: + RUST_BACKTRACE: "1" + + l10n_locale_embedding_regression_test: + name: L10n/Locale Embedding Regression Test + runs-on: ubuntu-latest + env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install/setup prerequisites + shell: bash + run: | + ## Install/setup prerequisites + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev build-essential + - name: Build binaries for locale embedding test + shell: bash + run: | + ## Build individual utilities and multicall binary for locale embedding test + echo "Building binaries with different locale embedding configurations..." + mkdir -p target + + # Build cat utility with targeted locale embedding + echo "Building cat utility with targeted locale embedding..." + echo "cat" > target/uucore_target_util.txt + cargo build -p uu_cat --release + + # Build ls utility with targeted locale embedding + echo "Building ls utility with targeted locale embedding..." + echo "ls" > target/uucore_target_util.txt + cargo build -p uu_ls --release + + # Build multicall binary (should have all locales) + echo "Building multicall binary (should have all locales)..." + echo "multicall" > target/uucore_target_util.txt + cargo build --release + + echo "✓ All binaries built successfully" + env: + RUST_BACKTRACE: "1" + + - name: Analyze embedded locale files + shell: bash + run: | + ## Extract and analyze .ftl files embedded in each binary + echo "=== Embedded Locale File Analysis ===" + + # Analyze cat binary + echo "--- cat binary embedded .ftl files ---" + cat_ftl_files=$(strings target/release/cat | grep -o "[a-z_][a-z_]*/en-US\.ftl" | sort | uniq) + cat_locales=$(echo "$cat_ftl_files" | wc -l) + if [ -n "$cat_ftl_files" ]; then + echo "$cat_ftl_files" + else + echo "(no locale keys found)" + fi + echo "Total: $cat_locales files" + echo + + # Analyze ls binary + echo "--- ls binary embedded .ftl files ---" + ls_ftl_files=$(strings target/release/ls | grep -o "[a-z_][a-z_]*/en-US\.ftl" | sort | uniq) + ls_locales=$(echo "$ls_ftl_files" | wc -l) + if [ -n "$ls_ftl_files" ]; then + echo "$ls_ftl_files" + else + echo "(no locale keys found)" + fi + echo "Total: $ls_locales files" + echo + + # Analyze multicall binary + echo "--- multicall binary embedded .ftl files (first 10) ---" + multi_ftl_files=$(strings target/release/coreutils | grep -o "[a-z_][a-z_]*/en-US\.ftl" | sort | uniq) + multi_locales=$(echo "$multi_ftl_files" | wc -l) + if [ -n "$multi_ftl_files" ]; then + echo "$multi_ftl_files" | head -10 + echo "... (showing first 10 of $multi_locales total files)" + else + echo "(no locale keys found)" + fi + echo + + # Store counts for validation step + echo "cat_locales=$cat_locales" >> $GITHUB_ENV + echo "ls_locales=$ls_locales" >> $GITHUB_ENV + echo "multi_locales=$multi_locales" >> $GITHUB_ENV + + - name: Validate cat binary locale embedding + shell: bash + run: | + ## Validate that cat binary only embeds its own locale files + echo "Validating cat binary locale embedding..." + if [ "$cat_locales" -le 5 ]; then + echo "✓ SUCCESS: cat binary uses targeted locale embedding ($cat_locales files)" + else + echo "✗ FAILURE: cat binary has too many embedded locale files ($cat_locales). Expected ≤ 5." + echo "This indicates LOCALE EMBEDDING REGRESSION - all locales are being embedded instead of just the target utility's locale." + echo "The optimization is not working correctly!" + exit 1 + fi + + - name: Validate ls binary locale embedding + shell: bash + run: | + ## Validate that ls binary only embeds its own locale files + echo "Validating ls binary locale embedding..." + if [ "$ls_locales" -le 5 ]; then + echo "✓ SUCCESS: ls binary uses targeted locale embedding ($ls_locales files)" + else + echo "✗ FAILURE: ls binary has too many embedded locale files ($ls_locales). Expected ≤ 5." + echo "This indicates LOCALE EMBEDDING REGRESSION - all locales are being embedded instead of just the target utility's locale." + echo "The optimization is not working correctly!" + exit 1 + fi + + - name: Validate multicall binary locale embedding + shell: bash + run: | + ## Validate that multicall binary embeds all utility locale files + echo "Validating multicall binary locale embedding..." + if [ "$multi_locales" -ge 80 ]; then + echo "✓ SUCCESS: multicall binary has all locales ($multi_locales files)" + else + echo "✗ FAILURE: multicall binary has too few embedded locale files ($multi_locales). Expected ≥ 80." + echo "This indicates the multicall binary is not getting all required locales." + exit 1 + fi + + - name: Finalize locale embedding tests + shell: bash + run: | + ## Clean up and report overall test results + rm -f test.txt target/uucore_target_util.txt + echo "✓ All locale embedding regression tests passed" + echo "Summary:" + echo " - cat binary: $cat_locales locale files (targeted embedding)" + echo " - ls binary: $ls_locales locale files (targeted embedding)" + echo " - multicall binary: $multi_locales locale files (full embedding)" diff --git a/.github/workflows/wsl2.yml b/.github/workflows/wsl2.yml new file mode 100644 index 00000000000..4f342847c88 --- /dev/null +++ b/.github/workflows/wsl2.yml @@ -0,0 +1,69 @@ +name: WSL2 + +# spell-checker:ignore nextest noprofile norc + +on: + pull_request: + push: + branches: + - '*' + +permissions: + contents: read # to fetch code (actions/checkout) + +# End the current execution if there is a new changeset in the PR. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + test: + name: Test + runs-on: ${{ matrix.job.os }} + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + job: + - { os: windows-latest, distribution: Ubuntu-24.04, features: feat_os_unix} + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Install WSL2 + uses: Vampire/setup-wsl@v6.0.0 + with: + additional-packages: build-essential + distribution: ${{ matrix.job.distribution }} + use-cache: 'true' + wsl-version: 2 + - name: Set up WSL2 user + shell: wsl-bash {0} + run: | + useradd -m -p password user + - name: Set up WSL2 shell command + uses: Vampire/setup-wsl@v6.0.0 + with: + distribution: ${{ matrix.job.distribution }} + wsl-shell-command: bash -c "sudo -u user bash --noprofile --norc -euo pipefail '{0}'" + # it is required to use WSL2's linux file system, so we do a second checkout there + - name: Checkout source in WSL2 + shell: wsl-bash {0} + run: | + git clone "$(pwd)" "$HOME/src" + - name: Install rust and nextest + shell: wsl-bash {0} + run: | + curl https://sh.rustup.rs -sSf --output rustup.sh + sh rustup.sh -y --profile=minimal + curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C "$HOME/.cargo/bin" + - name: Test + shell: wsl-bash {0} + run: | + cd "$HOME/src" + # chmod tests expect umask 0022 + umask 0022 + . "$HOME/.cargo/env" + export CARGO_TERM_COLOR=always + export RUST_BACKTRACE=1 + cargo nextest run --hide-progress-bar --profile ci --features '${{ matrix.job.features }}' diff --git a/.gitignore b/.gitignore index ed4e54ec5bc..7528e5f5380 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ +# spell-checker:ignore (misc) direnv + target/ +coverage/ /src/*/gen_table /build/ /tmp/ @@ -10,9 +13,11 @@ target/ .*.swp .*.swo .idea -Cargo.lock lib*.a /docs/_build *.iml ### macOS ### .DS_Store + +### direnv ### +/.direnv/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f90466bed2f..c4c511210c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,50 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks repos: -- repo: local + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 hooks: - - id: rust-linting + - id: check-added-large-files + - id: check-executables-have-shebangs + - id: check-json + - id: check-shebang-scripts-are-executable + exclude: '.+\.rs' # would be triggered by #![some_attribute] + - id: check-symlinks + - id: check-toml + - id: check-yaml + args: [ --allow-multiple-documents ] + - id: destroyed-symlinks + - id: end-of-file-fixer + - id: mixed-line-ending + args: [ --fix=lf ] + - id: trailing-whitespace + + - repo: https://github.com/mozilla-l10n/moz-fluent-linter + rev: v0.4.8 + hooks: + - id: fluent_linter + files: \.ftl$ + args: [--config, .github/fluent_linter_config.yml, src/uu/] + + - repo: local + hooks: + - id: rust-linting name: Rust linting description: Run cargo fmt on files included in the commit. - entry: cargo +nightly fmt -- + entry: cargo +stable fmt -- pass_filenames: true types: [file, rust] language: system - - id: rust-clippy + - id: rust-clippy name: Rust clippy description: Run cargo clippy on files included in the commit. - entry: cargo +nightly clippy --workspace --all-targets --all-features -- + entry: cargo +stable clippy --workspace --all-targets --all-features -- -D warnings pass_filenames: false types: [file, rust] 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 diff --git a/.vscode/cSpell.json b/.vscode/cSpell.json index 6ceb038c218..51dd4a30ce9 100644 --- a/.vscode/cSpell.json +++ b/.vscode/cSpell.json @@ -1,4 +1,5 @@ // `cspell` settings +// spell-checker:ignore oranda { // version of the setting file "version": "0.2", @@ -18,15 +19,20 @@ // files to ignore (globs supported) "ignorePaths": [ + ".git/**", "Cargo.lock", "oranda.json", "target/**", "tests/**/fixtures/**", "src/uu/dd/test-resources/**", "vendor/**", - "**/*.svg" + "**/*.svg", + "src/uu/*/locales/*.ftl", + "src/uucore/locales/*.ftl" ], + "enableGlobDot": true, + // words to ignore (even if they are in the flagWords) "ignoreWords": [], diff --git a/.vscode/cspell.dictionaries/acronyms+names.wordlist.txt b/.vscode/cspell.dictionaries/acronyms+names.wordlist.txt index 4a59ed094bd..8993f5d6a31 100644 --- a/.vscode/cspell.dictionaries/acronyms+names.wordlist.txt +++ b/.vscode/cspell.dictionaries/acronyms+names.wordlist.txt @@ -46,6 +46,7 @@ Codacy Cygwin Deno EditorConfig +EPEL FreeBSD Gmail GNU diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index 20e26990f3b..d1f36618c87 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -10,8 +10,10 @@ bytewise canonicalization canonicalize canonicalizing +capget codepoint codepoints +codeready codegen colorizable colorize @@ -37,6 +39,7 @@ executable executables exponentiate eval +esac falsey fileio filesystem @@ -45,6 +48,8 @@ flamegraph fsxattr fullblock getfacl +getfattr +getopt gibi gibibytes glob @@ -64,6 +69,8 @@ kibi kibibytes libacl lcase +listxattr +llistxattr lossily lstat mebi @@ -74,6 +81,7 @@ microbenchmarks microbenchmarking multibyte multicall +nmerge noatime nocache nocreat @@ -84,6 +92,7 @@ nolinks nonblock nonportable nonprinting +nonseekable notrunc noxfer ofile @@ -106,7 +115,9 @@ seedable semver semiprime semiprimes +setcap setfacl +setfattr shortcode shortcodes siginfo @@ -135,11 +146,14 @@ whitespace wordlist wordlists xattrs +xpass # * abbreviations consts deps dev +fdlimit +inacc maint proc procs @@ -155,3 +169,9 @@ retval subdir val vals +inval +nofield + +# * clippy +uninlined +nonminimal diff --git a/.vscode/cspell.dictionaries/shell.wordlist.txt b/.vscode/cspell.dictionaries/shell.wordlist.txt index 11ce341addf..b1ddec7a4e0 100644 --- a/.vscode/cspell.dictionaries/shell.wordlist.txt +++ b/.vscode/cspell.dictionaries/shell.wordlist.txt @@ -103,3 +103,4 @@ xargs # * directories sbin +libexec diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index c3c854a4cd5..eabcfb611c5 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -20,12 +20,13 @@ exacl filetime formatteriteminfo fsext -fundu getopts getrandom globset indicatif itertools +iuse +langid lscolors mdbook memchr @@ -47,6 +48,7 @@ termsize termwidth textwrap thiserror +unic ureq walkdir winapi @@ -136,6 +138,7 @@ vmsplice # * vars/libc COMFOLLOW +EXDEV FILENO FTSENT HOSTSIZE @@ -151,6 +154,7 @@ IFSOCK IRGRP IROTH IRUSR +ISDIR ISGID ISUID ISVTX @@ -166,6 +170,7 @@ RTLD_NEXT RTLD SIGINT SIGKILL +SIGSTOP SIGTERM SYS_fdatasync SYS_syncfs @@ -201,6 +206,7 @@ setgid setgroups settime setuid +socketpair socktype statfs statp @@ -305,6 +311,7 @@ freecon getfilecon lgetfilecon lsetfilecon +restorecon setfilecon # * vars/uucore @@ -323,15 +330,21 @@ libc libstdbuf musl tmpd +uchild ucmd ucommand utmpx uucore uucore_procs uudoc +uufuzz uumain uutil +uutests uutils # * function names getcwd + +# * other +algs diff --git a/.vscode/extensions.json b/.vscode/extensions.json index b9c161a4c28..7ee2695db0a 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,4 +1,4 @@ -// spell-checker:ignore (misc) matklad +// spell-checker:ignore (misc) matklad foxundermoon // see for the documentation about the extensions.json format // * // "foxundermoon.shell-format" ~ shell script formatting ; note: ENABLE "Use EditorConfig" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b10d3d11472..8668c9a27eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,9 @@ Now follows a very important warning: > uutils is original code and cannot contain any code from GNU or > other implementations. This means that **we cannot accept any changes based on > the GNU source code**. To make sure that cannot happen, **you cannot link to -> the GNU source code** either. +> the GNU source code** either. It is however possible to look at other implementations +> under a BSD or MIT license like [Apple's implementation](https://github.com/apple-oss-distributions/file_cmds/) +> or [OpenBSD](https://github.com/openbsd/src/tree/master/bin). Finally, feel free to join our [Discord](https://discord.gg/wQVJbvJ)! @@ -27,13 +29,15 @@ Finally, feel free to join our [Discord](https://discord.gg/wQVJbvJ)! uutils is a big project consisting of many parts. Here are the most important parts for getting started: -- [`src/uu`](./src/uu/): The code for all utilities -- [`src/uucore`](./src/uucore/): Crate containing all the shared code between +- [`src/uu`](https://github.com/uutils/coreutils/tree/main/src/uu/): The code for all utilities +- [`src/uucore`](https://github.com/uutils/coreutils/tree/main/src/uucore/): Crate containing all the shared code between the utilities. -- [`tests/by-util`](./tests/by-util/): The tests for all utilities. -- [`src/bin/coreutils.rs`](./src/bin/coreutils.rs): Code for the multicall +- [`tests/by-util`](https://github.com/uutils/coreutils/tree/main/tests/by-util/): The tests for all utilities. +- [`src/bin/coreutils.rs`](https://github.com/uutils/coreutils/tree/main/src/bin/coreutils.rs): Code for the multicall binary. -- [`docs`](./docs/src): the documentation for the website +- [`docs`](https://github.com/uutils/coreutils/tree/main/docs/src): the documentation for the website +- [`tests/uutests/`](https://github.com/uutils/coreutils/tree/main/tests/uutests/): + Crate implementing the various functions to test uutils commands. Each utility is defined as a separate crate. The structure of each of these crates is as follows: @@ -202,8 +206,8 @@ To ensure easy collaboration, we have guidelines for using Git and GitHub. ### Commit messages -You can read this section in the Git book to learn how to write good commit -messages: https://git-scm.com/book/ch5-2.html. +You can read [this section in the Git book](https://git-scm.com/book/ms/v2/Distributed-Git-Contributing-to-a-Project) to learn how to write good commit +messages. In addition, here are a few examples for a summary line when committing to uutils: @@ -255,8 +259,7 @@ 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: - +VirtualBox and Parallels) for development [on their official download page](https://developer.microsoft.com/windows/downloads/virtual-machines/). ## Improving the GNU compatibility @@ -303,6 +306,7 @@ completions: - [OpenBSD](https://github.com/openbsd/src/tree/master/bin) - [Busybox](https://github.com/mirror/busybox/tree/master/coreutils) - [Toybox (Android)](https://github.com/landley/toybox/tree/master/toys/posix) +- [Mac OS](https://github.com/apple-oss-distributions/file_cmds/) - [V lang](https://github.com/vlang/coreutils) - [SerenityOS](https://github.com/SerenityOS/serenity/tree/master/Userland/Utilities) - [Initial Unix](https://github.com/dspinellis/unix-history-repo) diff --git a/Cargo.lock b/Cargo.lock index f4d616dc518..859d09b3a48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,22 +1,28 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.0.4" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -32,78 +38,111 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "ansi-width" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219e3ce6f2611d83b51ec2098a12702112c29e57203a6b0a0929b2cddb486608" +dependencies = [ + "unicode-width 0.1.14", +] + [[package]] name = "anstream" -version = "0.5.0" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.0" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.0" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", ] +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + [[package]] name = "arrayref" -version = "0.3.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "bigdecimal" -version = "0.4.0" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5274a6b6e0ee020148397245b973e30163b7bffbc6d473613f850cb99888581e" +checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013" dependencies = [ + "autocfg", "libm", "num-bigint", "num-integer", @@ -119,26 +158,44 @@ dependencies = [ "compare", ] +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "bindgen" -version = "0.63.0" +version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.1", "cexpr", "clang-sys", - "lazy_static", - "lazycell", + "itertools 0.13.0", "log", - "peeking_take_while", + "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "syn 1.0.109", - "which", + "syn", ] [[package]] @@ -149,15 +206,27 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bitvec" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] [[package]] name = "blake2b_simd" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" dependencies = [ "arrayref", "arrayvec", @@ -166,9 +235,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.5.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" dependencies = [ "arrayref", "arrayvec", @@ -179,18 +248,18 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bstr" -version = "1.9.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", "regex-automata", @@ -199,15 +268,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.11.1" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" [[package]] name = "bytecount" -version = "0.6.7" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "byteorder" @@ -215,11 +284,20 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" -version = "1.0.79" +version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +dependencies = [ + "shlex", +] [[package]] name = "cexpr" @@ -227,38 +305,65 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.52.0", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", ] [[package]] name = "clang-sys" -version = "1.4.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", @@ -267,46 +372,46 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.2" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.4.2" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", - "terminal_size 0.2.6", + "terminal_size", ] [[package]] name = "clap_complete" -version = "4.4.0" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "586a385f7ef2f8b4d86bddaa0c094794e7ccbfe5ffef1f434fe928143fc783a5" +checksum = "4d9501bd3f5f09f7bbee01da9a511073ed30a80cd7a509f1214bb74eadea71ad" dependencies = [ "clap", ] [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "clap_mangen" -version = "0.2.9" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0f09a0ca8f0dd8ac92c546b426f466ef19828185c6d504c80c48c9c2768ed9" +checksum = "27b4c3c54b30f0d9adcb47f25f61fcce35c4dd8916638c6b82fbd5f4fb4179e2" dependencies = [ "clap", "roff", @@ -314,9 +419,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "compare" @@ -326,22 +431,22 @@ checksum = "120133d4db2ec47efe2e26502ee984747630c67f51974fca0b6c1340cf2368d3" [[package]] name = "console" -version = "0.15.8" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "unicode-width", - "windows-sys 0.52.0", + "once_cell", + "unicode-width 0.2.1", + "windows-sys 0.60.2", ] [[package]] name = "const-random" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11df32a13d7892ec42d51d3d175faba5211ffe13ed25d4fb348ac9e9ce835593" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" dependencies = [ "const-random-macro", ] @@ -352,57 +457,58 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom", + "getrandom 0.2.16", "once_cell", "tiny-keccak", ] [[package]] name = "constant_time_eq" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] -name = "conv" -version = "0.3.3" +name = "convert_case" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" dependencies = [ - "custom_derive", + "unicode-segmentation", ] [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "coreutils" -version = "0.0.25" +version = "0.1.0" dependencies = [ + "bincode", "chrono", "clap", "clap_complete", "clap_mangen", - "conv", + "ctor", "filetime", "glob", "hex-literal", "libc", "nix", - "once_cell", + "num-prime", "phf", "phf_codegen", "pretty_assertions", - "procfs", - "rand", - "rand_pcg", + "rand 0.9.2", "regex", "rlimit", "rstest", "selinux", + "serde", + "serde-big-array", "sha1", "tempfile", "textwrap", @@ -512,141 +618,102 @@ dependencies = [ "uu_yes", "uucore", "uuhelp_parser", + "uutests", "walkdir", "xattr", "zip", ] [[package]] -name = "coz" -version = "0.1.3" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef55b3fe2f5477d59e12bc792e8b3c95a25bd099eadcfae006ecea136de76e2" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", - "once_cell", ] [[package]] -name = "cpp" -version = "0.5.9" +name = "crc32fast" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa65869ef853e45c60e9828aa08cdd1398cb6e13f3911d9cb2a079b144fcd64" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "cpp_macros", + "cfg-if", ] [[package]] -name = "cpp_build" -version = "0.5.9" +name = "criterion" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e361fae2caf9758164b24da3eedd7f7d7451be30d90d8e7b5d2be29a2f0cf5b" +checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679" dependencies = [ - "cc", - "cpp_common", - "lazy_static", - "proc-macro2", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools 0.13.0", + "num-traits", + "oorandom", + "plotters", + "rayon", "regex", - "syn 2.0.32", - "unicode-xid", -] - -[[package]] -name = "cpp_common" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1a2532e4ed4ea13031c13bc7bc0dbca4aae32df48e9d77f0d1e743179f2ea1" -dependencies = [ - "lazy_static", - "proc-macro2", - "syn 2.0.32", -] - -[[package]] -name = "cpp_macros" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47ec9cc90633446f779ef481a9ce5a0077107dd5b87016440448d908625a83fd" -dependencies = [ - "aho-corasick", - "byteorder", - "cpp_common", - "lazy_static", - "proc-macro2", - "quote", - "syn 2.0.32", -] - -[[package]] -name = "cpufeatures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", + "serde", + "serde_json", + "tinytemplate", + "walkdir", ] [[package]] -name = "crossbeam-channel" -version = "0.5.10" +name = "criterion-plot" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a9b73a36529d9c47029b9fb3a6f0ea3cc916a261195352ba19e770fc1748b2" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ - "cfg-if", - "crossbeam-utils", + "cast", + "itertools 0.10.5", ] [[package]] name = "crossbeam-deque" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.17" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.18" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" -dependencies = [ - "cfg-if", -] +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" -version = "0.27.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.9.1", "crossterm_winapi", - "libc", + "derive_more", + "document-features", + "filedescriptor", "mio", "parking_lot", + "rustix", "signal-hook", "signal-hook-mio", "winapi", @@ -663,9 +730,9 @@ dependencies = [ [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "crypto-common" @@ -678,32 +745,42 @@ dependencies = [ ] [[package]] -name = "ctrlc" -version = "3.4.4" +name = "ctor" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" +checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb" dependencies = [ - "nix", - "windows-sys 0.52.0", + "ctor-proc-macro", + "dtor", ] [[package]] -name = "custom_derive" -version = "0.1.7" +name = "ctor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" + +[[package]] +name = "ctrlc" +version = "3.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" +checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73" +dependencies = [ + "nix", + "windows-sys 0.59.0", +] [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "data-encoding-macro" -version = "0.1.14" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20c01c06f5f429efdf2bae21eb67c28b3df3cf85b7dd2d8ef09c0838dac5d33e" +checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -711,12 +788,53 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.12" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0047d07f2c89b17dd631c80450d69841a6b5d7fb17278cbc43d7e4cfcf2576f3" +checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -735,60 +853,91 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dlv-list" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d529fd73d344663edfd598ccb3f344e46034db51ebd103518eae34338248ad73" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" dependencies = [ "const-random", ] [[package]] name = "dns-lookup" -version = "2.0.4" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5766087c2235fec47fafa4cfecc81e494ee679d0fd4a59887ea0919bfb0e4fc" +checksum = "91adf1f5ae09290d87cca8f4f0a8e49bcc30672993eb8aa11a5c9d8872d16a98" dependencies = [ "cfg-if", "libc", "socket2", - "windows-sys 0.48.0", + "windows-sys 0.60.2", +] + +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + +[[package]] +name = "dtor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58a0764cddb55ab28955347b45be00ade43d4d6f3ba4bf3dc354e4ec9432934" +dependencies = [ + "dtor-proc-macro", ] +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dunce" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.8.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] -name = "env_logger" -version = "0.8.4" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" -dependencies = [ - "log", - "regex", -] +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", "windows-sys 0.52.0", @@ -800,7 +949,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22be12de19decddab85d09f251ec8363f060ccb22ec9c81bc157c0c8433946d8" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.9.1", "log", "scopeguard", "uuid", @@ -808,9 +957,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "file_diff" @@ -818,164 +967,177 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31a7a908b8f32538a2143e59a6e4e2508988832d5d4d6f7c156b3cbc762643a5" +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "filetime" -version = "0.2.23" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", - "windows-sys 0.52.0", + "libredox", + "windows-sys 0.59.0", +] + +[[package]] +name = "fixed_decimal" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35943d22b2f19c0cb198ecf915910a8158e94541c89dcc63300d7799d46c2c5e" +dependencies = [ + "displaydoc", + "smallvec", + "writeable", ] [[package]] name = "flate2" -version = "1.0.24" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] [[package]] -name = "fnv" -version = "1.0.7" +name = "fluent" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" +dependencies = [ + "fluent-bundle", + "unic-langid", +] [[package]] -name = "fs_extra" -version = "1.3.0" +name = "fluent-bundle" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell", + "smallvec", + "unic-langid", +] [[package]] -name = "fsevent-sys" -version = "4.1.0" +name = "fluent-langneg" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" dependencies = [ - "libc", + "unic-langid", ] [[package]] -name = "fts-sys" -version = "0.2.4" +name = "fluent-syntax" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a66c0a21e344f20c87b4ca12643cf4f40a7018f132c98d344e989b959f49dd1" +checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" dependencies = [ - "bindgen", - "libc", + "memchr", + "thiserror 2.0.16", ] [[package]] -name = "fundu" -version = "2.0.0" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c04cb831a8dccadfe3774b07cba4574a1ec24974d761510e65d8a543c2d7cb4" -dependencies = [ - "fundu-core", -] +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "fundu-core" -version = "0.3.0" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a889e633afd839fb5b04fe53adfd588cefe518e71ec8d3c929698c6daf2acd" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "futures" -version = "0.3.28" +name = "fs_extra" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] -name = "futures-channel" -version = "0.3.28" +name = "fsevent-sys" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" dependencies = [ - "futures-core", - "futures-sink", + "libc", ] [[package]] -name = "futures-core" -version = "0.3.28" +name = "fts-sys" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "43119ec0f2227f8505c8bb6c60606b5eefc328607bfe1a421e561c4decfa02ab" +dependencies = [ + "bindgen", + "libc", +] [[package]] -name = "futures-executor" -version = "0.3.28" +name = "funty" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] -name = "futures-io" -version = "0.3.28" +name = "futures-core" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn", ] -[[package]] -name = "futures-sink" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" - [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", - "futures-io", "futures-macro", - "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", "slab", @@ -989,9 +1151,9 @@ checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -999,26 +1161,38 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "wasi", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] name = "glob" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "half" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", @@ -1026,15 +1200,20 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.13.2" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] -name = "hermit-abi" -version = "0.3.2" +name = "hashbrown" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "hex" @@ -1044,33 +1223,34 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hex-literal" -version = "0.4.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71" [[package]] name = "hostname" -version = "0.3.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" dependencies = [ + "cfg-if", "libc", - "match_cfg", - "winapi", + "windows-link", ] [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "winapi", + "windows-core", ] [[package]] @@ -1082,25 +1262,193 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collator" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ad4c6a556938dfd31f75a8c54141079e8821dc697ffb799cfe0f0fa11f2edc" +dependencies = [ + "displaydoc", + "icu_collator_data", + "icu_collections", + "icu_locale", + "icu_locale_core", + "icu_normalizer", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_collator_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d880b8e680799eabd90c054e1b95526cd48db16c95269f3c89fb3117e1ac92c5" + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_decimal" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec61c43fdc4e368a9f450272833123a8ef0d7083a44597660ce94d791b8a2e2" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_decimal_data", + "icu_locale", + "icu_locale_core", + "icu_provider", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_decimal_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b70963bc35f9bdf1bc66a5c1f458f4991c1dc71760e00fa06016b2c76b2738d5" + +[[package]] +name = "icu_locale" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ae5921528335e91da1b6c695dbf1ec37df5ac13faa3f91e5640be93aa2fbefd" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fdef0c124749d06a743c69e938350816554eb63ac979166590e2b4ee4252765" + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.4", +] + [[package]] name = "indicatif" -version = "0.17.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729" +checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" dependencies = [ "console", - "number_prefix", "portable-atomic", - "unicode-width", + "unicode-width 0.2.1", + "unit-prefix", + "web-time", ] [[package]] name = "inotify" -version = "0.9.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.1", "inotify-sys", "libc", ] @@ -1115,54 +1463,128 @@ dependencies = [ ] [[package]] -name = "io-lifetimes" -version = "1.0.11" +name = "intl-memoizer" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", + "type-map", + "unic-langid", ] +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" -version = "0.12.1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.4" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", + "windows-sys 0.52.0", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "keccak" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ "cpufeatures", ] [[package]] name = "kqueue" -version = "1.0.7" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" dependencies = [ "kqueue-sys", "libc", @@ -1170,65 +1592,85 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" dependencies = [ "bitflags 1.3.2", "libc", ] [[package]] -name = "lazy_static" -version = "1.4.0" +name = "libc" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] -name = "lazycell" -version = "1.3.0" +name = "libloading" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.2", +] [[package]] -name = "libc" -version = "0.2.153" +name = "libm" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] -name = "libloading" -version = "0.7.4" +name = "libredox" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "cfg-if", - "winapi", + "bitflags 2.9.1", + "libc", + "redox_syscall", ] [[package]] -name = "libm" -version = "0.2.7" +name = "libz-rs-sys" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", +] [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d6a630ed4f43c11056af8768c4773df2c43bc780b6d8a46de345c17236c562" + +[[package]] +name = "litemap" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -1236,24 +1678,28 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] -name = "lscolors" -version = "0.16.0" +name = "lru" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0b209ec3976527806024406fe765474b9a1750a0ed4b8f0372364741f50e7b" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "nu-ansi-term", + "hashbrown 0.15.4", ] [[package]] -name = "match_cfg" -version = "0.1.0" +name = "lscolors" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +checksum = "61183da5de8ba09a58e330d55e5ea796539d8443bd00fdeb863eac39724aa4ab" +dependencies = [ + "aho-corasick", + "nu-ansi-term", +] [[package]] name = "md-5" @@ -1267,19 +1713,28 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" -version = "0.9.0" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deaba38d7abf1d4cca21cc89e932e542ba2b9258664d2a9ef0e61512039c9375" +checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1288,35 +1743,36 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.5.4" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ - "adler", + "adler2", ] [[package]] name = "mio" -version = "0.8.11" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", - "wasi", - "windows-sys 0.48.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] name = "nix" -version = "0.28.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.9.1", "cfg-if", "cfg_aliases", "libc", + "memoffset", ] [[package]] @@ -1329,68 +1785,115 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "notify" -version = "6.0.1" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5738a2795d57ea20abec2d6d76c6081186709c0024187cd5977265eda6598b51" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 1.3.2", - "crossbeam-channel", - "filetime", + "bitflags 2.9.1", "fsevent-sys", "inotify", "kqueue", "libc", + "log", "mio", + "notify-types", "walkdir", - "windows-sys 0.45.0", + "windows-sys 0.60.2", ] +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "nu-ansi-term" -version = "0.49.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", + "rand 0.8.5", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] +[[package]] +name = "num-modular" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a5fe11d4135c3bcdf3a95b18b194afa9608a5f6ff034f5d857bc9a27fb0119" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-prime" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e238432a7881ec7164503ccc516c014bf009be7984cde1ba56837862543bdec3" +dependencies = [ + "bitvec", + "either", + "lru", + "num-bigint", + "num-integer", + "num-modular", + "num-traits", + "rand 0.8.5", +] + [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_threads" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] @@ -1403,17 +1906,23 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "onig" -version = "6.4.0" +version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.1", "libc", "once_cell", "onig_sys", @@ -1421,38 +1930,44 @@ dependencies = [ [[package]] name = "onig_sys" -version = "69.8.1" +version = "69.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" dependencies = [ "cc", "pkg-config", ] +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "ordered-multimap" -version = "0.6.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ "dlv-list", - "hashbrown", + "hashbrown 0.14.5", ] [[package]] name = "os_display" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6229bad892b46b0dcfaaeb18ad0d2e56400f5aaea05b768bde96e73676cf75" +checksum = "ad5fd71b79026fb918650dde6d125000a233764f1c2f1659a1c71118e33ea08f" dependencies = [ - "unicode-width", + "unicode-width 0.2.1", ] [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -1460,47 +1975,44 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall", "smallvec", - "windows-targets 0.48.0", + "windows-targets 0.52.6", ] [[package]] name = "parse_datetime" -version = "0.5.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bbf4e25b13841080e018a1e666358adfe5e39b6d353f986ca5091c210b586a1" +checksum = "c5b77d27257a460cefd73a54448e5f3fd4db224150baf6ca3e02eedf4eb2b3e9" dependencies = [ "chrono", + "num-traits", "regex", + "winnow", ] -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "phf" -version = "0.11.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" dependencies = [ "phf_shared", + "serde", ] [[package]] name = "phf_codegen" -version = "0.11.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +checksum = "efbdcb6f01d193b17f0b9c3360fa7e0e620991b193ff08702f78b3ce365d7e61" dependencies = [ "phf_generator", "phf_shared", @@ -1508,28 +2020,28 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.1" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b" dependencies = [ + "fastrand", "phf_shared", - "rand", ] [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" dependencies = [ "siphasher", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1539,100 +2051,147 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "platform-info" -version = "2.0.2" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6259c4860e53bf665016f1b2f46a8859cadfa717581dc9d597ae4069de6300f" +checksum = "7539aeb3fdd8cb4f6a331307cf71a1039cee75e94e8a71725b9484f4a0d9451a" dependencies = [ "libc", "winapi", ] +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "portable-atomic" -version = "0.3.15" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15eb2c6e362923af47e13c23ca5afb859e83d54452c55b0b9ac763b8f7c1ac16" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] -name = "ppv-lite86" -version = "0.2.17" +name = "portable-atomic-util" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] [[package]] -name = "pretty_assertions" -version = "1.4.0" +name = "potential_utf" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" dependencies = [ - "diff", - "yansi", + "serde", + "zerovec", ] [[package]] -name = "proc-macro2" -version = "1.0.63" +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "unicode-ident", + "zerocopy 0.8.25", ] [[package]] -name = "procfs" -version = "0.16.0" +name = "pretty_assertions" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ - "bitflags 2.4.2", - "hex", - "lazy_static", - "procfs-core", - "rustix 0.38.31", + "diff", + "yansi", ] [[package]] -name = "procfs-core" -version = "0.16.0" +name = "prettyplease" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" dependencies = [ - "bitflags 2.4.2", - "hex", + "proc-macro2", + "syn", ] [[package]] -name = "quick-error" -version = "2.0.1" +name = "proc-macro-crate" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] [[package]] -name = "quickcheck" -version = "1.0.3" +name = "proc-macro2" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ - "env_logger", - "log", - "rand", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.29" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -1640,8 +2199,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -1651,7 +2220,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -1660,23 +2239,23 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", ] [[package]] -name = "rand_pcg" -version = "0.3.1" +name = "rand_core" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "rand_core", + "getrandom 0.3.3", ] [[package]] name = "rayon" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -1684,9 +2263,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1694,33 +2273,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.1", ] -[[package]] -name = "redox_syscall" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c178f952cc7eac391f3124bd9851d1ac0bdbc4c9de2d892ccd5f0d8b160e96" -dependencies = [ - "bitflags 2.4.2", -] - -[[package]] -name = "reference-counted-singleton" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bfbf25d7eb88ddcbb1ec3d755d0634da8f7657b2cb8b74089121409ab8228f" - [[package]] name = "regex" -version = "1.10.4" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -1730,9 +2294,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.4" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -1741,65 +2305,65 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "relative-path" -version = "1.8.0" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bf2521270932c3c7bed1a59151222bd7643c79310f2916f01925e1e16255698" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "rlimit" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3560f70f30a0f16d11d01ed078a07740fe6b489667abc7c7b029155d9f21c3d8" +checksum = "7043b63bd0cd1aaa628e476b80e6d4023a3b50eb32789f2728908107bd0c793a" dependencies = [ "libc", ] [[package]] name = "roff" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" +checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" [[package]] name = "rstest" -version = "0.18.2" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" dependencies = [ - "futures", "futures-timer", + "futures-util", "rstest_macros", - "rustc_version", ] [[package]] name = "rstest_macros" -version = "0.18.2" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" dependencies = [ "cfg-if", "glob", + "proc-macro-crate", "proc-macro2", "quote", "regex", "relative-path", "rustc_version", - "syn 2.0.32", + "syn", "unicode-ident", ] [[package]] name = "rust-ini" -version = "0.19.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +checksum = "e7295b7ce3bf4806b419dc3420745998b447178b7005e2011947b38fc5aa6791" dependencies = [ "cfg-if", "ordered-multimap", @@ -1807,46 +2371,44 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.37.26" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84f3f8f960ed3b5a59055428714943298bf3fa2d4a1d53135084e0544829d995" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", + "semver", ] [[package]] name = "rustix" -version = "0.38.31" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.9.1", "errno", "libc", - "linux-raw-sys 0.4.12", + "linux-raw-sys 0.9.4", "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "same-file" version = "1.0.6" @@ -1864,29 +2426,30 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "self_cell" -version = "1.0.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba" +checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" [[package]] name = "selinux" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00576725d21b588213fbd4af84cd7e4cc4304e8e9bd6c0f5a1498a3e2ca6a51" +checksum = "2ef2ca58174235414aee5465f5d8ef9f5833023b31484eb52ca505f306f4573c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.1", + "errno", "libc", "once_cell", - "reference-counted-singleton", + "parking_lot", "selinux-sys", - "thiserror", + "thiserror 2.0.16", ] [[package]] name = "selinux-sys" -version = "0.6.2" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806d381649bb85347189d2350728817418138d11d738e2482cb644ec7f3c755d" +checksum = "280da3df1236da180be5ac50a893b26a1d3c49e3a44acb2d10d1f082523ff916" dependencies = [ "bindgen", "cc", @@ -1896,28 +2459,49 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.14" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", ] [[package]] @@ -1933,9 +2517,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -1960,9 +2544,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -1970,9 +2554,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio", @@ -1981,24 +2565,30 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "siphasher" -version = "0.3.10" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -2014,37 +2604,43 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smawk" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" dependencies = [ "proc-macro2", "quote", @@ -2052,89 +2648,109 @@ dependencies = [ ] [[package]] -name = "syn" -version = "2.0.32" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" -version = "3.10.1" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ - "cfg-if", "fastrand", - "rustix 0.38.31", + "getrandom 0.3.3", + "once_cell", + "rustix", "windows-sys 0.52.0", ] [[package]] name = "terminal_size" -version = "0.2.6" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "rustix 0.37.26", - "windows-sys 0.48.0", + "rustix", + "windows-sys 0.60.2", ] [[package]] -name = "terminal_size" -version = "0.3.0" +name = "textwrap" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ - "rustix 0.38.31", - "windows-sys 0.48.0", + "smawk", + "terminal_size", + "unicode-linebreak", + "unicode-width 0.2.1", ] [[package]] -name = "textwrap" -version = "0.16.1" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "smawk", - "terminal_size 0.2.6", - "unicode-linebreak", - "unicode-width", + "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" -version = "1.0.37" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl 2.0.16", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "thiserror-impl", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] name = "time" -version = "0.3.20" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ + "deranged", "itoa", "libc", + "num-conv", "num_threads", + "powerfmt", "serde", "time-core", "time-macros", @@ -2142,16 +2758,17 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.8" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ + "num-conv", "time-core", ] @@ -2164,17 +2781,81 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash", +] + [[package]] name = "typenum" -version = "1.15.0" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "tinystr", +] [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-linebreak" @@ -2184,159 +2865,228 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-segmentation" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] -name = "unicode-xid" -version = "0.2.4" +name = "unicode-width" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "unindent" -version = "0.2.1" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "unit-prefix" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa30f5ea51ff7edfc797c6d3f9ec8cbd8cfedef5371766b7181d33977f4814f" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "utmp-classic" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24c654e19afaa6b8f3877ece5d3bed849c2719c56f6752b18ca7da4fcc6e85a" +dependencies = [ + "cfg-if", + "libc", + "thiserror 1.0.69", + "time", + "utmp-classic-raw", + "zerocopy 0.7.35", +] + +[[package]] +name = "utmp-classic-raw" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "22c226537a3d6e01c440c1926ca0256dbee2d19b2229ede6fc4863a6493dd831" +dependencies = [ + "cfg-if", + "zerocopy 0.7.35", +] [[package]] name = "uu_arch" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "platform-info", "uucore", ] [[package]] name = "uu_base32" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_base64" -version = "0.0.25" +version = "0.1.0" dependencies = [ + "clap", + "fluent", "uu_base32", "uucore", ] [[package]] name = "uu_basename" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_basenc" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uu_base32", "uucore", ] [[package]] name = "uu_cat" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", + "memchr", "nix", - "thiserror", + "tempfile", + "thiserror 2.0.16", "uucore", + "winapi-util", + "windows-sys 0.60.2", ] [[package]] name = "uu_chcon" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "fts-sys", "libc", "selinux", - "thiserror", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_chgrp" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_chmod" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", - "libc", + "fluent", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_chown" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_chroot" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_cksum" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "hex", "uucore", ] [[package]] name = "uu_comm" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_cp" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", "exacl", "filetime", + "fluent", "indicatif", "libc", - "quick-error", + "linux-raw-sys 0.10.0", "selinux", + "thiserror 2.0.16", "uucore", "walkdir", "xattr", @@ -2344,61 +3094,69 @@ dependencies = [ [[package]] name = "uu_csplit" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "regex", - "thiserror", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_cut" -version = "0.0.25" +version = "0.1.0" dependencies = [ "bstr", "clap", + "fluent", "memchr", "uucore", ] [[package]] name = "uu_date" -version = "0.0.25" +version = "0.1.0" dependencies = [ "chrono", "clap", + "fluent", + "jiff", "libc", "parse_datetime", "uucore", - "windows-sys 0.48.0", + "windows-sys 0.60.2", ] [[package]] name = "uu_dd" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "gcd", "libc", "nix", "signal-hook", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_df" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "tempfile", - "unicode-width", + "thiserror 2.0.16", + "unicode-width 0.2.1", "uucore", ] [[package]] name = "uu_dir" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", "uu_ls", @@ -2407,298 +3165,345 @@ dependencies = [ [[package]] name = "uu_dircolors" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_dirname" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_du" -version = "0.0.25" +version = "0.1.0" dependencies = [ - "chrono", "clap", + "fluent", "glob", + "thiserror 2.0.16", "uucore", - "windows-sys 0.48.0", + "windows-sys 0.60.2", ] [[package]] name = "uu_echo" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_env" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "nix", "rust-ini", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_expand" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", - "unicode-width", + "fluent", + "thiserror 2.0.16", + "unicode-width 0.2.1", "uucore", ] [[package]] name = "uu_expr" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "num-bigint", "num-traits", "onig", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_factor" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", - "coz", + "fluent", + "num-bigint", + "num-prime", "num-traits", - "quickcheck", - "rand", - "smallvec", "uucore", ] +[[package]] +name = "uu_factor_benches" +version = "0.0.0" +dependencies = [ + "array-init", + "criterion", + "num-prime", + "rand 0.9.2", + "rand_chacha 0.9.0", +] + [[package]] name = "uu_false" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_fmt" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", - "unicode-width", + "fluent", + "thiserror 2.0.16", + "unicode-width 0.2.1", "uucore", ] [[package]] name = "uu_fold" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_groups" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_hashsum" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", - "hex", - "memchr", - "regex", + "fluent", "uucore", ] [[package]] name = "uu_head" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "memchr", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_hostid" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "libc", "uucore", ] [[package]] name = "uu_hostname" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "dns-lookup", + "fluent", "hostname", "uucore", - "windows-sys 0.48.0", + "windows-sys 0.60.2", ] [[package]] name = "uu_id" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "selinux", "uucore", ] [[package]] name = "uu_install" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", "file_diff", "filetime", - "libc", + "fluent", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_join" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "memchr", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_kill" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "nix", "uucore", ] [[package]] name = "uu_link" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_ln" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_logname" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "libc", "uucore", ] [[package]] name = "uu_ls" -version = "0.0.25" +version = "0.1.0" dependencies = [ - "chrono", + "ansi-width", "clap", + "fluent", "glob", "hostname", "lscolors", - "number_prefix", - "once_cell", "selinux", - "terminal_size 0.3.0", - "unicode-width", + "terminal_size", + "thiserror 2.0.16", "uucore", "uutils_term_grid", ] [[package]] name = "uu_mkdir" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_mkfifo" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "libc", "uucore", ] [[package]] name = "uu_mknod" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "libc", "uucore", ] [[package]] name = "uu_mktemp" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", - "rand", + "fluent", + "rand 0.9.2", "tempfile", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_more" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", "crossterm", + "fluent", "nix", - "unicode-segmentation", - "unicode-width", + "tempfile", "uucore", ] [[package]] name = "uu_mv" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "fs_extra", "indicatif", + "libc", + "thiserror 2.0.16", "uucore", + "windows-sys 0.60.2", ] [[package]] name = "uu_nice" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "libc", "nix", "uucore", @@ -2706,308 +3511,343 @@ dependencies = [ [[package]] name = "uu_nl" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "regex", "uucore", ] [[package]] name = "uu_nohup" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "libc", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_nproc" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "libc", "uucore", ] [[package]] name = "uu_numfmt" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_od" -version = "0.0.25" +version = "0.1.0" dependencies = [ "byteorder", "clap", + "fluent", "half", "uucore", ] [[package]] name = "uu_paste" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_pathchk" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "libc", "uucore", ] [[package]] name = "uu_pinky" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_pr" -version = "0.0.25" +version = "0.1.0" dependencies = [ - "chrono", "clap", - "itertools", - "quick-error", + "fluent", + "itertools 0.14.0", "regex", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_printenv" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_printf" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_ptx" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "regex", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_pwd" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_readlink" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_realpath" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_rm" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "libc", + "thiserror 2.0.16", "uucore", - "walkdir", - "windows-sys 0.48.0", + "windows-sys 0.60.2", ] [[package]] name = "uu_rmdir" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "libc", "uucore", ] [[package]] name = "uu_runcon" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "libc", "selinux", - "thiserror", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_seq" -version = "0.0.25" +version = "0.1.0" dependencies = [ "bigdecimal", "clap", + "fluent", "num-bigint", "num-traits", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_shred" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "libc", - "rand", + "rand 0.9.2", "uucore", ] [[package]] name = "uu_shuf" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", - "memchr", - "rand", - "rand_core", + "fluent", + "rand 0.9.2", + "rand_core 0.9.3", "uucore", ] [[package]] name = "uu_sleep" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", - "fundu", + "fluent", "uucore", ] [[package]] name = "uu_sort" -version = "0.0.25" +version = "0.1.0" dependencies = [ + "bigdecimal", "binary-heap-plus", "clap", "compare", "ctrlc", + "fluent", "fnv", - "itertools", + "itertools 0.14.0", "memchr", - "rand", + "nix", + "rand 0.9.2", "rayon", "self_cell", "tempfile", - "unicode-width", + "thiserror 2.0.16", + "unicode-width 0.2.1", "uucore", ] [[package]] name = "uu_split" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "memchr", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_stat" -version = "0.0.25" +version = "0.1.0" dependencies = [ - "chrono", "clap", + "fluent", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_stdbuf" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "tempfile", + "thiserror 2.0.16", "uu_stdbuf_libstdbuf", "uucore", ] [[package]] name = "uu_stdbuf_libstdbuf" -version = "0.0.25" +version = "0.1.0" dependencies = [ - "cpp", - "cpp_build", + "ctor", "libc", ] [[package]] name = "uu_stty" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "nix", "uucore", ] [[package]] name = "uu_sum" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_sync" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", - "libc", + "fluent", "nix", "uucore", - "windows-sys 0.48.0", + "windows-sys 0.60.2", ] [[package]] name = "uu_tac" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "memchr", "memmap2", "regex", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_tail" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", - "fundu", + "fluent", "libc", "memchr", "notify", @@ -3015,33 +3855,36 @@ dependencies = [ "same-file", "uucore", "winapi-util", - "windows-sys 0.48.0", + "windows-sys 0.60.2", ] [[package]] name = "uu_tee" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", - "libc", + "fluent", + "nix", "uucore", ] [[package]] name = "uu_test" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "libc", - "redox_syscall 0.5.0", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_timeout" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "libc", "nix", "uucore", @@ -3049,112 +3892,131 @@ dependencies = [ [[package]] name = "uu_touch" -version = "0.0.25" +version = "0.1.0" dependencies = [ "chrono", "clap", "filetime", + "fluent", "parse_datetime", + "thiserror 2.0.16", "uucore", - "windows-sys 0.48.0", + "windows-sys 0.60.2", ] [[package]] name = "uu_tr" -version = "0.0.25" +version = "0.1.0" dependencies = [ + "bytecount", "clap", - "nom", + "fluent", + "nom 8.0.0", "uucore", ] [[package]] name = "uu_true" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_truncate" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_tsort" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", + "thiserror 2.0.16", "uucore", ] [[package]] name = "uu_tty" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "nix", "uucore", ] [[package]] name = "uu_uname" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "platform-info", "uucore", ] [[package]] name = "uu_unexpand" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", - "unicode-width", + "fluent", + "thiserror 2.0.16", + "unicode-width 0.2.1", "uucore", ] [[package]] name = "uu_uniq" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_unlink" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_uptime" -version = "0.0.25" +version = "0.1.0" dependencies = [ "chrono", "clap", + "fluent", + "thiserror 2.0.16", + "utmp-classic", "uucore", ] [[package]] name = "uu_users" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", + "utmp-classic", "uucore", ] [[package]] name = "uu_vdir" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", "uu_ls", @@ -3163,85 +4025,104 @@ dependencies = [ [[package]] name = "uu_wc" -version = "0.0.25" +version = "0.1.0" dependencies = [ "bytecount", "clap", + "fluent", "libc", "nix", - "thiserror", - "unicode-width", + "thiserror 2.0.16", + "unicode-width 0.2.1", "uucore", ] [[package]] name = "uu_who" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", + "fluent", "uucore", ] [[package]] name = "uu_whoami" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", - "libc", + "fluent", "uucore", - "windows-sys 0.48.0", + "windows-sys 0.60.2", ] [[package]] name = "uu_yes" -version = "0.0.25" +version = "0.1.0" dependencies = [ "clap", - "itertools", + "fluent", + "itertools 0.14.0", "nix", "uucore", ] [[package]] name = "uucore" -version = "0.0.25" +version = "0.1.0" dependencies = [ + "bigdecimal", "blake2b_simd", "blake3", + "bstr", + "chrono", "clap", + "crc32fast", "data-encoding", "data-encoding-macro", "digest", "dns-lookup", "dunce", + "fluent", + "fluent-bundle", + "fluent-syntax", "glob", "hex", - "itertools", + "icu_collator", + "icu_decimal", + "icu_locale", + "icu_provider", + "itertools 0.14.0", + "jiff", "libc", "md-5", "memchr", "nix", - "once_cell", + "num-traits", + "number_prefix", "os_display", + "selinux", "sha1", "sha2", "sha3", "sm3", "tempfile", - "thiserror", + "thiserror 2.0.16", "time", + "unic-langid", + "utmp-classic", "uucore_procs", "walkdir", "wild", "winapi-util", - "windows-sys 0.48.0", + "windows-sys 0.60.2", "xattr", "z85", ] [[package]] name = "uucore_procs" -version = "0.0.25" +version = "0.1.0" dependencies = [ "proc-macro2", "quote", @@ -3250,28 +4131,54 @@ dependencies = [ [[package]] name = "uuhelp_parser" -version = "0.0.25" +version = "0.1.0" [[package]] name = "uuid" -version = "1.7.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "uutests" +version = "0.1.0" +dependencies = [ + "ctor", + "libc", + "nix", + "pretty_assertions", + "rand 0.9.2", + "regex", + "rlimit", + "tempfile", + "uucore", + "xattr", +] [[package]] name = "uutils_term_grid" -version = "0.3.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b389452a568698688dda38802068378a16c15c4af9b153cdd99b65391292bbc7" +checksum = "fcba141ce511bad08e80b43f02976571072e1ff4286f7d628943efbd277c6361" dependencies = [ - "unicode-width", + "ansi-width", ] [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtue" +version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" [[package]] name = "walkdir" @@ -3285,40 +4192,50 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.32", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3326,32 +4243,44 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] -name = "which" -version = "4.3.0" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ - "either", - "libc", - "once_cell", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -3381,11 +4310,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -3395,21 +4324,62 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.45.0" +name = "windows-core" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-targets 0.42.2", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-targets 0.48.0", + "windows-link", ] [[package]] @@ -3418,211 +4388,357 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.6", ] [[package]] -name = "windows-targets" -version = "0.42.2" +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets 0.52.6", ] [[package]] -name = "windows-targets" -version = "0.48.0" +name = "windows-sys" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows-targets 0.53.2", ] [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" +name = "windows-targets" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" -version = "0.42.2" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] -name = "windows_i686_gnu" -version = "0.52.0" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_msvc" -version = "0.42.2" +name = "windows_i686_gnullvm" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" +name = "windows_x86_64_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" +name = "winnow" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "wyz" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] [[package]] name = "xattr" -version = "1.3.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" dependencies = [ "libc", - "linux-raw-sys 0.4.12", - "rustix 0.38.31", + "rustix", ] [[package]] name = "yansi" -version = "0.5.1" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] name = "z85" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a599daf1b507819c1121f0bf87fa37eb19daac6aff3aefefd4e6e2e0f2020fc" +checksum = "9b3a41ce106832b4da1c065baa4c31cf640cf965fa1483816402b7f6b96f0a64" [[package]] -name = "zip" -version = "0.6.6" +name = "zerocopy" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive 0.8.25", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aed4ac33e8eb078c89e6cbb1d5c4c7703ec6d299fc3e7c3695af8f8b423468b" +dependencies = [ + "arbitrary", "crc32fast", - "crossbeam-utils", "flate2", + "indexmap", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", ] diff --git a/Cargo.toml b/Cargo.toml index 68bdf8faed9..e508b0b628c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,25 +1,24 @@ # coreutils (uutils) # * see the repository LICENSE, README, and CONTRIBUTING files for more information -# spell-checker:ignore (libs) bigdecimal datetime fundu gethostid kqueue libselinux mangen memmap procfs uuhelp +# spell-checker:ignore (libs) bigdecimal datetime serde bincode gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested [package] name = "coreutils" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "coreutils ~ GNU coreutils (updated); implemented as universal (cross-platform) utils, written in Rust" default-run = "coreutils" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils" -readme = "README.md" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -rust-version = "1.70.0" -edition = "2021" +rust-version = "1.85.0" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true -build = "build.rs" +[package.metadata.docs.rs] +all-features = true [features] default = ["feat_common_core"] @@ -30,9 +29,15 @@ windows = ["feat_os_windows"] ## project-specific feature shortcodes nightly = [] test_unimplemented = [] +expensive_tests = [] +# "test_risky_names" == enable tests that create problematic file names (would make a network share inaccessible to Windows, breaks SVN on Mac OS, etc.) +test_risky_names = [] # * only build `uudoc` when `--feature uudoc` is activated uudoc = ["zip", "dep:uuhelp_parser"] ## features +## Optional feature for stdbuf +# "feat_external_libstdbuf" == use an external libstdbuf.so for stdbuf instead of embedding it +feat_external_libstdbuf = ["stdbuf/feat_external_libstdbuf"] # "feat_acl" == enable support for ACLs (access control lists; by using`--features feat_acl`) # NOTE: # * On linux, the posix-acl/acl-sys crate requires `libacl` headers and shared library to be accessible in the C toolchain at compile time. @@ -44,10 +49,15 @@ feat_acl = ["cp/feat_acl"] # * Running a uutils compiled with `feat_selinux` requires an SELinux enabled Kernel at run time. feat_selinux = [ "cp/selinux", + "feat_require_selinux", "id/selinux", + "install/selinux", "ls/selinux", + "mkdir/selinux", + "mkfifo/selinux", + "mknod/selinux", "selinux", - "feat_require_selinux", + "stat/selinux", ] ## ## feature sets @@ -65,11 +75,11 @@ feat_common_core = [ "csplit", "cut", "date", + "dd", "df", "dir", "dircolors", "dirname", - "dd", "du", "echo", "env", @@ -113,11 +123,11 @@ feat_common_core = [ "tail", "tee", "test", + "touch", "tr", "true", "truncate", "tsort", - "touch", "unexpand", "uniq", "unlink", @@ -144,14 +154,14 @@ feat_os_macos = [ # "feat_require_unix_hostid", ] -# "feat_os_unix" == set of utilities which can be built/run on modern/usual *nix platforms +# "feat_os_unix" == set of utilities which can be built/run on modern/usual *nix platforms. +# Also used for targets binding to the "musl" library (ref: ) feat_os_unix = [ "feat_Tier1", # - "feat_require_crate_cpp", "feat_require_unix", - "feat_require_unix_utmpx", "feat_require_unix_hostid", + "feat_require_unix_utmpx", ] # "feat_os_windows" == set of utilities which can be built/run on modern/usual windows platforms feat_os_windows = [ @@ -166,13 +176,6 @@ feat_os_unix_gnueabihf = [ "feat_require_unix_hostid", "feat_require_unix_utmpx", ] -# "feat_os_unix_musl" == set of utilities which can be built/run on targets binding to the "musl" library (ref: ) -feat_os_unix_musl = [ - "feat_Tier1", - # - "feat_require_unix", - "feat_require_unix_hostid", -] feat_os_unix_android = [ "feat_Tier1", # @@ -182,9 +185,7 @@ feat_os_unix_android = [ # # ** NOTE: these `feat_require_...` sets should be minimized as much as possible to encourage cross-platform availability of utilities # -# "feat_require_crate_cpp" == set of utilities requiring the `cpp` crate (which fail to compile on several platforms; as of 2020-04-23) -feat_require_crate_cpp = ["stdbuf"] -# "feat_require_unix" == set of utilities requiring support which is only available on unix platforms (as of 2020-04-23) +# "feat_require_unix" == set of utilities requiring support which is only available on unix platforms feat_require_unix = [ "chgrp", "chmod", @@ -201,6 +202,7 @@ feat_require_unix = [ "nohup", "pathchk", "stat", + "stdbuf", "stty", "timeout", "tty", @@ -217,8 +219,6 @@ feat_require_selinux = ["chcon", "runcon"] feat_os_unix_fuchsia = [ "feat_common_core", # - "feat_require_crate_cpp", - # "chgrp", "chmod", "chown", @@ -257,82 +257,114 @@ feat_os_windows_legacy = [ # * bypass/override ~ translate 'test' feature name to avoid dependency collision with rust core 'test' crate (o/w surfaces as compiler errors during testing) test = ["uu_test"] +[workspace] +resolver = "3" +members = [ + ".", + "src/uu/*", + "src/uu/stdbuf/src/libstdbuf", + "src/uucore", + "src/uucore_procs", + "src/uuhelp_parser", + "tests/benches/factor", + "tests/uutests", + # "fuzz", # TODO +] + +[workspace.package] +authors = ["uutils developers"] +categories = ["command-line-utilities"] +edition = "2024" +homepage = "https://github.com/uutils/coreutils" +keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] +license = "MIT" +readme = "README.package.md" +version = "0.1.0" + [workspace.dependencies] +ansi-width = "0.1.0" bigdecimal = "0.4" binary-heap-plus = "0.5.0" bstr = "1.9.1" -bytecount = "0.6.7" +bytecount = "0.6.8" byteorder = "1.5.0" -chrono = { version = "^0.4.35", default-features = false, features = [ +chrono = { version = "0.4.41", default-features = false, features = [ "std", "alloc", "clock", ] } -clap = { version = "4.4", features = ["wrap_help", "cargo"] } +clap = { version = "4.5", features = ["wrap_help", "cargo"] } clap_complete = "4.4" clap_mangen = "0.2" compare = "0.1.0" -coz = { version = "0.1.3" } -crossterm = ">=0.27.0" -ctrlc = { version = "3.4", features = ["termination"] } +crossterm = "0.29.0" +ctor = "0.5.0" +ctrlc = { version = "3.4.7", features = ["termination"] } +dns-lookup = { version = "2.0.4" } exacl = "0.12.0" file_diff = "1.0.0" -filetime = "0.2" +filetime = "0.2.23" fnv = "1.0.7" fs_extra = "1.3.0" -fts-sys = "0.2" -fundu = "2.0.0" +fts-sys = "0.2.16" gcd = "2.3" glob = "0.3.1" -half = "2.4" -hostname = "0.3" -indicatif = "0.17" -itertools = "0.12.1" -libc = "0.2.153" -lscolors = { version = "0.16.0", default-features = false, features = [ +half = "2.4.1" +hostname = "0.4" +icu_collator = "2.0.0" +icu_decimal = "2.0.0" +icu_locale = "2.0.0" +icu_provider = "2.0.0" +indicatif = "0.18.0" +itertools = "0.14.0" +jiff = { version = "0.2.10", default-features = false, features = [ + "std", + "alloc", + "tz-system", +] } +libc = "0.2.172" +linux-raw-sys = "0.10" +lscolors = { version = "0.20.0", default-features = false, features = [ "gnu_legacy", ] } -memchr = "2" -memmap2 = "0.9" -nix = { version = "0.28", default-features = false } -nom = "7.1.3" -notify = { version = "=6.0.1", features = ["macos_kqueue"] } +memchr = "2.7.2" +memmap2 = "0.9.4" +nix = { version = "0.30", default-features = false } +nom = "8.0.0" +notify = { version = "=8.2.0", features = ["macos_kqueue"] } num-bigint = "0.4.4" -num-traits = "0.2.18" +num-prime = "0.4.4" +num-traits = "0.2.19" number_prefix = "0.4" -once_cell = "1.19.0" -onig = { version = "~6.4", default-features = false } -parse_datetime = "0.5.0" -phf = "0.11.2" -phf_codegen = "0.11.2" -platform-info = "2.0.2" -quick-error = "2.0.1" -rand = { version = "0.8", features = ["small_rng"] } -rand_core = "0.6" -rayon = "1.9" -redox_syscall = "0.5" +onig = { version = "~6.5.1", default-features = false } +parse_datetime = "0.11.0" +phf = "0.12.1" +phf_codegen = "0.12.1" +platform-info = "2.0.3" +rand = { version = "0.9.0", features = ["small_rng"] } +rand_core = "0.9.0" +rayon = "1.10" regex = "1.10.4" -rstest = "0.18.2" -rust-ini = "0.19.0" +rstest = "0.26.0" +rust-ini = "0.21.0" same-file = "1.0.6" -self_cell = "1.0.3" -selinux = "0.4" +self_cell = "1.0.4" +# FIXME we use the exact version because the new 0.5.3 requires an MSRV of 1.88 +selinux = "=0.5.2" signal-hook = "0.3.17" -smallvec = { version = "1.13", features = ["union"] } -tempfile = "3.10.1" -uutils_term_grid = "0.3" -terminal_size = "0.3.0" +tempfile = "3.15.0" +terminal_size = "0.4.0" textwrap = { version = "0.16.1", features = ["terminal_size"] } -thiserror = "1.0" -time = { version = "0.3" } -unicode-segmentation = "1.11.0" -unicode-width = "0.1.11" -utf-8 = "0.7.6" +thiserror = "2.0.3" +time = { version = "0.3.36" } +unicode-width = "0.2.0" +utmp-classic = "0.1.6" +uutils_term_grid = "0.7" walkdir = "2.5" -winapi-util = "0.1.6" -windows-sys = { version = "0.48.0", default-features = false } +winapi-util = "0.1.8" +windows-sys = { version = "0.60.1", default-features = false } xattr = "1.3.1" -zip = { version = "0.6.6", default-features = false, features = ["deflate"] } +zip = { version = "4.0.0", default-features = false, features = ["deflate"] } hex = "0.4.3" md-5 = "0.10.6" @@ -342,130 +374,137 @@ sha3 = "0.10.8" blake2b_simd = "1.0.2" blake3 = "1.5.1" sm3 = "0.4.2" +crc32fast = "1.4.2" digest = "0.10.7" -uucore = { version = ">=0.0.19", package = "uucore", path = "src/uucore" } -uucore_procs = { version = ">=0.0.19", package = "uucore_procs", path = "src/uucore_procs" } -uu_ls = { version = ">=0.0.18", path = "src/uu/ls" } -uu_base32 = { version = ">=0.0.18", path = "src/uu/base32" } +# Fluent dependencies +fluent = "0.17.0" +fluent-bundle = "0.16.0" +unic-langid = "0.9.6" +fluent-syntax = "0.12.0" + +uucore = { version = "0.1.0", package = "uucore", path = "src/uucore" } +uucore_procs = { version = "0.1.0", package = "uucore_procs", path = "src/uucore_procs" } +uu_ls = { version = "0.1.0", path = "src/uu/ls" } +uu_base32 = { version = "0.1.0", path = "src/uu/base32" } +uutests = { version = "0.1.0", package = "uutests", path = "tests/uutests" } [dependencies] -clap = { workspace = true } -once_cell = { workspace = true } -uucore = { workspace = true } -clap_complete = { workspace = true } -clap_mangen = { workspace = true } -phf = { workspace = true } +clap.workspace = true +uucore.workspace = true +clap_complete.workspace = true +clap_mangen.workspace = true +phf.workspace = true selinux = { workspace = true, optional = true } -textwrap = { workspace = true } +textwrap.workspace = true zip = { workspace = true, optional = true } uuhelp_parser = { optional = true, version = ">=0.0.19", path = "src/uuhelp_parser" } # * uutils -uu_test = { optional = true, version = "0.0.25", package = "uu_test", path = "src/uu/test" } +uu_test = { optional = true, version = "0.1.0", package = "uu_test", path = "src/uu/test" } # -arch = { optional = true, version = "0.0.25", package = "uu_arch", path = "src/uu/arch" } -base32 = { optional = true, version = "0.0.25", package = "uu_base32", path = "src/uu/base32" } -base64 = { optional = true, version = "0.0.25", package = "uu_base64", path = "src/uu/base64" } -basename = { optional = true, version = "0.0.25", package = "uu_basename", path = "src/uu/basename" } -basenc = { optional = true, version = "0.0.25", package = "uu_basenc", path = "src/uu/basenc" } -cat = { optional = true, version = "0.0.25", package = "uu_cat", path = "src/uu/cat" } -chcon = { optional = true, version = "0.0.25", package = "uu_chcon", path = "src/uu/chcon" } -chgrp = { optional = true, version = "0.0.25", package = "uu_chgrp", path = "src/uu/chgrp" } -chmod = { optional = true, version = "0.0.25", package = "uu_chmod", path = "src/uu/chmod" } -chown = { optional = true, version = "0.0.25", package = "uu_chown", path = "src/uu/chown" } -chroot = { optional = true, version = "0.0.25", package = "uu_chroot", path = "src/uu/chroot" } -cksum = { optional = true, version = "0.0.25", package = "uu_cksum", path = "src/uu/cksum" } -comm = { optional = true, version = "0.0.25", package = "uu_comm", path = "src/uu/comm" } -cp = { optional = true, version = "0.0.25", package = "uu_cp", path = "src/uu/cp" } -csplit = { optional = true, version = "0.0.25", package = "uu_csplit", path = "src/uu/csplit" } -cut = { optional = true, version = "0.0.25", package = "uu_cut", path = "src/uu/cut" } -date = { optional = true, version = "0.0.25", package = "uu_date", path = "src/uu/date" } -dd = { optional = true, version = "0.0.25", package = "uu_dd", path = "src/uu/dd" } -df = { optional = true, version = "0.0.25", package = "uu_df", path = "src/uu/df" } -dir = { optional = true, version = "0.0.25", package = "uu_dir", path = "src/uu/dir" } -dircolors = { optional = true, version = "0.0.25", package = "uu_dircolors", path = "src/uu/dircolors" } -dirname = { optional = true, version = "0.0.25", package = "uu_dirname", path = "src/uu/dirname" } -du = { optional = true, version = "0.0.25", package = "uu_du", path = "src/uu/du" } -echo = { optional = true, version = "0.0.25", package = "uu_echo", path = "src/uu/echo" } -env = { optional = true, version = "0.0.25", package = "uu_env", path = "src/uu/env" } -expand = { optional = true, version = "0.0.25", package = "uu_expand", path = "src/uu/expand" } -expr = { optional = true, version = "0.0.25", package = "uu_expr", path = "src/uu/expr" } -factor = { optional = true, version = "0.0.25", package = "uu_factor", path = "src/uu/factor" } -false = { optional = true, version = "0.0.25", package = "uu_false", path = "src/uu/false" } -fmt = { optional = true, version = "0.0.25", package = "uu_fmt", path = "src/uu/fmt" } -fold = { optional = true, version = "0.0.25", package = "uu_fold", path = "src/uu/fold" } -groups = { optional = true, version = "0.0.25", package = "uu_groups", path = "src/uu/groups" } -hashsum = { optional = true, version = "0.0.25", package = "uu_hashsum", path = "src/uu/hashsum" } -head = { optional = true, version = "0.0.25", package = "uu_head", path = "src/uu/head" } -hostid = { optional = true, version = "0.0.25", package = "uu_hostid", path = "src/uu/hostid" } -hostname = { optional = true, version = "0.0.25", package = "uu_hostname", path = "src/uu/hostname" } -id = { optional = true, version = "0.0.25", package = "uu_id", path = "src/uu/id" } -install = { optional = true, version = "0.0.25", package = "uu_install", path = "src/uu/install" } -join = { optional = true, version = "0.0.25", package = "uu_join", path = "src/uu/join" } -kill = { optional = true, version = "0.0.25", package = "uu_kill", path = "src/uu/kill" } -link = { optional = true, version = "0.0.25", package = "uu_link", path = "src/uu/link" } -ln = { optional = true, version = "0.0.25", package = "uu_ln", path = "src/uu/ln" } -ls = { optional = true, version = "0.0.25", package = "uu_ls", path = "src/uu/ls" } -logname = { optional = true, version = "0.0.25", package = "uu_logname", path = "src/uu/logname" } -mkdir = { optional = true, version = "0.0.25", package = "uu_mkdir", path = "src/uu/mkdir" } -mkfifo = { optional = true, version = "0.0.25", package = "uu_mkfifo", path = "src/uu/mkfifo" } -mknod = { optional = true, version = "0.0.25", package = "uu_mknod", path = "src/uu/mknod" } -mktemp = { optional = true, version = "0.0.25", package = "uu_mktemp", path = "src/uu/mktemp" } -more = { optional = true, version = "0.0.25", package = "uu_more", path = "src/uu/more" } -mv = { optional = true, version = "0.0.25", package = "uu_mv", path = "src/uu/mv" } -nice = { optional = true, version = "0.0.25", package = "uu_nice", path = "src/uu/nice" } -nl = { optional = true, version = "0.0.25", package = "uu_nl", path = "src/uu/nl" } -nohup = { optional = true, version = "0.0.25", package = "uu_nohup", path = "src/uu/nohup" } -nproc = { optional = true, version = "0.0.25", package = "uu_nproc", path = "src/uu/nproc" } -numfmt = { optional = true, version = "0.0.25", package = "uu_numfmt", path = "src/uu/numfmt" } -od = { optional = true, version = "0.0.25", package = "uu_od", path = "src/uu/od" } -paste = { optional = true, version = "0.0.25", package = "uu_paste", path = "src/uu/paste" } -pathchk = { optional = true, version = "0.0.25", package = "uu_pathchk", path = "src/uu/pathchk" } -pinky = { optional = true, version = "0.0.25", package = "uu_pinky", path = "src/uu/pinky" } -pr = { optional = true, version = "0.0.25", package = "uu_pr", path = "src/uu/pr" } -printenv = { optional = true, version = "0.0.25", package = "uu_printenv", path = "src/uu/printenv" } -printf = { optional = true, version = "0.0.25", package = "uu_printf", path = "src/uu/printf" } -ptx = { optional = true, version = "0.0.25", package = "uu_ptx", path = "src/uu/ptx" } -pwd = { optional = true, version = "0.0.25", package = "uu_pwd", path = "src/uu/pwd" } -readlink = { optional = true, version = "0.0.25", package = "uu_readlink", path = "src/uu/readlink" } -realpath = { optional = true, version = "0.0.25", package = "uu_realpath", path = "src/uu/realpath" } -rm = { optional = true, version = "0.0.25", package = "uu_rm", path = "src/uu/rm" } -rmdir = { optional = true, version = "0.0.25", package = "uu_rmdir", path = "src/uu/rmdir" } -runcon = { optional = true, version = "0.0.25", package = "uu_runcon", path = "src/uu/runcon" } -seq = { optional = true, version = "0.0.25", package = "uu_seq", path = "src/uu/seq" } -shred = { optional = true, version = "0.0.25", package = "uu_shred", path = "src/uu/shred" } -shuf = { optional = true, version = "0.0.25", package = "uu_shuf", path = "src/uu/shuf" } -sleep = { optional = true, version = "0.0.25", package = "uu_sleep", path = "src/uu/sleep" } -sort = { optional = true, version = "0.0.25", package = "uu_sort", path = "src/uu/sort" } -split = { optional = true, version = "0.0.25", package = "uu_split", path = "src/uu/split" } -stat = { optional = true, version = "0.0.25", package = "uu_stat", path = "src/uu/stat" } -stdbuf = { optional = true, version = "0.0.25", package = "uu_stdbuf", path = "src/uu/stdbuf" } -stty = { optional = true, version = "0.0.25", package = "uu_stty", path = "src/uu/stty" } -sum = { optional = true, version = "0.0.25", package = "uu_sum", path = "src/uu/sum" } -sync = { optional = true, version = "0.0.25", package = "uu_sync", path = "src/uu/sync" } -tac = { optional = true, version = "0.0.25", package = "uu_tac", path = "src/uu/tac" } -tail = { optional = true, version = "0.0.25", package = "uu_tail", path = "src/uu/tail" } -tee = { optional = true, version = "0.0.25", package = "uu_tee", path = "src/uu/tee" } -timeout = { optional = true, version = "0.0.25", package = "uu_timeout", path = "src/uu/timeout" } -touch = { optional = true, version = "0.0.25", package = "uu_touch", path = "src/uu/touch" } -tr = { optional = true, version = "0.0.25", package = "uu_tr", path = "src/uu/tr" } -true = { optional = true, version = "0.0.25", package = "uu_true", path = "src/uu/true" } -truncate = { optional = true, version = "0.0.25", package = "uu_truncate", path = "src/uu/truncate" } -tsort = { optional = true, version = "0.0.25", package = "uu_tsort", path = "src/uu/tsort" } -tty = { optional = true, version = "0.0.25", package = "uu_tty", path = "src/uu/tty" } -uname = { optional = true, version = "0.0.25", package = "uu_uname", path = "src/uu/uname" } -unexpand = { optional = true, version = "0.0.25", package = "uu_unexpand", path = "src/uu/unexpand" } -uniq = { optional = true, version = "0.0.25", package = "uu_uniq", path = "src/uu/uniq" } -unlink = { optional = true, version = "0.0.25", package = "uu_unlink", path = "src/uu/unlink" } -uptime = { optional = true, version = "0.0.25", package = "uu_uptime", path = "src/uu/uptime" } -users = { optional = true, version = "0.0.25", package = "uu_users", path = "src/uu/users" } -vdir = { optional = true, version = "0.0.25", package = "uu_vdir", path = "src/uu/vdir" } -wc = { optional = true, version = "0.0.25", package = "uu_wc", path = "src/uu/wc" } -who = { optional = true, version = "0.0.25", package = "uu_who", path = "src/uu/who" } -whoami = { optional = true, version = "0.0.25", package = "uu_whoami", path = "src/uu/whoami" } -yes = { optional = true, version = "0.0.25", package = "uu_yes", path = "src/uu/yes" } +arch = { optional = true, version = "0.1.0", package = "uu_arch", path = "src/uu/arch" } +base32 = { optional = true, version = "0.1.0", package = "uu_base32", path = "src/uu/base32" } +base64 = { optional = true, version = "0.1.0", package = "uu_base64", path = "src/uu/base64" } +basename = { optional = true, version = "0.1.0", package = "uu_basename", path = "src/uu/basename" } +basenc = { optional = true, version = "0.1.0", package = "uu_basenc", path = "src/uu/basenc" } +cat = { optional = true, version = "0.1.0", package = "uu_cat", path = "src/uu/cat" } +chcon = { optional = true, version = "0.1.0", package = "uu_chcon", path = "src/uu/chcon" } +chgrp = { optional = true, version = "0.1.0", package = "uu_chgrp", path = "src/uu/chgrp" } +chmod = { optional = true, version = "0.1.0", package = "uu_chmod", path = "src/uu/chmod" } +chown = { optional = true, version = "0.1.0", package = "uu_chown", path = "src/uu/chown" } +chroot = { optional = true, version = "0.1.0", package = "uu_chroot", path = "src/uu/chroot" } +cksum = { optional = true, version = "0.1.0", package = "uu_cksum", path = "src/uu/cksum" } +comm = { optional = true, version = "0.1.0", package = "uu_comm", path = "src/uu/comm" } +cp = { optional = true, version = "0.1.0", package = "uu_cp", path = "src/uu/cp" } +csplit = { optional = true, version = "0.1.0", package = "uu_csplit", path = "src/uu/csplit" } +cut = { optional = true, version = "0.1.0", package = "uu_cut", path = "src/uu/cut" } +date = { optional = true, version = "0.1.0", package = "uu_date", path = "src/uu/date" } +dd = { optional = true, version = "0.1.0", package = "uu_dd", path = "src/uu/dd" } +df = { optional = true, version = "0.1.0", package = "uu_df", path = "src/uu/df" } +dir = { optional = true, version = "0.1.0", package = "uu_dir", path = "src/uu/dir" } +dircolors = { optional = true, version = "0.1.0", package = "uu_dircolors", path = "src/uu/dircolors" } +dirname = { optional = true, version = "0.1.0", package = "uu_dirname", path = "src/uu/dirname" } +du = { optional = true, version = "0.1.0", package = "uu_du", path = "src/uu/du" } +echo = { optional = true, version = "0.1.0", package = "uu_echo", path = "src/uu/echo" } +env = { optional = true, version = "0.1.0", package = "uu_env", path = "src/uu/env" } +expand = { optional = true, version = "0.1.0", package = "uu_expand", path = "src/uu/expand" } +expr = { optional = true, version = "0.1.0", package = "uu_expr", path = "src/uu/expr" } +factor = { optional = true, version = "0.1.0", package = "uu_factor", path = "src/uu/factor" } +false = { optional = true, version = "0.1.0", package = "uu_false", path = "src/uu/false" } +fmt = { optional = true, version = "0.1.0", package = "uu_fmt", path = "src/uu/fmt" } +fold = { optional = true, version = "0.1.0", package = "uu_fold", path = "src/uu/fold" } +groups = { optional = true, version = "0.1.0", package = "uu_groups", path = "src/uu/groups" } +hashsum = { optional = true, version = "0.1.0", package = "uu_hashsum", path = "src/uu/hashsum" } +head = { optional = true, version = "0.1.0", package = "uu_head", path = "src/uu/head" } +hostid = { optional = true, version = "0.1.0", package = "uu_hostid", path = "src/uu/hostid" } +hostname = { optional = true, version = "0.1.0", package = "uu_hostname", path = "src/uu/hostname" } +id = { optional = true, version = "0.1.0", package = "uu_id", path = "src/uu/id" } +install = { optional = true, version = "0.1.0", package = "uu_install", path = "src/uu/install" } +join = { optional = true, version = "0.1.0", package = "uu_join", path = "src/uu/join" } +kill = { optional = true, version = "0.1.0", package = "uu_kill", path = "src/uu/kill" } +link = { optional = true, version = "0.1.0", package = "uu_link", path = "src/uu/link" } +ln = { optional = true, version = "0.1.0", package = "uu_ln", path = "src/uu/ln" } +ls = { optional = true, version = "0.1.0", package = "uu_ls", path = "src/uu/ls" } +logname = { optional = true, version = "0.1.0", package = "uu_logname", path = "src/uu/logname" } +mkdir = { optional = true, version = "0.1.0", package = "uu_mkdir", path = "src/uu/mkdir" } +mkfifo = { optional = true, version = "0.1.0", package = "uu_mkfifo", path = "src/uu/mkfifo" } +mknod = { optional = true, version = "0.1.0", package = "uu_mknod", path = "src/uu/mknod" } +mktemp = { optional = true, version = "0.1.0", package = "uu_mktemp", path = "src/uu/mktemp" } +more = { optional = true, version = "0.1.0", package = "uu_more", path = "src/uu/more" } +mv = { optional = true, version = "0.1.0", package = "uu_mv", path = "src/uu/mv" } +nice = { optional = true, version = "0.1.0", package = "uu_nice", path = "src/uu/nice" } +nl = { optional = true, version = "0.1.0", package = "uu_nl", path = "src/uu/nl" } +nohup = { optional = true, version = "0.1.0", package = "uu_nohup", path = "src/uu/nohup" } +nproc = { optional = true, version = "0.1.0", package = "uu_nproc", path = "src/uu/nproc" } +numfmt = { optional = true, version = "0.1.0", package = "uu_numfmt", path = "src/uu/numfmt" } +od = { optional = true, version = "0.1.0", package = "uu_od", path = "src/uu/od" } +paste = { optional = true, version = "0.1.0", package = "uu_paste", path = "src/uu/paste" } +pathchk = { optional = true, version = "0.1.0", package = "uu_pathchk", path = "src/uu/pathchk" } +pinky = { optional = true, version = "0.1.0", package = "uu_pinky", path = "src/uu/pinky" } +pr = { optional = true, version = "0.1.0", package = "uu_pr", path = "src/uu/pr" } +printenv = { optional = true, version = "0.1.0", package = "uu_printenv", path = "src/uu/printenv" } +printf = { optional = true, version = "0.1.0", package = "uu_printf", path = "src/uu/printf" } +ptx = { optional = true, version = "0.1.0", package = "uu_ptx", path = "src/uu/ptx" } +pwd = { optional = true, version = "0.1.0", package = "uu_pwd", path = "src/uu/pwd" } +readlink = { optional = true, version = "0.1.0", package = "uu_readlink", path = "src/uu/readlink" } +realpath = { optional = true, version = "0.1.0", package = "uu_realpath", path = "src/uu/realpath" } +rm = { optional = true, version = "0.1.0", package = "uu_rm", path = "src/uu/rm" } +rmdir = { optional = true, version = "0.1.0", package = "uu_rmdir", path = "src/uu/rmdir" } +runcon = { optional = true, version = "0.1.0", package = "uu_runcon", path = "src/uu/runcon" } +seq = { optional = true, version = "0.1.0", package = "uu_seq", path = "src/uu/seq" } +shred = { optional = true, version = "0.1.0", package = "uu_shred", path = "src/uu/shred" } +shuf = { optional = true, version = "0.1.0", package = "uu_shuf", path = "src/uu/shuf" } +sleep = { optional = true, version = "0.1.0", package = "uu_sleep", path = "src/uu/sleep" } +sort = { optional = true, version = "0.1.0", package = "uu_sort", path = "src/uu/sort" } +split = { optional = true, version = "0.1.0", package = "uu_split", path = "src/uu/split" } +stat = { optional = true, version = "0.1.0", package = "uu_stat", path = "src/uu/stat" } +stdbuf = { optional = true, version = "0.1.0", package = "uu_stdbuf", path = "src/uu/stdbuf" } +stty = { optional = true, version = "0.1.0", package = "uu_stty", path = "src/uu/stty" } +sum = { optional = true, version = "0.1.0", package = "uu_sum", path = "src/uu/sum" } +sync = { optional = true, version = "0.1.0", package = "uu_sync", path = "src/uu/sync" } +tac = { optional = true, version = "0.1.0", package = "uu_tac", path = "src/uu/tac" } +tail = { optional = true, version = "0.1.0", package = "uu_tail", path = "src/uu/tail" } +tee = { optional = true, version = "0.1.0", package = "uu_tee", path = "src/uu/tee" } +timeout = { optional = true, version = "0.1.0", package = "uu_timeout", path = "src/uu/timeout" } +touch = { optional = true, version = "0.1.0", package = "uu_touch", path = "src/uu/touch" } +tr = { optional = true, version = "0.1.0", package = "uu_tr", path = "src/uu/tr" } +true = { optional = true, version = "0.1.0", package = "uu_true", path = "src/uu/true" } +truncate = { optional = true, version = "0.1.0", package = "uu_truncate", path = "src/uu/truncate" } +tsort = { optional = true, version = "0.1.0", package = "uu_tsort", path = "src/uu/tsort" } +tty = { optional = true, version = "0.1.0", package = "uu_tty", path = "src/uu/tty" } +uname = { optional = true, version = "0.1.0", package = "uu_uname", path = "src/uu/uname" } +unexpand = { optional = true, version = "0.1.0", package = "uu_unexpand", path = "src/uu/unexpand" } +uniq = { optional = true, version = "0.1.0", package = "uu_uniq", path = "src/uu/uniq" } +unlink = { optional = true, version = "0.1.0", package = "uu_unlink", path = "src/uu/unlink" } +uptime = { optional = true, version = "0.1.0", package = "uu_uptime", path = "src/uu/uptime" } +users = { optional = true, version = "0.1.0", package = "uu_users", path = "src/uu/users" } +vdir = { optional = true, version = "0.1.0", package = "uu_vdir", path = "src/uu/vdir" } +wc = { optional = true, version = "0.1.0", package = "uu_wc", path = "src/uu/wc" } +who = { optional = true, version = "0.1.0", package = "uu_who", path = "src/uu/who" } +whoami = { optional = true, version = "0.1.0", package = "uu_whoami", path = "src/uu/whoami" } +yes = { optional = true, version = "0.1.0", package = "uu_yes", path = "src/uu/yes" } # this breaks clippy linting with: "tests/by-util/test_factor_benches.rs: No such file or directory (os error 2)" # factor_benches = { optional = true, version = "0.0.0", package = "uu_factor_benches", path = "tests/benches/factor" } @@ -476,34 +515,52 @@ yes = { optional = true, version = "0.0.25", package = "uu_yes", path = "src/uu/ #pin_cc = { version="1.0.61, < 1.0.62", package="cc" } ## cc v1.0.62 has compiler errors for MinRustV v1.32.0, requires 1.34 (for `std::str::split_ascii_whitespace()`) [dev-dependencies] -chrono = { workspace = true } -conv = "0.3" -filetime = { workspace = true } -glob = { workspace = true } -libc = { workspace = true } -pretty_assertions = "1" -rand = { workspace = true } -regex = { workspace = true } -sha1 = { version = "0.10", features = ["std"] } -tempfile = { workspace = true } +chrono.workspace = true +ctor.workspace = true +filetime.workspace = true +glob.workspace = true +libc.workspace = true +num-prime.workspace = true +pretty_assertions = "1.4.0" +rand.workspace = true +regex.workspace = true +sha1 = { workspace = true, features = ["std"] } +tempfile.workspace = true time = { workspace = true, features = ["local-offset"] } -unindent = "0.2" -uucore = { workspace = true, features = ["entries", "process", "signals"] } -walkdir = { workspace = true } -hex-literal = "0.4.1" -rstest = { workspace = true } - -[target.'cfg(any(target_os = "linux", target_os = "android"))'.dev-dependencies] -procfs = { version = "0.16", default-features = false } +unindent = "0.2.3" +uutests.workspace = true +uucore = { workspace = true, features = [ + "mode", + "entries", + "process", + "signals", + "utmpx", +] } +walkdir.workspace = true +hex-literal = "1.0.0" +rstest.workspace = true [target.'cfg(unix)'.dev-dependencies] -nix = { workspace = true, features = ["process", "signal", "user", "term"] } +nix = { workspace = true, features = [ + "process", + "signal", + "socket", + "user", + "term", +] } rlimit = "0.10.1" -rand_pcg = "0.3" -xattr = { workspace = true } +xattr.workspace = true + +# Used in test_uptime::test_uptime_with_file_containing_valid_boot_time_utmpx_record +# to deserialize an utmpx struct into a binary file +[target.'cfg(all(target_family= "unix",not(target_os = "macos")))'.dev-dependencies] +serde = { version = "1.0.202", features = ["derive"] } +bincode = { version = "2.0.1", features = ["serde"] } +serde-big-array = "0.5.1" + [build-dependencies] -phf_codegen = { workspace = true } +phf_codegen.workspace = true [[bin]] name = "coreutils" @@ -514,9 +571,9 @@ name = "uudoc" path = "src/bin/uudoc.rs" required-features = ["uudoc"] -# The default release profile. It contains all optimizations, without -# sacrificing debug info. With this profile (like in the standard -# release profile), the debug info and the stack traces will still be available. +# The default release profile. It contains all optimizations. +# With this profile (like in the standard release profile), +# the stack traces will still be available. [profile.release] lto = true @@ -532,3 +589,78 @@ inherits = "release" opt-level = "z" panic = "abort" strip = true + +# A release-like profile with debug info, useful for profiling. +# See https://github.com/mstange/samply . +[profile.profiling] +inherits = "release" +debug = true + +[lints.clippy] +multiple_crate_versions = "allow" +cargo_common_metadata = "allow" +missing_panics_doc = "allow" +# TODO remove when https://github.com/rust-lang/rust-clippy/issues/13774 is fixed +large_stack_arrays = "allow" + +use_self = "warn" +needless_pass_by_value = "warn" +semicolon_if_nothing_returned = "warn" +single_char_pattern = "warn" +explicit_iter_loop = "warn" +if_not_else = "warn" +manual_let_else = "warn" + +all = { level = "deny", priority = -1 } +cargo = { level = "warn", priority = -1 } +pedantic = { level = "deny", priority = -1 } + +# This is the linting configuration for all crates. +# Eventually the clippy settings from the `[lints]` section should be moved here. +# In order to use these, all crates have `[lints] workspace = true` section. +[workspace.lints.rust] +unused_qualifications = "warn" + +[workspace.lints.clippy] +# The counts were generated with this command: +# cargo clippy --all-targets --workspace --message-format=json --quiet \ +# | jq -r '.message.code.code | select(. != null and startswith("clippy::"))' \ +# | sort | uniq -c | sort -h -r +# +# TODO: +# remove large_stack_arrays when https://github.com/rust-lang/rust-clippy/issues/13774 is fixed +# +all = { level = "warn", priority = -1 } +cargo = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +cargo_common_metadata = "allow" # 3240 +multiple_crate_versions = "allow" # 2882 +missing_errors_doc = "allow" # 1572 +missing_panics_doc = "allow" # 946 +must_use_candidate = "allow" # 322 +match_same_arms = "allow" # 204 +redundant_closure_for_method_calls = "allow" # 125 +cast_possible_truncation = "allow" # 122 +too_many_lines = "allow" # 101 +trivially_copy_pass_by_ref = "allow" # 84 +single_match_else = "allow" # 82 +cast_possible_wrap = "allow" # 78 +cast_sign_loss = "allow" # 70 +struct_excessive_bools = "allow" # 68 +cast_precision_loss = "allow" # 52 +cast_lossless = "allow" # 35 +unnecessary_wraps = "allow" # 33 +ignored_unit_patterns = "allow" # 21 +similar_names = "allow" # 20 +large_stack_arrays = "allow" # 20 +wildcard_imports = "allow" # 18 +used_underscore_binding = "allow" # 18 +needless_pass_by_value = "allow" # 16 +float_cmp = "allow" # 12 +items_after_statements = "allow" # 11 +return_self_not_must_use = "allow" # 8 +needless_continue = "allow" # 6 +inline_always = "allow" # 6 +fn_params_excessive_bools = "allow" # 6 +used_underscore_items = "allow" # 2 +should_panic_without_expect = "allow" # 2 diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 00000000000..52f5bad21dd --- /dev/null +++ b/Cross.toml @@ -0,0 +1,7 @@ +# spell-checker:ignore (misc) dpkg noninteractive tzdata +[build] +pre-build = [ + "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install tzdata", +] +[build.env] +passthrough = ["CI", "RUST_BACKTRACE", "CARGO_TERM_COLOR"] diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 6f1de3b5476..6091c394fc4 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,4 +1,4 @@ - + # Setting up your local development environment @@ -241,6 +241,8 @@ DEBUG=1 bash util/run-gnu-test.sh tests/misc/sm3sum.pl Note that GNU test suite relies on individual utilities (not the multicall binary). +You also need to install [quilt](https://savannah.nongnu.org/projects/quilt), a tool used to manage a stack of patches for modifying GNU tests. + On FreeBSD, you need to install packages for GNU coreutils and sed (used in shell scripts instead of system commands): ```shell @@ -251,13 +253,11 @@ pkg install coreutils gsed Code coverage report can be generated using [grcov](https://github.com/mozilla/grcov). -### Using Nightly Rust - To generate [gcov-based](https://github.com/mozilla/grcov#example-how-to-generate-gcda-files-for-a-rust-project) coverage report ```shell export CARGO_INCREMENTAL=0 -export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" +export RUSTFLAGS="-Cinstrument-coverage -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" export RUSTDOCFLAGS="-Cpanic=abort" cargo build # e.g., --features feat_os_unix cargo test # e.g., --features feat_os_unix test_pathchk @@ -267,11 +267,6 @@ grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existin if changes are not reflected in the report then run `cargo clean` and run the above commands. -### Using Stable Rust - -If you are using stable version of Rust that doesn't enable code coverage instrumentation by default -then add `-Z-Zinstrument-coverage` flag to `RUSTFLAGS` env variable specified above. - ## Tips for setting up on Mac ### C Compiler and linker @@ -335,3 +330,13 @@ Otherwise please follow [this guide](https://learn.microsoft.com/en-us/windows/d If you have used [Git for Windows](https://gitforwindows.org) to install `git` on you Windows system you might already have some GNU core utilities installed as part of "GNU Bash" included in Git for Windows package, but it is not a complete package. [This article](https://gist.github.com/evanwill/0207876c3243bbb6863e65ec5dc3f058) provides instruction on how to add more to it. Alternatively you can install [Cygwin](https://www.cygwin.com) and/or use [WSL2](https://learn.microsoft.com/en-us/windows/wsl/compare-versions#whats-new-in-wsl-2) to get access to all GNU core utilities on Windows. + +## Preparing a new release + +1. Modify `util/update-version.sh` (FROM & TO) and run it +1. Submit a new PR with these changes and wait for it to be merged +1. Tag the new release `git tag -a X.Y.Z` and `git push --tags` +1. Once the CI is green, a new release will be automatically created in draft mode. + Reuse this release and make sure that assets have been added. +1. Write the release notes (it takes time) following previous examples +1. Run `util/publish.sh --do-it` to publish the new release to crates.io diff --git a/GNUmakefile b/GNUmakefile index ad2d38081f3..20dc731d3b0 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -3,11 +3,20 @@ # Config options PROFILE ?= debug MULTICALL ?= n +COMPLETIONS ?= y +MANPAGES ?= y +LOCALES ?= y INSTALL ?= install ifneq (,$(filter install, $(MAKECMDGOALS))) override PROFILE:=release endif +# Needed for the foreach loops to split each loop into a separate command +define newline + + +endef + PROFILE_CMD := ifeq ($(PROFILE),release) PROFILE_CMD = --release @@ -24,6 +33,9 @@ PREFIX ?= /usr/local DESTDIR ?= BINDIR ?= $(PREFIX)/bin DATAROOTDIR ?= $(PREFIX)/share +LIBSTDBUF_DIR ?= $(PREFIX)/libexec/coreutils +# Export variable so that it is used during the build +export LIBSTDBUF_DIR INSTALLDIR_BIN=$(DESTDIR)$(BINDIR) @@ -33,23 +45,39 @@ PROG_PREFIX ?= # This won't support any directory with spaces in its name, but you can just # make a symlink without spaces that points to the directory. BASEDIR ?= $(shell pwd) +ifdef CARGO_TARGET_DIR +BUILDDIR := $(CARGO_TARGET_DIR)/${PROFILE} +else BUILDDIR := $(BASEDIR)/target/${PROFILE} +endif PKG_BUILDDIR := $(BUILDDIR)/deps DOCSDIR := $(BASEDIR)/docs BUSYBOX_ROOT := $(BASEDIR)/tmp -BUSYBOX_VER := 1.35.0 +BUSYBOX_VER := 1.36.1 BUSYBOX_SRC := $(BUSYBOX_ROOT)/busybox-$(BUSYBOX_VER) TOYBOX_ROOT := $(BASEDIR)/tmp -TOYBOX_VER := 0.8.8 +TOYBOX_VER := 0.8.12 TOYBOX_SRC := $(TOYBOX_ROOT)/toybox-$(TOYBOX_VER) -ifeq ($(SELINUX_ENABLED),) - SELINUX_ENABLED := 0 +#------------------------------------------------------------------------ +# Detect the host system. +# On Windows the environment already sets OS = Windows_NT. +# Otherwise let it default to the kernel name returned by uname -s +# (Linux, Darwin, FreeBSD, …). +#------------------------------------------------------------------------ +OS ?= $(shell uname -s) + +ifdef SELINUX_ENABLED + override SELINUX_ENABLED := 0 +# Now check if we should enable it (only on non-Windows) ifneq ($(OS),Windows_NT) - ifeq ($(shell /sbin/selinuxenabled 2>/dev/null ; echo $$?),0) - SELINUX_ENABLED := 1 + ifeq ($(shell if [ -x /sbin/selinuxenabled ] && /sbin/selinuxenabled 2>/dev/null; then echo 0; else echo 1; fi),0) + override SELINUX_ENABLED := 1 +$(info /sbin/selinuxenabled successful) + else +$(info SELINUX_ENABLED=1 but /sbin/selinuxenabled failed) endif endif endif @@ -110,6 +138,7 @@ PROGS := \ sleep \ sort \ split \ + stty \ sum \ sync \ tac \ @@ -162,12 +191,19 @@ SELINUX_PROGS := \ chcon \ runcon -ifneq ($(OS),Windows_NT) - PROGS := $(PROGS) $(UNIX_PROGS) +$(info Detected OS = $(OS)) + +# Don't build the SELinux programs on macOS (Darwin) and FreeBSD +ifeq ($(filter $(OS),Darwin FreeBSD),$(OS)) + SELINUX_PROGS := endif -ifeq ($(SELINUX_ENABLED),1) +ifneq ($(OS),Windows_NT) + PROGS := $(PROGS) $(UNIX_PROGS) +# Build the selinux command even if not on the system PROGS := $(PROGS) $(SELINUX_PROGS) +# Always use external libstdbuf when building with make (Unix only) + CARGOFLAGS += --features feat_external_libstdbuf endif UTILS ?= $(PROGS) @@ -221,6 +257,7 @@ TEST_PROGS := \ rmdir \ runcon \ seq \ + sleep \ sort \ split \ stat \ @@ -251,7 +288,8 @@ TEST_NO_FAIL_FAST :=--no-fail-fast TEST_SPEC_FEATURE := test_unimplemented else ifeq ($(SELINUX_ENABLED),1) TEST_NO_FAIL_FAST := -TEST_SPEC_FEATURE := feat_selinux +TEST_SPEC_FEATURE := selinux +BUILD_SPEC_FEATURE := selinux endif define TEST_BUSYBOX @@ -275,13 +313,17 @@ use_default := 1 build-pkgs: ifneq (${MULTICALL}, y) +ifdef BUILD_SPEC_FEATURE + ${CARGO} build ${CARGOFLAGS} --features "$(BUILD_SPEC_FEATURE)" ${PROFILE_CMD} $(foreach pkg,$(EXES),-p uu_$(pkg)) +else ${CARGO} build ${CARGOFLAGS} ${PROFILE_CMD} $(foreach pkg,$(EXES),-p uu_$(pkg)) endif +endif build-coreutils: - ${CARGO} build ${CARGOFLAGS} --features "${EXES}" ${PROFILE_CMD} --no-default-features + ${CARGO} build ${CARGOFLAGS} --features "${EXES} $(BUILD_SPEC_FEATURE)" ${PROFILE_CMD} --no-default-features -build: build-coreutils build-pkgs +build: build-coreutils build-pkgs locales $(foreach test,$(filter-out $(SKIP_UTILS),$(PROGS)),$(eval $(call TEST_BUSYBOX,$(test)))) @@ -336,47 +378,105 @@ clean: distclean: clean $(CARGO) clean $(CARGOFLAGS) && $(CARGO) update $(CARGOFLAGS) +ifeq ($(MANPAGES),y) manpages: build-coreutils mkdir -p $(BUILDDIR)/man/ $(foreach prog, $(INSTALLEES), \ - $(BUILDDIR)/coreutils manpage $(prog) > $(BUILDDIR)/man/$(PROG_PREFIX)$(prog).1; \ + $(BUILDDIR)/coreutils manpage $(prog) > $(BUILDDIR)/man/$(PROG_PREFIX)$(prog).1 $(newline) \ ) +install-manpages: manpages + mkdir -p $(DESTDIR)$(DATAROOTDIR)/man/man1 + $(foreach prog, $(INSTALLEES), \ + $(INSTALL) $(BUILDDIR)/man/$(PROG_PREFIX)$(prog).1 $(DESTDIR)$(DATAROOTDIR)/man/man1/ $(newline) \ + ) +else +install-manpages: +endif + +ifeq ($(COMPLETIONS),y) completions: build-coreutils mkdir -p $(BUILDDIR)/completions/zsh $(BUILDDIR)/completions/bash $(BUILDDIR)/completions/fish $(foreach prog, $(INSTALLEES), \ - $(BUILDDIR)/coreutils completion $(prog) zsh > $(BUILDDIR)/completions/zsh/_$(PROG_PREFIX)$(prog); \ - $(BUILDDIR)/coreutils completion $(prog) bash > $(BUILDDIR)/completions/bash/$(PROG_PREFIX)$(prog); \ - $(BUILDDIR)/coreutils completion $(prog) fish > $(BUILDDIR)/completions/fish/$(PROG_PREFIX)$(prog).fish; \ + $(BUILDDIR)/coreutils completion $(prog) zsh > $(BUILDDIR)/completions/zsh/_$(PROG_PREFIX)$(prog) $(newline) \ + $(BUILDDIR)/coreutils completion $(prog) bash > $(BUILDDIR)/completions/bash/$(PROG_PREFIX)$(prog) $(newline) \ + $(BUILDDIR)/coreutils completion $(prog) fish > $(BUILDDIR)/completions/fish/$(PROG_PREFIX)$(prog).fish $(newline) \ + ) + +install-completions: completions + mkdir -p $(DESTDIR)$(DATAROOTDIR)/zsh/site-functions + mkdir -p $(DESTDIR)$(DATAROOTDIR)/bash-completion/completions + mkdir -p $(DESTDIR)$(DATAROOTDIR)/fish/vendor_completions.d + $(foreach prog, $(INSTALLEES), \ + $(INSTALL) $(BUILDDIR)/completions/zsh/_$(PROG_PREFIX)$(prog) $(DESTDIR)$(DATAROOTDIR)/zsh/site-functions/ $(newline) \ + $(INSTALL) $(BUILDDIR)/completions/bash/$(PROG_PREFIX)$(prog) $(DESTDIR)$(DATAROOTDIR)/bash-completion/completions/ $(newline) \ + $(INSTALL) $(BUILDDIR)/completions/fish/$(PROG_PREFIX)$(prog).fish $(DESTDIR)$(DATAROOTDIR)/fish/vendor_completions.d/ $(newline) \ ) +else +install-completions: +endif + +ifeq ($(LOCALES),y) +locales: + @# Copy uucore common locales + @if [ -d "$(BASEDIR)/src/uucore/locales" ]; then \ + mkdir -p "$(BUILDDIR)/locales/uucore"; \ + for locale_file in "$(BASEDIR)"/src/uucore/locales/*.ftl; do \ + $(INSTALL) -v "$$locale_file" "$(BUILDDIR)/locales/uucore/"; \ + done; \ + fi; \ + # Copy utility-specific locales + @for prog in $(INSTALLEES); do \ + if [ -d "$(BASEDIR)/src/uu/$$prog/locales" ]; then \ + mkdir -p "$(BUILDDIR)/locales/$$prog"; \ + for locale_file in "$(BASEDIR)"/src/uu/$$prog/locales/*.ftl; do \ + if [ "$$(basename "$$locale_file")" != "en-US.ftl" ]; then \ + $(INSTALL) -v "$$locale_file" "$(BUILDDIR)/locales/$$prog/"; \ + fi; \ + done; \ + fi; \ + done + + +install-locales: + @for prog in $(INSTALLEES); do \ + if [ -d "$(BASEDIR)/src/uu/$$prog/locales" ]; then \ + mkdir -p "$(DESTDIR)$(DATAROOTDIR)/locales/$$prog"; \ + for locale_file in "$(BASEDIR)"/src/uu/$$prog/locales/*.ftl; do \ + if [ "$$(basename "$$locale_file")" != "en-US.ftl" ]; then \ + $(INSTALL) -v "$$locale_file" "$(DESTDIR)$(DATAROOTDIR)/locales/$$prog/"; \ + fi; \ + done; \ + fi; \ + done +else +install-locales: +endif -install: build manpages completions +install: build install-manpages install-completions install-locales mkdir -p $(INSTALLDIR_BIN) +ifneq ($(OS),Windows_NT) + mkdir -p $(DESTDIR)$(LIBSTDBUF_DIR) + $(INSTALL) -m 755 $(BUILDDIR)/deps/libstdbuf* $(DESTDIR)$(LIBSTDBUF_DIR)/ +endif ifeq (${MULTICALL}, y) $(INSTALL) $(BUILDDIR)/coreutils $(INSTALLDIR_BIN)/$(PROG_PREFIX)coreutils - cd $(INSTALLDIR_BIN) && $(foreach prog, $(filter-out coreutils, $(INSTALLEES)), \ - ln -fs $(PROG_PREFIX)coreutils $(PROG_PREFIX)$(prog) &&) : + $(foreach prog, $(filter-out coreutils, $(INSTALLEES)), \ + cd $(INSTALLDIR_BIN) && ln -fs $(PROG_PREFIX)coreutils $(PROG_PREFIX)$(prog) $(newline) \ + ) $(if $(findstring test,$(INSTALLEES)), cd $(INSTALLDIR_BIN) && ln -fs $(PROG_PREFIX)coreutils $(PROG_PREFIX)[) else $(foreach prog, $(INSTALLEES), \ - $(INSTALL) $(BUILDDIR)/$(prog) $(INSTALLDIR_BIN)/$(PROG_PREFIX)$(prog);) + $(INSTALL) $(BUILDDIR)/$(prog) $(INSTALLDIR_BIN)/$(PROG_PREFIX)$(prog) $(newline) \ + ) $(if $(findstring test,$(INSTALLEES)), $(INSTALL) $(BUILDDIR)/test $(INSTALLDIR_BIN)/$(PROG_PREFIX)[) endif - mkdir -p $(DESTDIR)$(DATAROOTDIR)/man/man1 - $(foreach prog, $(INSTALLEES), \ - $(INSTALL) $(BUILDDIR)/man/$(PROG_PREFIX)$(prog).1 $(DESTDIR)$(DATAROOTDIR)/man/man1/; \ - ) - - mkdir -p $(DESTDIR)$(DATAROOTDIR)/zsh/site-functions - mkdir -p $(DESTDIR)$(DATAROOTDIR)/bash-completion/completions - mkdir -p $(DESTDIR)$(DATAROOTDIR)/fish/vendor_completions.d - $(foreach prog, $(INSTALLEES), \ - $(INSTALL) $(BUILDDIR)/completions/zsh/_$(PROG_PREFIX)$(prog) $(DESTDIR)$(DATAROOTDIR)/zsh/site-functions/; \ - $(INSTALL) $(BUILDDIR)/completions/bash/$(PROG_PREFIX)$(prog) $(DESTDIR)$(DATAROOTDIR)/bash-completion/completions/; \ - $(INSTALL) $(BUILDDIR)/completions/fish/$(PROG_PREFIX)$(prog).fish $(DESTDIR)$(DATAROOTDIR)/fish/vendor_completions.d/; \ - ) uninstall: +ifneq ($(OS),Windows_NT) + rm -f $(DESTDIR)$(LIBSTDBUF_DIR)/libstdbuf* + -rmdir $(DESTDIR)$(LIBSTDBUF_DIR) 2>/dev/null || true +endif ifeq (${MULTICALL}, y) rm -f $(addprefix $(INSTALLDIR_BIN)/,$(PROG_PREFIX)coreutils) endif diff --git a/README.md b/README.md index dbcd7d86021..1d9a7ddd190 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ [![dependency status](https://deps.rs/repo/github/uutils/coreutils/status.svg)](https://deps.rs/repo/github/uutils/coreutils) [![CodeCov](https://codecov.io/gh/uutils/coreutils/branch/master/graph/badge.svg)](https://codecov.io/gh/uutils/coreutils) -![MSRV](https://img.shields.io/badge/MSRV-1.70.0-brightgreen) +![MSRV](https://img.shields.io/badge/MSRV-1.85.0-brightgreen) @@ -45,7 +45,7 @@ uutils aims to be a drop-in replacement for the GNU utils. Differences with GNU are treated as bugs. uutils aims to work on as many platforms as possible, to be able to use the same -utils on Linux, Mac, Windows and other platforms. This ensures, for example, +utils on Linux, macOS, Windows and other platforms. This ensures, for example, that scripts can be easily transferred between platforms.
@@ -53,7 +53,7 @@ that scripts can be easily transferred between platforms. ## Documentation uutils has both user and developer documentation available: -- [User Manual](https://uutils.github.io/coreutils/book/) +- [User Manual](https://uutils.github.io/coreutils/docs/) - [Developer Documentation](https://docs.rs/crate/coreutils/) Both can also be generated locally, the instructions for that can be found in @@ -70,7 +70,7 @@ the [coreutils docs](https://github.com/uutils/uutils.github.io) repository. ### Rust Version uutils follows Rust's release channels and is tested against stable, beta and -nightly. The current Minimum Supported Rust Version (MSRV) is `1.70.0`. +nightly. The current Minimum Supported Rust Version (MSRV) is `1.85.0`. ## Building @@ -78,7 +78,7 @@ There are currently two methods to build the uutils binaries: either Cargo or GNU Make. > Building the full package, including all documentation, requires both Cargo -> and Gnu Make on a Unix platform. +> and GNU Make on a Unix platform. For either method, we first need to fetch the repository: @@ -223,6 +223,12 @@ Installing with `make` installs shell completions for all installed utilities for `bash`, `fish` and `zsh`. Completions for `elvish` and `powershell` can also be generated; See `Manually install shell completions`. +To skip installation of completions and manpages: + +```shell +make COMPLETIONS=n MANPAGES=n install +``` + ### Manually install shell completions The `coreutils` binary can generate completions for the `bash`, `elvish`, @@ -266,7 +272,7 @@ Make to uninstall. To uninstall uutils: ```shell -cargo uninstall uutils +cargo uninstall coreutils ``` ### Uninstall with GNU Make @@ -302,12 +308,12 @@ make PREFIX=/my/path uninstall Below is the evolution of how many GNU tests uutils passes. A more detailed breakdown of the GNU test results of the main branch can be found -[in the user manual](https://uutils.github.io/coreutils/book/test_coverage.html). +[in the user manual](https://uutils.github.io/coreutils/docs/test_coverage.html). See for the main meta bugs (many are missing). -![Evolution over time](https://github.com/uutils/coreutils-tracking/blob/main/gnu-results.png?raw=true) +![Evolution over time](https://github.com/uutils/coreutils-tracking/blob/main/gnu-results.svg?raw=true)
diff --git a/README.package.md b/README.package.md new file mode 100644 index 00000000000..355b153db28 --- /dev/null +++ b/README.package.md @@ -0,0 +1,31 @@ + + + +
+
+ +![uutils logo](docs/src/logo.svg) + +# uutils coreutils + +[![Crates.io](https://img.shields.io/crates/v/coreutils.svg)](https://crates.io/crates/coreutils) +[![Discord](https://img.shields.io/badge/discord-join-7289DA.svg?logo=discord&longCache=true&style=flat)](https://discord.gg/wQVJbvJ) +[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/coreutils/blob/main/LICENSE) +[![dependency status](https://deps.rs/repo/github/uutils/coreutils/status.svg)](https://deps.rs/repo/github/uutils/coreutils) + +[![CodeCov](https://codecov.io/gh/uutils/coreutils/branch/master/graph/badge.svg)](https://codecov.io/gh/uutils/coreutils) +![MSRV](https://img.shields.io/badge/MSRV-1.70.0-brightgreen) + +
+ +--- + +
+ +This package is part of uutils coreutils. + +uutils coreutils is a cross-platform reimplementation of the GNU coreutils in +[Rust](http://www.rust-lang.org). + +This package does not have its specific `README.md`. + diff --git a/build.rs b/build.rs index bb4e2b53609..1a7bba8dc0a 100644 --- a/build.rs +++ b/build.rs @@ -11,14 +11,18 @@ use std::io::Write; use std::path::Path; pub fn main() { - if let Ok(profile) = env::var("PROFILE") { - println!("cargo:rustc-cfg=build={profile:?}"); - } - const ENV_FEATURE_PREFIX: &str = "CARGO_FEATURE_"; const FEATURE_PREFIX: &str = "feat_"; const OVERRIDE_PREFIX: &str = "uu_"; + // Do not rebuild build script unless the script itself or the enabled features are modified + // See + println!("cargo:rerun-if-changed=build.rs"); + + if let Ok(profile) = env::var("PROFILE") { + println!("cargo:rustc-cfg=build={profile:?}"); + } + let out_dir = env::var("OUT_DIR").unwrap(); let mut crates = Vec::new(); @@ -29,8 +33,10 @@ pub fn main() { #[allow(clippy::match_same_arms)] match krate.as_ref() { "default" | "macos" | "unix" | "windows" | "selinux" | "zip" => continue, // common/standard feature names - "nightly" | "test_unimplemented" => continue, // crate-local custom features - "uudoc" => continue, // is not a utility + "nightly" | "test_unimplemented" | "expensive_tests" | "test_risky_names" => { + continue; + } // crate-local custom features + "uudoc" => continue, // is not a utility "test" => continue, // over-ridden with 'uu_test' to avoid collision with rust core crate 'test' s if s.starts_with(FEATURE_PREFIX) => continue, // crate feature sets _ => {} // util feature name @@ -46,6 +52,7 @@ pub fn main() { "type UtilityMap = phf::OrderedMap<&'static str, (fn(T) -> i32, fn() -> Command)>;\n\ \n\ #[allow(clippy::too_many_lines)] + #[allow(clippy::unreadable_literal)] fn util_map() -> UtilityMap {\n" .as_bytes(), ) @@ -58,39 +65,39 @@ pub fn main() { // 'test' is named uu_test to avoid collision with rust core crate 'test'. // It can also be invoked by name '[' for the '[ expr ] syntax'. "uu_test" => { - phf_map.entry("test", &map_value); - phf_map.entry("[", &map_value); + phf_map.entry("test", map_value.clone()); + phf_map.entry("[", map_value.clone()); } k if k.starts_with(OVERRIDE_PREFIX) => { - phf_map.entry(&k[OVERRIDE_PREFIX.len()..], &map_value); + phf_map.entry(&k[OVERRIDE_PREFIX.len()..], map_value.clone()); } "false" | "true" => { - phf_map.entry(krate, &format!("(r#{krate}::uumain, r#{krate}::uu_app)")); + phf_map.entry(krate, format!("(r#{krate}::uumain, r#{krate}::uu_app)")); } "hashsum" => { - phf_map.entry(krate, &format!("({krate}::uumain, {krate}::uu_app_custom)")); + phf_map.entry(krate, format!("({krate}::uumain, {krate}::uu_app_custom)")); let map_value = format!("({krate}::uumain, {krate}::uu_app_common)"); let map_value_bits = format!("({krate}::uumain, {krate}::uu_app_bits)"); let map_value_b3sum = format!("({krate}::uumain, {krate}::uu_app_b3sum)"); - phf_map.entry("md5sum", &map_value); - phf_map.entry("sha1sum", &map_value); - phf_map.entry("sha224sum", &map_value); - phf_map.entry("sha256sum", &map_value); - phf_map.entry("sha384sum", &map_value); - phf_map.entry("sha512sum", &map_value); - phf_map.entry("sha3sum", &map_value_bits); - phf_map.entry("sha3-224sum", &map_value); - phf_map.entry("sha3-256sum", &map_value); - phf_map.entry("sha3-384sum", &map_value); - phf_map.entry("sha3-512sum", &map_value); - phf_map.entry("shake128sum", &map_value_bits); - phf_map.entry("shake256sum", &map_value_bits); - phf_map.entry("b2sum", &map_value); - phf_map.entry("b3sum", &map_value_b3sum); + phf_map.entry("md5sum", map_value.clone()); + phf_map.entry("sha1sum", map_value.clone()); + phf_map.entry("sha224sum", map_value.clone()); + phf_map.entry("sha256sum", map_value.clone()); + phf_map.entry("sha384sum", map_value.clone()); + phf_map.entry("sha512sum", map_value.clone()); + phf_map.entry("sha3sum", map_value_bits.clone()); + phf_map.entry("sha3-224sum", map_value.clone()); + phf_map.entry("sha3-256sum", map_value.clone()); + phf_map.entry("sha3-384sum", map_value.clone()); + phf_map.entry("sha3-512sum", map_value.clone()); + phf_map.entry("shake128sum", map_value_bits.clone()); + phf_map.entry("shake256sum", map_value_bits.clone()); + phf_map.entry("b2sum", map_value.clone()); + phf_map.entry("b3sum", map_value_b3sum); } _ => { - phf_map.entry(krate, &map_value); + phf_map.entry(krate, map_value.clone()); } } } diff --git a/deny.toml b/deny.toml index 943fcdfa918..63397aab16f 100644 --- a/deny.toml +++ b/deny.toml @@ -22,10 +22,11 @@ allow = [ "Apache-2.0", "ISC", "BSD-2-Clause", - "BSD-2-Clause-FreeBSD", "BSD-3-Clause", + "BSL-1.0", "CC0-1.0", - "Unicode-DFS-2016", + "Unicode-3.0", + "Zlib", ] confidence-threshold = 0.8 @@ -53,54 +54,56 @@ highlight = "all" # introduces it. # spell-checker: disable skip = [ - # rustix - { name = "linux-raw-sys", version = "0.3.8" }, - # terminal_size - { name = "rustix", version = "0.37.26" }, - # notify - { name = "windows-sys", version = "0.45.0" }, - # various crates - { name = "windows-sys", version = "0.48.0" }, - # windows-sys - { name = "windows-targets", version = "0.42.2" }, - # windows-sys - { name = "windows-targets", version = "0.48.0" }, - # windows-targets - { name = "windows_aarch64_gnullvm", version = "0.42.2" }, - # windows-targets - { name = "windows_aarch64_msvc", version = "0.42.2" }, - # windows-targets - { name = "windows_i686_gnu", version = "0.42.2" }, + # mio, nu-ansi-term, socket2 + { name = "windows-sys", version = "0.52.0" }, + # anstyle-query + { name = "windows-sys", version = "0.59.0" }, + # parking_lot_core + { name = "windows-targets", version = "0.52.6" }, # windows-targets - { name = "windows_i686_msvc", version = "0.42.2" }, + { name = "windows_aarch64_gnullvm", version = "0.52.6" }, # windows-targets - { name = "windows_x86_64_gnu", version = "0.42.2" }, + { name = "windows_aarch64_msvc", version = "0.52.6" }, # windows-targets - { name = "windows_x86_64_gnullvm", version = "0.42.2" }, + { name = "windows_i686_gnu", version = "0.52.6" }, # windows-targets - { name = "windows_x86_64_msvc", version = "0.42.2" }, + { name = "windows_i686_gnullvm", version = "0.52.6" }, # windows-targets - { name = "windows_aarch64_gnullvm", version = "0.48.0" }, + { name = "windows_i686_msvc", version = "0.52.6" }, # windows-targets - { name = "windows_aarch64_msvc", version = "0.48.0" }, + { name = "windows_x86_64_gnu", version = "0.52.6" }, # windows-targets - { name = "windows_i686_gnu", version = "0.48.0" }, + { name = "windows_x86_64_gnullvm", version = "0.52.6" }, # windows-targets - { name = "windows_i686_msvc", version = "0.48.0" }, - # windows-targets - { name = "windows_x86_64_gnu", version = "0.48.0" }, - # windows-targets - { name = "windows_x86_64_gnullvm", version = "0.48.0" }, - # windows-targets - { name = "windows_x86_64_msvc", version = "0.48.0" }, - # various crates - { name = "syn", version = "1.0.109" }, - # various crates + { name = "windows_x86_64_msvc", version = "0.52.6" }, + # kqueue-sys, onig { name = "bitflags", version = "1.3.2" }, - # clap_builder, textwrap - { name = "terminal_size", version = "0.2.6" }, - # filetime, parking_lot_core - { name = "redox_syscall", version = "0.4.1" }, + # ansi-width + { name = "unicode-width", version = "0.1.13" }, + # filedescriptor, utmp-classic + { name = "thiserror", version = "1.0.69" }, + # thiserror + { name = "thiserror-impl", version = "1.0.69" }, + # bindgen + { name = "itertools", version = "0.13.0" }, + # ordered-multimap + { name = "hashbrown", version = "0.14.5" }, + # cexpr (via bindgen) + { name = "nom", version = "7.1.3" }, + # const-random-macro, rand_core + { name = "getrandom", version = "0.2.15" }, + # getrandom, mio + { name = "wasi", version = "0.11.0+wasi-snapshot-preview1" }, + # num-bigint, num-prime, phf_generator + { name = "rand", version = "0.8.5" }, + # rand + { name = "rand_chacha", version = "0.3.1" }, + # rand + { name = "rand_core", version = "0.6.4" }, + # utmp-classic + { name = "zerocopy", version = "0.7.35" }, + # rustix + { name = "linux-raw-sys", version = "0.9.4" }, ] # spell-checker: enable diff --git a/docs/src/CODE_OF_CONDUCT.md b/docs/src/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..ce326a1ee03 --- /dev/null +++ b/docs/src/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ + + +{{ #include ../../CODE_OF_CONDUCT.md }} diff --git a/docs/src/contributing.md b/docs/src/CONTRIBUTING.md similarity index 100% rename from docs/src/contributing.md rename to docs/src/CONTRIBUTING.md diff --git a/docs/src/DEVELOPMENT.md b/docs/src/DEVELOPMENT.md new file mode 100644 index 00000000000..580cecf0855 --- /dev/null +++ b/docs/src/DEVELOPMENT.md @@ -0,0 +1,3 @@ + + +{{ #include ../../DEVELOPMENT.md }} diff --git a/docs/src/extensions.md b/docs/src/extensions.md index 79746498f2d..86db20172ed 100644 --- a/docs/src/extensions.md +++ b/docs/src/extensions.md @@ -1,3 +1,5 @@ + + # Extensions over GNU Though the main goal of the project is compatibility, uutils supports a few @@ -22,8 +24,13 @@ $ ls -w=80 ## `env` -`env` has an additional `-f`/`--file` flag that can parse `.env` files and set -variables accordingly. This feature is adopted from `dotenv` style packages. +GNU `env` allows the empty string to be used as an environment variable name. +This is unsupported by uutils, and it will show a warning on any such +assignment. + + `env` has an additional `-f`/`--file` flag that can +parse `.env` files and set variables accordingly. This feature is adopted from `dotenv` style +packages. ## `cp` @@ -66,16 +73,134 @@ feature is adopted from [FreeBSD](https://www.freebsd.org/cgi/man.cgi?cut). mail headers in the input. `-q`/`--quick` breaks lines more quickly. And `-T`/`--tab-width` defines the number of spaces representing a tab when determining the line length. +## `printf` + +`printf` uses arbitrary precision decimal numbers to parse and format floating point +numbers. GNU coreutils uses `long double`, whose actual size may be [double precision +64-bit float](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) +(e.g 32-bit arm), [extended precision 80-bit float](https://en.wikipedia.org/wiki/Extended_precision) +(x86(-64)), or +[quadruple precision 128-bit float](https://en.wikipedia.org/wiki/Quadruple-precision_floating-point_format) (e.g. arm64). + +Practically, this means that printing a number with a large precision will stay exact: +``` +printf "%.48f\n" 0.1 +0.100000000000000000000000000000000000000000000000 << uutils on all platforms +0.100000000000000000001355252715606880542509316001 << GNU coreutils on x86(-64) +0.100000000000000000000000000000000004814824860968 << GNU coreutils on arm64 +0.100000000000000005551115123125782702118158340454 << GNU coreutils on armv7 (32-bit) +``` + +### Hexadecimal floats + +For hexadecimal float format (`%a`), POSIX only states that one hexadecimal number +should be present left of the decimal point (`0xh.hhhhp±d` [1]), but does not say how +many _bits_ should be included (between 1 and 4). On x86(-64), the first digit always +includes 4 bits, so its value is always between `0x8` and `0xf`, while on other +architectures, only 1 bit is included, so the value is always `0x1`. + +However, the first digit will of course be `0x0` if the number is zero. Also, +rounding numbers may cause the first digit to be `0x1` on x86(-64) (e.g. +`0xf.fffffffp-5` rounds to `0x1.00p-1`), or `0x2` on other architectures. + +We chose to replicate x86-64 behavior on all platforms. + +Additionally, the default precision of the hexadecimal float format (`%a` without +any specifier) is expected to be "sufficient for exact representation of the value" [1]. +This is not possible in uutils as we store arbitrary precision numbers that may be +periodic in hexadecimal form (`0.1 = 0xc.ccc...p-7`), so we revert +to the number of digits that would be required to exactly print an +[extended precision 80-bit float](https://en.wikipedia.org/wiki/Extended_precision), +emulating GNU coreutils behavior on x86(-64). An 80-bit float has 64 bits in its +integer and fractional part, so 16 hexadecimal digits are printed in total (1 digit +before the decimal point, 15 after). + +Practically, this means that the default hexadecimal floating point output is +identical to x86(-64) GNU coreutils: +``` +printf "%a\n" 0.1 +0xc.ccccccccccccccdp-7 << uutils on all platforms +0xc.ccccccccccccccdp-7 << GNU coreutils on x86-64 +0x1.999999999999999999999999999ap-4 << GNU coreutils on arm64 +0x1.999999999999ap-4 << GNU coreutils on armv7 (32-bit) +``` + +We _can_ print an arbitrary number of digits if a larger precision is requested, +and the leading digit will still be in the `0x8`-`0xf` range: +``` +printf "%.32a\n" 0.1 +0xc.cccccccccccccccccccccccccccccccdp-7 << uutils on all platforms +0xc.ccccccccccccccd00000000000000000p-7 << GNU coreutils on x86-64 +0x1.999999999999999999999999999a0000p-4 << GNU coreutils on arm64 +0x1.999999999999a0000000000000000000p-4 << GNU coreutils on armv7 (32-bit) +``` + +***Note: The architecture-specific behavior on non-x86(-64) platforms may change in +the future.*** + ## `seq` +Unlike GNU coreutils, `seq` always uses arbitrary precision decimal numbers, no +matter the parameters (integers, decimal numbers, positive or negative increments, +format specified, etc.), so its output will be more correct than GNU coreutils for +some inputs (e.g. small fractional increments where GNU coreutils uses `long double`). + +The only limitation is that the position of the decimal point is stored in a `i64`, +so values smaller than 10**(-2**63) will underflow to 0, and some values larger +than 10**(2**63) may overflow to infinity. + +See also comments under `printf` for formatting precision and differences. + `seq` provides `-t`/`--terminator` to set the terminator character. +## `sort` + +When sorting with `-g`/`--general-numeric-sort`, arbitrary precision decimal numbers +are parsed and compared, unlike GNU coreutils that uses platform-specific long +double floating point numbers. + +Extremely large or small values can still overflow or underflow to infinity or zero, +see note in `seq`. + ## `ls` GNU `ls` provides two ways to use a long listing format: `-l` and `--format=long`. We support a third way: `--long`. +GNU `ls --sort=VALUE` only supports special non-default sort orders. +We support `--sort=name`, which makes it possible to override an earlier value. + ## `du` `du` allows `birth` and `creation` as values for the `--time` argument to show the creation time. It also provides a `-v`/`--verbose` flag. + +## `id` + +`id` has three additional flags: +* `-P` displays the id as a password file entry +* `-p` makes the output human-readable +* `-A` displays the process audit user ID + +## `uptime` + +Similar to the proc-ps implementation and unlike GNU/Coreutils, `uptime` provides `-s`/`--since` to show since when the system is up. + +## `base32/base64/basenc` + +Just like on macOS, `base32/base64/basenc` provides `-D` to decode data. + +## `shred` + +The number of random passes is deterministic in both GNU and uutils. However, uutils `shred` computes the number of random passes in a simplified way, specifically `max(3, x / 10)`, which is very close but not identical to the number of random passes that GNU would do. This also satisfies an expectation that reasonable users might have, namely that the number of random passes increases monotonically with the number of passes overall; GNU `shred` violates this assumption. + +## `unexpand` + +GNU `unexpand` provides `--first-only` to convert only leading sequences of blanks. We support a +second way: `-f` like busybox. + +Using `-U`/`--no-utf8`, you can interpret input files as 8-bit ASCII rather than UTF-8. + +## `expand` + +`expand` also offers the `-U`/`--no-utf8` option to interpret input files as 8-bit ASCII instead of UTF-8. diff --git a/docs/src/installation.md b/docs/src/installation.md index 80afdda53f7..856ca9d22f5 100644 --- a/docs/src/installation.md +++ b/docs/src/installation.md @@ -16,11 +16,11 @@ You can also [build uutils from source](build.md). ```shell # Linux -cargo install coreutils --features unix +cargo install coreutils --features unix --locked # MacOs -cargo install coreutils --features macos +cargo install coreutils --features macos --locked # Windows -cargo install coreutils --features windows +cargo install coreutils --features windows --locked ``` ## Linux @@ -53,6 +53,16 @@ apt install rust-coreutils export PATH=/usr/lib/cargo/bin/coreutils:$PATH ``` +### Fedora + +[![Fedora package](https://repology.org/badge/version-for-repo/fedora_rawhide/uutils-coreutils.svg)](https://packages.fedoraproject.org/pkgs/rust-coreutils/uutils-coreutils) + +```shell +dnf install uutils-coreutils +# To use it: +export PATH=/usr/libexec/uutils-coreutils:$PATH +``` + ### Gentoo [![Gentoo package](https://repology.org/badge/version-for-repo/gentoo/uutils-coreutils.svg)](https://packages.gentoo.org/packages/sys-apps/uutils-coreutils) @@ -89,9 +99,22 @@ nix-env -iA nixos.uutils-coreutils dnf install uutils-coreutils ``` +### RHEL/AlmaLinux/CENTOS Stream/Rocky Linux/EPEL 9 + +[![epel 9 package](https://repology.org/badge/version-for-repo/epel_9/uutils-coreutils.svg)](https://packages.fedoraproject.org/pkgs/rust-coreutils/uutils-coreutils/epel-9.html) + +```shell +# Install EPEL 9 - Specific For RHEL please check codeready-builder-for-rhel-9 First then install epel +dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm -y +# Install Core Utils +dnf install uutils-coreutils +# To use it: +export PATH=/usr/libexec/uutils-coreutils:$PATH +``` + ### Ubuntu -[![Ubuntu package](https://repology.org/badge/version-for-repo/ubuntu_23_04/uutils-coreutils.svg)](https://packages.ubuntu.com/source/lunar/rust-coreutils) +[![Ubuntu package](https://repology.org/badge/version-for-repo/ubuntu_25_04/uutils-coreutils.svg)](https://packages.ubuntu.com/source/plucky/rust-coreutils) ```shell apt install rust-coreutils @@ -164,10 +187,8 @@ then build your usual yocto image. ## Non-standard packages -### `coreutils-hybrid` (AUR) +### `coreutils-uutils` (AUR) -[![AUR package](https://repology.org/badge/version-for-repo/aur/coreutils-hybrid.svg)](https://aur.archlinux.org/packages/coreutils-hybrid) +[AUR package](https://aur.archlinux.org/packages/coreutils-uutils) -A GNU coreutils / uutils coreutils hybrid package. Uses stable uutils -programs mixed with GNU counterparts if uutils counterpart is -unfinished or buggy. +Cross-platform Rust rewrite of the GNU coreutils being used as actual system coreutils. diff --git a/docs/src/l10n.md b/docs/src/l10n.md new file mode 100644 index 00000000000..5704004ce70 --- /dev/null +++ b/docs/src/l10n.md @@ -0,0 +1,210 @@ +# 🌠Localization (L10n) in uutils coreutils + +This guide explains how localization (L10n) is implemented in the **Rust-based coreutils project**, detailing the use of [Fluent](https://projectfluent.org/) files, runtime behavior, and developer integration. + +## ðŸ—ï¸ Architecture Overview + +**English (US) locale files (`en-US.ftl`) are embedded directly in the binary**, ensuring that English always works regardless of how the software is installed. Other language locale files are loaded from the filesystem at runtime. + +### Source Repository Structure + +- **Main repository**: Contains English (`en-US.ftl`) locale files embedded in binaries +- **Translation repository**: [uutils/coreutils-l10n](https://github.com/uutils/coreutils-l10n) contains all other language translations + +--- + +## 📠Fluent File Layout + +Each utility has its own set of translation files under: + +``` + src/uu//locales/.ftl +``` + +Examples: + +``` + src/uu/ls/locales/en-US.ftl # Embedded in binary + src/uu/ls/locales/fr-FR.ftl # Loaded from filesystem +``` + +These files follow Fluent syntax and contain localized message patterns. + +--- + +## âš™ï¸ Initialization + +Localization must be explicitly initialized at runtime using: + +``` + setup_localization(path) +``` + +This is typically done: +- In `src/bin/coreutils.rs` for **multi-call binaries** +- In `src/uucore/src/lib.rs` for **single-call utilities** + +The string parameter determines the lookup path for Fluent files. **English always works** because it's embedded, but other languages need their `.ftl` files to be available at runtime. + +--- + +## 🌠Locale Detection + +Locale selection is automatic and performed via: + +``` + fn detect_system_locale() -> Result +``` + +It reads the `LANG` environment variable (e.g., `fr-FR.UTF-8`), strips encoding, and parses the identifier. + +If parsing fails or `LANG` is not set, it falls back to: + +``` + const DEFAULT_LOCALE: &str = "en-US"; +``` + +You can override the locale at runtime by running: + +``` + LANG=ja-JP ./target/debug/ls +``` + +--- + +## 📥 Retrieving Messages + +We have a single macro to handle translations. +It can be used in two ways: + +### `translate!(id: &str) -> String` + +Returns the message from the current locale bundle. + +``` + let msg = translate!("id-greeting"); +``` + +If not found, falls back to `en-US`. If still missing, returns the ID itself. + +--- + +### `translate!(id: &str, args: key-value pairs) -> String` + +Supports variable interpolation and pluralization. + +``` + let msg = translate!( + "error-io", + "error" => std::io::Error::last_os_error() + ); +``` + +Fluent message example: + +``` + error-io = I/O error occurred: { $error } +``` + +Variables must match the Fluent placeholder keys (`$error`, `$name`, `$count`, etc.). + +--- + +## 📦 Fluent Syntax Example + +``` + id-greeting = Hello, world! + welcome = Welcome, { $name }! + count-files = You have { $count -> + [one] { $count } file + *[other] { $count } files + } +``` + +Use plural rules and inline variables to adapt messages dynamically. + +--- + +## 🧪 Testing Localization + +Run all localization-related unit tests with: + +``` + cargo test --lib -p uucore +``` + +Tests include: +- Loading bundles +- Plural logic +- Locale fallback +- Fluent parse errors +- Thread-local behavior +- ... + +--- + +## 🧵 Thread-local Storage + +Localization is stored per thread using a `OnceLock`. +Each thread must call `setup_localization()` individually. +Initialization is **one-time-only** per thread — re-initialization results in an error. + +--- + +## 🧪 Development vs Release Mode + +During development (`cfg(debug_assertions)`), paths are resolved relative to the crate source: + +``` + $CARGO_MANIFEST_DIR/../uu//locales/ +``` + +In release mode, **paths are resolved relative to the executable**: + +``` + /locales// + /share/locales// + ~/.local/share/coreutils/locales// + ~/.cargo/share/coreutils/locales// + /usr/share/coreutils/locales// +``` + +If external locale files aren't found, the system falls back to embedded English locales. + +--- + +## 🔤 Unicode Isolation Handling + +By default, the Fluent system wraps variables with Unicode directional isolate characters (`U+2068`, `U+2069`) to protect against visual reordering issues in bidirectional text (e.g., mixing Arabic and English). + +In this implementation, isolation is **disabled** via: + +``` + bundle.set_use_isolating(false); +``` + +This improves readability in CLI environments by preventing extraneous characters around interpolated values: + +Correct (as rendered): + +``` + "Welcome, Alice!" +``` + +Fluent default (disabled here): + +``` + "\u{2068}Alice\u{2069}" +``` + +--- + +## 🔧 Embedded English Locales + +English locale files are always embedded directly in the binary during the build process. This ensures that: + +- **English always works** regardless of installation method (e.g., `cargo install`) +- **No runtime dependency** on external `.ftl` files for English +- **Fallback behavior** when other language files are missing + +The embedded English locales are generated at build time and included in the binary, providing a reliable fallback while still supporting full localization for other languages when their `.ftl` files are available. diff --git a/docs/src/performance.md b/docs/src/performance.md new file mode 100644 index 00000000000..10c7b383447 --- /dev/null +++ b/docs/src/performance.md @@ -0,0 +1,108 @@ + + +# Performance Profiling Tutorial + +## Effective Benchmarking with Hyperfine + +[Hyperfine](https://github.com/sharkdp/hyperfine) is a powerful command-line benchmarking tool that allows you to measure and compare execution times of commands with statistical rigor. + +### Benchmarking Best Practices + +When evaluating performance improvements, always set up your benchmarks to compare: + +1. The GNU implementation as reference +2. The implementation without the change +3. The implementation with your change + +This three-way comparison provides clear insights into: +- How your implementation compares to the standard (GNU) +- The actual performance impact of your specific change + +### Example Benchmark + +First, you will need to build the binary in release mode. Debug builds are significantly slower: + +```bash +cargo build --features unix --profile profiling +``` + +```bash +# Three-way comparison benchmark +hyperfine \ + --warmup 3 \ + "/usr/bin/ls -R ." \ + "./target/profiling/coreutils.prev ls -R ." \ + "./target/profiling/coreutils ls -R ." + +# can be simplified with: +hyperfine \ + --warmup 3 \ + -L ls /usr/bin/ls,"./target/profiling/coreutils.prev ls","./target/profiling/coreutils ls" \ + "{ls} -R ." +``` + +``` +# to improve the reproducibility of the results: +taskset -c 0 +``` + +### Interpreting Results + +Hyperfine provides summary statistics including: +- Mean execution time +- Standard deviation +- Min/max times +- Relative performance comparison + +Look for consistent patterns rather than focusing on individual runs, and be aware of system noise that might affect results. + +## Using Samply for Profiling + +[Samply](https://github.com/mstange/samply) is a sampling profiler that helps you identify performance bottlenecks in your code. + +### Basic Profiling + +```bash +# Generate a flame graph for your application +samply record ./target/debug/coreutils ls -R + +# Profile with higher sampling frequency +samply record --rate 1000 ./target/debug/coreutils seq 1 1000 +``` + +The output using the `debug` profile might be easier to understand, but the performance characteristics may be somewhat different from `release` profile that we _actually_ care about. + +Consider using the `profiling` profile, that compiles in `release` mode but with debug symbols. For example: +```bash +cargo build --profile profiling -p uu_ls +samply record -r 10000 target/profiling/ls -lR /var .git .git .git > /dev/null +``` + +## Workflow: Measuring Performance Improvements + +1. **Establish baselines**: + ```bash + hyperfine --warmup 3 \ + "/usr/bin/sort large_file.txt" \ + "our-sort-v1 large_file.txt" + ``` + +2. **Identify bottlenecks**: + ```bash + samply record ./our-sort-v1 large_file.txt + ``` + +3. **Make targeted improvements** based on profiling data + +4. **Verify improvements**: + ```bash + hyperfine --warmup 3 \ + "/usr/bin/sort large_file.txt" \ + "our-sort-v1 large_file.txt" \ + "our-sort-v2 large_file.txt" + ``` + +5. **Document performance changes** with concrete numbers + ```bash + hyperfine --export-markdown file.md [...] + ``` diff --git a/docs/src/test_coverage.js b/docs/src/test_coverage.js index e601229affc..318c9934d53 100644 --- a/docs/src/test_coverage.js +++ b/docs/src/test_coverage.js @@ -19,7 +19,7 @@ function progressBar(totals) { var(--SKIP) ${skipPercentage}%` ) + (skipPercentage === 100 ? ")" : ", var(--FAIL) 0)"); - + const progress = document.createElement("div"); progress.className = "progress" progress.innerHTML = ` @@ -74,7 +74,7 @@ function parse_result(parent, obj) { return totals; } -fetch("https://raw.githubusercontent.com/uutils/coreutils-tracking/main/gnu-full-result.json") +fetch("https://raw.githubusercontent.com/uutils/coreutils-tracking/main/aggregated-result.json") .then((r) => r.json()) .then((obj) => { let parent = document.getElementById("test-cov"); diff --git a/docs/src/test_coverage.md b/docs/src/test_coverage.md index b8376058873..2bfad68bcac 100644 --- a/docs/src/test_coverage.md +++ b/docs/src/test_coverage.md @@ -18,4 +18,4 @@ or resulted in an error. ## Progress over time - + diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000000..fbf85d3df31 --- /dev/null +++ b/flake.lock @@ -0,0 +1,42 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1720633750, + "narHash": "sha256-N8apMO2pP/upWeH+JY5eM8VDp2qBAAzE+OY5LRW6qpw=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "54bc082f5a7219d122e74fe52c021cf59fed9d6f", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "systems": "systems" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000000..9c99b7b72e1 --- /dev/null +++ b/flake.nix @@ -0,0 +1,75 @@ +# spell-checker:ignore bintools gnum gperf ldflags libclang nixpkgs numtide pkgs texinfo gettext +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs"; + + # + systems.url = "github:nix-systems/default"; + }; + + outputs = inputs: let + inherit (inputs.nixpkgs) lib legacyPackages; + eachSystem = lib.genAttrs (import inputs.systems); + pkgsFor = legacyPackages; + in { + devShells = eachSystem ( + system: let + libselinuxPath = with pkgsFor.${system}; + lib.makeLibraryPath [ + libselinux + ]; + + libaclPath = with pkgsFor.${system}; + lib.makeLibraryPath [ + acl + ]; + + build_deps = with pkgsFor.${system}; [ + clang + llvmPackages.bintools + rustup + + pre-commit + nodePackages.cspell + + # debugging + gdb + ]; + + gnu_testing_deps = with pkgsFor.${system}; [ + autoconf + automake + bison + gnum4 + gperf + gettext + texinfo + ]; + in { + default = pkgsFor.${system}.pkgs.mkShell { + packages = build_deps ++ gnu_testing_deps; + + RUSTC_VERSION = "1.85"; + LIBCLANG_PATH = pkgsFor.${system}.lib.makeLibraryPath [pkgsFor.${system}.llvmPackages_latest.libclang.lib]; + shellHook = '' + export PATH=$PATH:''${CARGO_HOME:-~/.cargo}/bin + export PATH=$PATH:''${RUSTUP_HOME:-~/.rustup}/toolchains/$RUSTC_VERSION-x86_64-unknown-linux-gnu/bin/ + ''; + + SELINUX_INCLUDE_DIR = ''${pkgsFor.${system}.libselinux.dev}/include''; + SELINUX_LIB_DIR = libselinuxPath; + SELINUX_STATIC = "0"; + + # Necessary to build GNU. + LDFLAGS = ''-L ${libselinuxPath} -L ${libaclPath}''; + + # Add precompiled library to rustc search path + RUSTFLAGS = [ + ''-L ${libselinuxPath}'' + ''-L ${libaclPath}'' + ]; + }; + } + ); + }; +} diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 00000000000..bc3481f7560 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,2113 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bigdecimal" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "binary-heap-plus" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4551d8382e911ecc0d0f0ffb602777988669be09447d536ff4388d1def11296" +dependencies = [ + "compare", +] + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "blake2b_simd" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "cc" +version = "1.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "compare" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120133d4db2ec47efe2e26502ee984747630c67f51974fca0b6c1340cf2368d3" + +[[package]] +name = "console" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.60.2", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctrlc" +version = "3.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73" +dependencies = [ + "nix", + "windows-sys 0.59.0", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "data-encoding-macro" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" +dependencies = [ + "data-encoding", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "errno" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fluent" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" +dependencies = [ + "memchr", + "thiserror", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collator" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ad4c6a556938dfd31f75a8c54141079e8821dc697ffb799cfe0f0fa11f2edc" +dependencies = [ + "displaydoc", + "icu_collator_data", + "icu_collections", + "icu_locale", + "icu_locale_core", + "icu_normalizer", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_collator_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d880b8e680799eabd90c054e1b95526cd48db16c95269f3c89fb3117e1ac92c5" + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ae5921528335e91da1b6c695dbf1ec37df5ac13faa3f91e5640be93aa2fbefd" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fdef0c124749d06a743c69e938350816554eb63ac979166590e2b4ee4252765" + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", + "windows-sys 0.59.0", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown", +] + +[[package]] +name = "os_display" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5fd71b79026fb918650dde6d125000a233764f1c2f1659a1c71118e33ea08f" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "parse_datetime" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b77d27257a460cefd73a54448e5f3fd4db224150baf6ca3e02eedf4eb2b3e9" +dependencies = [ + "chrono", + "num-traits", + "regex", + "winnow", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "serde", + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rust-ini" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "self_cell" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "sm3" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" +dependencies = [ + "digest", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.60.2", +] + +[[package]] +name = "terminal_size" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "tinystr", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uu_cksum" +version = "0.1.0" +dependencies = [ + "clap", + "fluent", + "hex", + "uucore", +] + +[[package]] +name = "uu_cut" +version = "0.1.0" +dependencies = [ + "bstr", + "clap", + "fluent", + "memchr", + "uucore", +] + +[[package]] +name = "uu_date" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", + "fluent", + "jiff", + "libc", + "parse_datetime", + "uucore", + "windows-sys 0.60.2", +] + +[[package]] +name = "uu_echo" +version = "0.1.0" +dependencies = [ + "clap", + "fluent", + "uucore", +] + +[[package]] +name = "uu_env" +version = "0.1.0" +dependencies = [ + "clap", + "fluent", + "nix", + "rust-ini", + "thiserror", + "uucore", +] + +[[package]] +name = "uu_expr" +version = "0.1.0" +dependencies = [ + "clap", + "fluent", + "num-bigint", + "num-traits", + "onig", + "thiserror", + "uucore", +] + +[[package]] +name = "uu_printf" +version = "0.1.0" +dependencies = [ + "clap", + "fluent", + "uucore", +] + +[[package]] +name = "uu_seq" +version = "0.1.0" +dependencies = [ + "bigdecimal", + "clap", + "fluent", + "num-bigint", + "num-traits", + "thiserror", + "uucore", +] + +[[package]] +name = "uu_sort" +version = "0.1.0" +dependencies = [ + "bigdecimal", + "binary-heap-plus", + "clap", + "compare", + "ctrlc", + "fluent", + "fnv", + "itertools", + "memchr", + "nix", + "rand", + "rayon", + "self_cell", + "tempfile", + "thiserror", + "unicode-width", + "uucore", +] + +[[package]] +name = "uu_split" +version = "0.1.0" +dependencies = [ + "clap", + "fluent", + "memchr", + "thiserror", + "uucore", +] + +[[package]] +name = "uu_test" +version = "0.1.0" +dependencies = [ + "clap", + "fluent", + "libc", + "thiserror", + "uucore", +] + +[[package]] +name = "uu_tr" +version = "0.1.0" +dependencies = [ + "bytecount", + "clap", + "fluent", + "nom", + "uucore", +] + +[[package]] +name = "uu_wc" +version = "0.1.0" +dependencies = [ + "bytecount", + "clap", + "fluent", + "libc", + "nix", + "thiserror", + "unicode-width", + "uucore", +] + +[[package]] +name = "uucore" +version = "0.1.0" +dependencies = [ + "bigdecimal", + "blake2b_simd", + "blake3", + "bstr", + "clap", + "crc32fast", + "data-encoding", + "data-encoding-macro", + "digest", + "dunce", + "fluent", + "fluent-bundle", + "fluent-syntax", + "glob", + "hex", + "icu_collator", + "icu_locale", + "itertools", + "libc", + "md-5", + "memchr", + "nix", + "num-traits", + "number_prefix", + "os_display", + "sha1", + "sha2", + "sha3", + "sm3", + "thiserror", + "unic-langid", + "uucore_procs", + "wild", + "winapi-util", + "windows-sys 0.60.2", + "z85", +] + +[[package]] +name = "uucore-fuzz" +version = "0.0.0" +dependencies = [ + "libfuzzer-sys", + "rand", + "uu_cksum", + "uu_cut", + "uu_date", + "uu_echo", + "uu_env", + "uu_expr", + "uu_printf", + "uu_seq", + "uu_sort", + "uu_split", + "uu_test", + "uu_tr", + "uu_wc", + "uucore", + "uufuzz", +] + +[[package]] +name = "uucore_procs" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "uuhelp_parser", +] + +[[package]] +name = "uufuzz" +version = "0.1.0" +dependencies = [ + "console", + "libc", + "rand", + "similar", + "tempfile", + "uucore", +] + +[[package]] +name = "uuhelp_parser" +version = "0.1.0" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wild" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3131afc8c575281e1e80f36ed6a092aa502c08b18ed7524e86fbbb12bb410e1" +dependencies = [ + "glob", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "z85" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3a41ce106832b4da1c065baa4c31cf640cf965fa1483816402b7f6b96f0a64" + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index dfb62aba5e1..14282122461 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -1,34 +1,41 @@ [package] name = "uucore-fuzz" version = "0.0.0" +description = "uutils ~ 'core' uutils fuzzers" +repository = "https://github.com/uutils/coreutils/tree/main/fuzz/" +edition.workspace = true +license.workspace = true publish = false -edition = "2021" + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[workspace.package] +edition = "2024" +license = "MIT" [package.metadata] cargo-fuzz = true [dependencies] -libfuzzer-sys = "0.4" -libc = "0.2" -tempfile = "3" -rand = { version = "0.8", features = ["small_rng"] } -similar = "2" - -uucore = { path = "../src/uucore/" } -uu_date = { path = "../src/uu/date/" } -uu_test = { path = "../src/uu/test/" } -uu_expr = { path = "../src/uu/expr/" } -uu_printf = { path = "../src/uu/printf/" } -uu_echo = { path = "../src/uu/echo/" } -uu_seq = { path = "../src/uu/seq/" } -uu_sort = { path = "../src/uu/sort/" } -uu_wc = { path = "../src/uu/wc/" } -uu_cut = { path = "../src/uu/cut/" } -uu_split = { path = "../src/uu/split/" } - -# Prevent this from interfering with workspaces -[workspace] -members = ["."] +libfuzzer-sys = "0.4.7" +rand = { version = "0.9.0", features = ["small_rng"] } +uufuzz = { path = "uufuzz" } +uucore = { path = "../src/uucore", features = ["parser"] } +uu_date = { path = "../src/uu/date" } +uu_test = { path = "../src/uu/test" } +uu_expr = { path = "../src/uu/expr" } +uu_printf = { path = "../src/uu/printf" } +uu_echo = { path = "../src/uu/echo" } +uu_seq = { path = "../src/uu/seq" } +uu_sort = { path = "../src/uu/sort" } +uu_wc = { path = "../src/uu/wc" } +uu_cut = { path = "../src/uu/cut" } +uu_split = { path = "../src/uu/split" } +uu_tr = { path = "../src/uu/tr" } +uu_env = { path = "../src/uu/env" } +uu_cksum = { path = "../src/uu/cksum" } [[bin]] name = "fuzz_date" @@ -90,6 +97,12 @@ path = "fuzz_targets/fuzz_test.rs" test = false doc = false +[[bin]] +name = "fuzz_seq_parse_number" +path = "fuzz_targets/fuzz_seq_parse_number.rs" +test = false +doc = false + [[bin]] name = "fuzz_parse_glob" path = "fuzz_targets/fuzz_parse_glob.rs" @@ -107,3 +120,27 @@ name = "fuzz_parse_time" path = "fuzz_targets/fuzz_parse_time.rs" test = false doc = false + +[[bin]] +name = "fuzz_tr" +path = "fuzz_targets/fuzz_tr.rs" +test = false +doc = false + +[[bin]] +name = "fuzz_env" +path = "fuzz_targets/fuzz_env.rs" +test = false +doc = false + +[[bin]] +name = "fuzz_cksum" +path = "fuzz_targets/fuzz_cksum.rs" +test = false +doc = false + +[[bin]] +name = "fuzz_non_utf8_paths" +path = "fuzz_targets/fuzz_non_utf8_paths.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/fuzz_cksum.rs b/fuzz/fuzz_targets/fuzz_cksum.rs new file mode 100644 index 00000000000..be93a96050e --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_cksum.rs @@ -0,0 +1,170 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore chdir + +#![no_main] +use libfuzzer_sys::fuzz_target; +use rand::Rng; +use std::env::temp_dir; +use std::ffi::OsString; +use std::fs::{self, File}; +use std::io::Write; +use std::process::Command; +use uu_cksum::uumain; +use uufuzz::{ + CommandResult, compare_result, generate_and_run_uumain, generate_random_file, + generate_random_string, + pretty_print::{print_or_empty, print_test_begin}, + replace_fuzz_binary_name, run_gnu_cmd, +}; + +static CMD_PATH: &str = "cksum"; + +fn generate_cksum_args() -> Vec { + let mut rng = rand::rng(); + let mut args = Vec::new(); + + let digests = [ + "sysv", "bsd", "crc", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "blake2b", + "sm3", + ]; + let digest_opts = [ + "--base64", + "--raw", + "--tag", + "--untagged", + "--text", + "--binary", + ]; + + if rng.random_bool(0.3) { + args.push("-a".to_string()); + args.push(digests[rng.random_range(0..digests.len())].to_string()); + } + + if rng.random_bool(0.2) { + args.push(digest_opts[rng.random_range(0..digest_opts.len())].to_string()); + } + + if rng.random_bool(0.15) { + args.push("-l".to_string()); + args.push(rng.random_range(8..513).to_string()); + } + + if rng.random_bool(0.05) { + for _ in 0..rng.random_range(0..3) { + args.push(format!("file_{}", generate_random_string(5))); + } + } else { + args.push("-c".to_string()); + } + + if rng.random_bool(0.25) { + if let Ok(file_path) = generate_random_file() { + args.push(file_path); + } + } + + if args.is_empty() || !args.iter().any(|arg| arg.starts_with("file_")) { + args.push("-a".to_string()); + args.push(digests[rng.random_range(0..digests.len())].to_string()); + + if let Ok(file_path) = generate_random_file() { + args.push(file_path); + } + } + + args +} + +fn generate_checksum_file( + algo: &str, + file_path: &str, + digest_opts: &[&str], +) -> Result { + let checksum_file_path = temp_dir().join("checksum_file"); + let mut cmd = Command::new(CMD_PATH); + cmd.arg("-a").arg(algo); + + for opt in digest_opts { + cmd.arg(opt); + } + + cmd.arg(file_path); + let output = cmd.output()?; + + let mut checksum_file = File::create(&checksum_file_path)?; + checksum_file.write_all(&output.stdout)?; + + Ok(checksum_file_path.to_str().unwrap().to_string()) +} + +fn select_random_digest_opts<'a>( + rng: &mut rand::rngs::ThreadRng, + digest_opts: &'a [&'a str], +) -> Vec<&'a str> { + digest_opts + .iter() + .filter(|_| rng.random_bool(0.5)) + .copied() + .collect() +} + +fuzz_target!(|_data: &[u8]| { + let cksum_args = generate_cksum_args(); + let mut args = vec![OsString::from("cksum")]; + args.extend(cksum_args.iter().map(OsString::from)); + + if let Ok(file_path) = generate_random_file() { + let algo = cksum_args + .iter() + .position(|arg| arg == "-a") + .map_or("md5", |index| &cksum_args[index + 1]); + + let all_digest_opts = ["--base64", "--raw", "--tag", "--untagged"]; + let mut rng = rand::rng(); + let selected_digest_opts = select_random_digest_opts(&mut rng, &all_digest_opts); + + if let Ok(checksum_file_path) = + generate_checksum_file(algo, &file_path, &selected_digest_opts) + { + print_test_begin(format!("cksum {args:?}")); + + if let Ok(content) = fs::read_to_string(&checksum_file_path) { + println!("File content ({checksum_file_path})"); + print_or_empty(&content); + } else { + eprintln!("Error reading the checksum file."); + } + let mut rust_result = generate_and_run_uumain(&args, uumain, None); + + let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false, None) { + Ok(result) => result, + Err(error_result) => { + eprintln!("Failed to run GNU command:"); + eprintln!("Stderr: {}", error_result.stderr); + eprintln!("Exit Code: {}", error_result.exit_code); + CommandResult { + stdout: String::new(), + stderr: error_result.stderr, + exit_code: error_result.exit_code, + } + } + }; + + // Lower the number of false positives caused by binary names + replace_fuzz_binary_name("cksum", &mut rust_result); + + compare_result( + "cksum", + &format!("{:?}", &args[1..]), + None, + &rust_result, + &gnu_result, + false, + ); + } + } +}); diff --git a/fuzz/fuzz_targets/fuzz_cut.rs b/fuzz/fuzz_targets/fuzz_cut.rs index fa5f8fcc472..4a5215f8aec 100644 --- a/fuzz/fuzz_targets/fuzz_cut.rs +++ b/fuzz/fuzz_targets/fuzz_cut.rs @@ -11,26 +11,25 @@ use uu_cut::uumain; use rand::Rng; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::{ - compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, CommandResult, +use uufuzz::{ + CommandResult, compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, }; static CMD_PATH: &str = "cut"; fn generate_cut_args() -> String { - let mut rng = rand::thread_rng(); - let arg_count = rng.gen_range(1..=6); + let mut rng = rand::rng(); + let arg_count = rng.random_range(1..=6); let mut args = Vec::new(); for _ in 0..arg_count { - if rng.gen_bool(0.1) { - args.push(generate_random_string(rng.gen_range(1..=20))); + if rng.random_bool(0.1) { + args.push(generate_random_string(rng.random_range(1..=20))); } else { - match rng.gen_range(0..=4) { - 0 => args.push(String::from("-b") + &rng.gen_range(1..=10).to_string()), - 1 => args.push(String::from("-c") + &rng.gen_range(1..=10).to_string()), + match rng.random_range(0..=4) { + 0 => args.push(String::from("-b") + &rng.random_range(1..=10).to_string()), + 1 => args.push(String::from("-c") + &rng.random_range(1..=10).to_string()), 2 => args.push(String::from("-d,") + &generate_random_string(1)), // Using a comma as a default delimiter - 3 => args.push(String::from("-f") + &rng.gen_range(1..=5).to_string()), + 3 => args.push(String::from("-f") + &rng.random_range(1..=5).to_string()), _ => (), } } @@ -40,12 +39,12 @@ fn generate_cut_args() -> String { } fn generate_delimited_data(count: usize) -> String { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let mut lines = Vec::new(); for _ in 0..count { - let fields = (0..rng.gen_range(1..=5)) - .map(|_| generate_random_string(rng.gen_range(1..=10))) + let fields = (0..rng.random_range(1..=5)) + .map(|_| generate_random_string(rng.random_range(1..=10))) .collect::>() .join(","); lines.push(fields); diff --git a/fuzz/fuzz_targets/fuzz_echo.rs b/fuzz/fuzz_targets/fuzz_echo.rs index 3f15b257e6e..e6b0ba9a6aa 100644 --- a/fuzz/fuzz_targets/fuzz_echo.rs +++ b/fuzz/fuzz_targets/fuzz_echo.rs @@ -2,26 +2,24 @@ use libfuzzer_sys::fuzz_target; use uu_echo::uumain; -use rand::prelude::SliceRandom; use rand::Rng; +use rand::prelude::IndexedRandom; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::CommandResult; -use crate::fuzz_common::{ - compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, -}; +use uufuzz::CommandResult; +use uufuzz::{compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd}; static CMD_PATH: &str = "echo"; fn generate_echo() -> String { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let mut echo_str = String::new(); // Randomly decide whether to include options - let include_n = rng.gen_bool(0.1); // 10% chance - let include_e = rng.gen_bool(0.1); // 10% chance - let include_E = rng.gen_bool(0.1); // 10% chance + let include_n = rng.random_bool(0.1); // 10% chance + let include_e = rng.random_bool(0.1); // 10% chance + #[allow(non_snake_case)] + let include_E = rng.random_bool(0.1); // 10% chance if include_n { echo_str.push_str("-n "); @@ -34,12 +32,12 @@ fn generate_echo() -> String { } // Add a random string - echo_str.push_str(&generate_random_string(rng.gen_range(1..=10))); + echo_str.push_str(&generate_random_string(rng.random_range(1..=10))); // Include escape sequences if -e is enabled if include_e { // Add a 10% chance of including an escape sequence - if rng.gen_bool(0.1) { + if rng.random_bool(0.1) { echo_str.push_str(&generate_escape_sequence(&mut rng)); } } diff --git a/fuzz/fuzz_targets/fuzz_env.rs b/fuzz/fuzz_targets/fuzz_env.rs new file mode 100644 index 00000000000..284089f8378 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_env.rs @@ -0,0 +1,96 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore chdir + +#![no_main] +use libfuzzer_sys::fuzz_target; +use uu_env::uumain; + +use std::ffi::OsString; + +use rand::Rng; +use uufuzz::{ + CommandResult, compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, +}; + +static CMD_PATH: &str = "env"; + +fn generate_env_args() -> Vec { + let mut rng = rand::rng(); + let mut args = Vec::new(); + + let opts = ["-i", "-0", "-v", "-vv"]; + for opt in &opts { + if rng.random_bool(0.2) { + args.push(opt.to_string()); + } + } + + if rng.random_bool(0.3) { + args.push(format!( + "-u={}", + generate_random_string(rng.random_range(3..10)) + )); + } + + if rng.random_bool(0.2) { + args.push(format!("--chdir={}", "/tmp")); // Simplified example + } + + /* + Options not implemented for now + if rng.random_bool(0.15) { + let sig_opts = ["--block-signal"];//, /*"--default-signal",*/ "--ignore-signal"]; + let chosen_sig_opt = sig_opts[rng.random_range(0..sig_opts.len())]; + args.push(chosen_sig_opt.to_string()); + // Simplify by assuming SIGPIPE for demonstration + if !chosen_sig_opt.ends_with("list-signal-handling") { + args.push(String::from("SIGPIPE")); + } + }*/ + + // Adding a few random NAME=VALUE pairs + for _ in 0..rng.random_range(0..3) { + args.push(format!( + "{}={}", + generate_random_string(5), + generate_random_string(5) + )); + } + + args +} + +fuzz_target!(|_data: &[u8]| { + let env_args = generate_env_args(); + let mut args = vec![OsString::from("env")]; + args.extend(env_args.iter().map(OsString::from)); + let input_lines = generate_random_string(10); + + let rust_result = generate_and_run_uumain(&args, uumain, Some(&input_lines)); + + let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false, None) { + Ok(result) => result, + Err(error_result) => { + eprintln!("Failed to run GNU command:"); + eprintln!("Stderr: {}", error_result.stderr); + eprintln!("Exit Code: {}", error_result.exit_code); + CommandResult { + stdout: String::new(), + stderr: error_result.stderr, + exit_code: error_result.exit_code, + } + } + }; + + compare_result( + "env", + &format!("{:?}", &args[1..]), + None, + &rust_result, + &gnu_result, + false, + ); +}); diff --git a/fuzz/fuzz_targets/fuzz_expr.rs b/fuzz/fuzz_targets/fuzz_expr.rs index 8bc18fae47e..77ecffabc1b 100644 --- a/fuzz/fuzz_targets/fuzz_expr.rs +++ b/fuzz/fuzz_targets/fuzz_expr.rs @@ -8,21 +8,19 @@ use libfuzzer_sys::fuzz_target; use uu_expr::uumain; -use rand::seq::SliceRandom; use rand::Rng; +use rand::prelude::IndexedRandom; use std::{env, ffi::OsString}; -mod fuzz_common; -use crate::fuzz_common::CommandResult; -use crate::fuzz_common::{ - compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, -}; +use uufuzz::CommandResult; +use uufuzz::{compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd}; static CMD_PATH: &str = "expr"; fn generate_expr(max_depth: u32) -> String { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let ops = [ - "+", "-", "*", "/", "%", "<", ">", "=", "&", "|", "!=", "<=", ">=", ":", "index", "length", "substr", + "+", "-", "*", "/", "%", "<", ">", "=", "&", "|", "!=", "<=", ">=", ":", "index", "length", + "substr", ]; let mut expr = String::new(); @@ -32,18 +30,18 @@ fn generate_expr(max_depth: u32) -> String { while depth <= max_depth { if last_was_operator || depth == 0 { // Add a number - expr.push_str(&rng.gen_range(1..=100).to_string()); + expr.push_str(&rng.random_range(1..=100).to_string()); last_was_operator = false; } else { // 90% chance to add an operator followed by a number - if rng.gen_bool(0.9) { + if rng.random_bool(0.9) { let op = *ops.choose(&mut rng).unwrap(); - expr.push_str(&format!(" {} ", op)); + expr.push_str(&format!(" {op} ")); last_was_operator = true; } // 10% chance to add a random string (potentially invalid syntax) else { - let random_str = generate_random_string(rng.gen_range(1..=10)); + let random_str = generate_random_string(rng.random_range(1..=10)); expr.push_str(&random_str); last_was_operator = false; } @@ -53,22 +51,24 @@ fn generate_expr(max_depth: u32) -> String { // Ensure the expression ends with a number if it ended with an operator if last_was_operator { - expr.push_str(&rng.gen_range(1..=100).to_string()); + expr.push_str(&rng.random_range(1..=100).to_string()); } expr } fuzz_target!(|_data: &[u8]| { - let mut rng = rand::thread_rng(); - let expr = generate_expr(rng.gen_range(0..=20)); + let mut rng = rand::rng(); + let expr = generate_expr(rng.random_range(0..=20)); let mut args = vec![OsString::from("expr")]; args.extend(expr.split_whitespace().map(OsString::from)); // Use C locale to avoid false positives, like in https://github.com/uutils/coreutils/issues/5378, // because uutils expr doesn't support localization yet // TODO remove once uutils expr supports localization - env::set_var("LC_COLLATE", "C"); + unsafe { + env::set_var("LC_COLLATE", "C"); + } let rust_result = generate_and_run_uumain(&args, uumain, None); let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false, None) { diff --git a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs new file mode 100644 index 00000000000..ac7480f3230 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs @@ -0,0 +1,442 @@ +// 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 osstring + +#![no_main] +use libfuzzer_sys::fuzz_target; +use rand::Rng; +use rand::prelude::IndexedRandom; +use std::collections::HashSet; +use std::env::temp_dir; +use std::ffi::{OsStr, OsString}; +use std::fs; +use std::os::unix::ffi::{OsStrExt, OsStringExt}; +use std::path::PathBuf; + +use uufuzz::{CommandResult, run_gnu_cmd}; +// Programs that typically take file/path arguments and should be tested +static PATH_PROGRAMS: &[&str] = &[ + // Core file operations + "cat", + "cp", + "mv", + "rm", + "ln", + "link", + "unlink", + "touch", + "truncate", + // Path operations + "ls", + "mkdir", + "rmdir", + "du", + "stat", + "mktemp", + "df", + "basename", + "dirname", + "readlink", + "realpath", + "pathchk", + "chroot", + // File processing + "head", + "tail", + "tee", + "more", + "od", + "wc", + "cksum", + "sum", + "nl", + "tac", + "sort", + "uniq", + "split", + "csplit", + "cut", + "tr", + "shred", + "shuf", + "ptx", + "tsort", + // Text processing with files + "chmod", + "chown", + "chgrp", + "install", + "chcon", + "runcon", + "comm", + "join", + "paste", + "pr", + "fmt", + "fold", + "expand", + "unexpand", + "dir", + "vdir", + "mkfifo", + "mknod", + "hashsum", + // File I/O utilities + "dd", + "sync", + "stdbuf", + "dircolors", + // Encoding/decoding utilities + "base32", + "base64", + "basenc", + "stty", + "tty", + "env", + "nohup", + "nice", + "timeout", +]; + +fn generate_non_utf8_bytes() -> Vec { + let mut rng = rand::rng(); + let mut bytes = Vec::new(); + + // Start with some valid UTF-8 to make it look like a reasonable path + bytes.extend_from_slice(b"test_"); + + // Add some invalid UTF-8 sequences + match rng.random_range(0..4) { + 0 => bytes.extend_from_slice(&[0xFF, 0xFE]), // Invalid UTF-8 + 1 => bytes.extend_from_slice(&[0xC0, 0x80]), // Overlong encoding + 2 => bytes.extend_from_slice(&[0xED, 0xA0, 0x80]), // UTF-16 surrogate + _ => bytes.extend_from_slice(&[0xF4, 0x90, 0x80, 0x80]), // Beyond Unicode range + } + + bytes +} + +fn generate_non_utf8_osstring() -> OsString { + OsString::from_vec(generate_non_utf8_bytes()) +} + +fn setup_test_files() -> Result<(PathBuf, Vec), std::io::Error> { + let mut rng = rand::rng(); + let temp_root = temp_dir().join(format!("utf8_test_{}", rng.random::())); + fs::create_dir_all(&temp_root)?; + + let mut test_files = Vec::new(); + + // Create some files with non-UTF-8 names + for i in 0..3 { + let mut path_bytes = temp_root.as_os_str().as_bytes().to_vec(); + path_bytes.push(b'/'); + + if i == 0 { + // One normal UTF-8 file for comparison + path_bytes.extend_from_slice(b"normal_file.txt"); + } else { + // Files with invalid UTF-8 names + path_bytes.extend_from_slice(&generate_non_utf8_bytes()); + } + + let file_path = PathBuf::from(OsStr::from_bytes(&path_bytes)); + + // Try to create the file - this may fail on some filesystems + if let Ok(mut file) = fs::File::create(&file_path) { + use std::io::Write; + let _ = write!(file, "test content for file {}\n", i); + test_files.push(file_path); + } + } + + Ok((temp_root, test_files)) +} + +fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResult { + let path_os = path.as_os_str(); + + // Use the locally built uutils binary instead of system PATH + let local_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + + // Build appropriate arguments for each program + let local_args = match program { + // Programs that need mode/permissions + "chmod" => vec![ + OsString::from(program), + OsString::from("644"), + path_os.to_owned(), + ], + "chown" => vec![ + OsString::from(program), + OsString::from("root:root"), + path_os.to_owned(), + ], + "chgrp" => vec![ + OsString::from(program), + OsString::from("root"), + path_os.to_owned(), + ], + "chcon" => vec![ + OsString::from(program), + OsString::from("system_u:object_r:admin_home_t:s0"), + path_os.to_owned(), + ], + "runcon" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from("system_u:object_r:admin_home_t:s0"), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + } + // Programs that need source and destination + "cp" | "mv" | "ln" | "link" => { + let dest_path = path.with_extension("dest"); + vec![ + OsString::from(program), + path_os.to_owned(), + dest_path.as_os_str().to_owned(), + ] + } + "install" => { + let dest_path = path.with_extension("dest"); + vec![ + OsString::from(program), + path_os.to_owned(), + dest_path.as_os_str().to_owned(), + ] + } + // Programs that need size/truncate operations + "truncate" => vec![ + OsString::from(program), + OsString::from("--size=0"), + path_os.to_owned(), + ], + "split" => vec![ + OsString::from(program), + path_os.to_owned(), + OsString::from("split_prefix_"), + ], + "csplit" => vec![ + OsString::from(program), + path_os.to_owned(), + OsString::from("1"), + ], + // File creation programs + "mkfifo" | "mknod" => { + let new_path = path.with_extension("new"); + if program == "mknod" { + vec![ + OsString::from(program), + new_path.as_os_str().to_owned(), + OsString::from("c"), + OsString::from("1"), + OsString::from("3"), + ] + } else { + vec![OsString::from(program), new_path.as_os_str().to_owned()] + } + } + "dd" => vec![ + OsString::from(program), + OsString::from(format!("if={}", path_os.to_string_lossy())), + OsString::from("of=/dev/null"), + OsString::from("bs=1"), + OsString::from("count=1"), + ], + // Hashsum needs algorithm + "hashsum" => vec![ + OsString::from(program), + OsString::from("--md5"), + path_os.to_owned(), + ], + // Encoding/decoding programs + "base32" | "base64" | "basenc" => vec![OsString::from(program), path_os.to_owned()], + "df" => vec![OsString::from(program), path_os.to_owned()], + "chroot" => { + // chroot needs a directory and command + vec![ + OsString::from(program), + path_os.to_owned(), + OsString::from("true"), + ] + } + "sync" => vec![OsString::from(program), path_os.to_owned()], + "stty" => vec![ + OsString::from(program), + OsString::from("-F"), + path_os.to_owned(), + ], + "tty" => vec![OsString::from(program)], // tty doesn't take file args, but test anyway + "env" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + } + "nohup" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + } + "nice" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + } + "timeout" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from("1"), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + } + "stdbuf" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from("-o0"), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + } + // Programs that work with multiple files (use just one for testing) + "comm" | "join" => { + // These need two files, use the same file twice for simplicity + vec![ + OsString::from(program), + path_os.to_owned(), + path_os.to_owned(), + ] + } + // Programs that typically take file input + _ => vec![OsString::from(program), path_os.to_owned()], + }; + + // Try to run the local uutils version + match run_gnu_cmd(&local_binary, &local_args, false, None) { + Ok(result) => result, + Err(error_result) => { + // Local command failed, return the error + error_result + } + } +} + +fn cleanup_test_files(temp_root: &PathBuf) { + let _ = fs::remove_dir_all(temp_root); +} + +fn check_for_utf8_error_and_panic(result: &CommandResult, program: &str, path: &PathBuf) { + let stderr_lower = result.stderr.to_lowercase(); + let is_utf8_error = stderr_lower.contains("invalid utf-8") + || stderr_lower.contains("not valid unicode") + || stderr_lower.contains("invalid utf8") + || stderr_lower.contains("utf-8 error"); + + if is_utf8_error { + println!( + "UTF-8 conversion error detected in {}: {}", + program, result.stderr + ); + println!("Path: {:?}", path); + println!("Exit code: {}", result.exit_code); + panic!( + "FUZZER FAILURE: {} failed with UTF-8 error on non-UTF-8 path: {:?}", + program, path + ); + } +} + +fuzz_target!(|_data: &[u8]| { + let mut rng = rand::rng(); + + // Set up test environment + let (temp_root, test_files) = match setup_test_files() { + Ok(files) => files, + Err(_) => return, // Skip if we can't set up test files + }; + + // Pick multiple random programs to test in each iteration + let num_programs_to_test = rng.random_range(1..=3); // Test 1-3 programs per iteration + let mut tested_programs = HashSet::new(); + + let mut programs_tested = Vec::::new(); + + for _ in 0..num_programs_to_test { + // Pick a random program that we haven't tested yet in this iteration + let available_programs: Vec<_> = PATH_PROGRAMS + .iter() + .filter(|p| !tested_programs.contains(*p)) + .collect(); + + if available_programs.is_empty() { + break; + } + + let program = available_programs.choose(&mut rng).unwrap(); + tested_programs.insert(*program); + programs_tested.push(program.to_string()); + + // Test with one random file that has non-UTF-8 names (not all files to speed up) + if let Some(test_file) = test_files.choose(&mut rng) { + let result = test_program_with_non_utf8_path(program, test_file); + + // Check if the program handled the non-UTF-8 path gracefully + check_for_utf8_error_and_panic(&result, program, test_file); + } + + // Special cases for programs that need additional testing + if **program == "mkdir" || **program == "mktemp" { + let non_utf8_dir_name = generate_non_utf8_osstring(); + let non_utf8_dir = temp_root.join(non_utf8_dir_name); + + let local_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + let mkdir_args = vec![OsString::from("mkdir"), non_utf8_dir.as_os_str().to_owned()]; + + let mkdir_result = run_gnu_cmd(&local_binary, &mkdir_args, false, None); + match mkdir_result { + Ok(result) => { + check_for_utf8_error_and_panic(&result, "mkdir", &non_utf8_dir); + } + Err(error) => { + check_for_utf8_error_and_panic(&error, "mkdir", &non_utf8_dir); + } + } + } + } + + println!("Tested programs: {}", programs_tested.join(", ")); + + // Clean up + cleanup_test_files(&temp_root); +}); diff --git a/fuzz/fuzz_targets/fuzz_parse_glob.rs b/fuzz/fuzz_targets/fuzz_parse_glob.rs index e235c0c9d89..66e772959e7 100644 --- a/fuzz/fuzz_targets/fuzz_parse_glob.rs +++ b/fuzz/fuzz_targets/fuzz_parse_glob.rs @@ -1,7 +1,7 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use uucore::parse_glob; +use uucore::parser::parse_glob; fuzz_target!(|data: &[u8]| { if let Ok(s) = std::str::from_utf8(data) { diff --git a/fuzz/fuzz_targets/fuzz_parse_size.rs b/fuzz/fuzz_targets/fuzz_parse_size.rs index d032adf0666..4e8d7e2216b 100644 --- a/fuzz/fuzz_targets/fuzz_parse_size.rs +++ b/fuzz/fuzz_targets/fuzz_parse_size.rs @@ -1,7 +1,7 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use uucore::parse_size::parse_size_u64; +use uucore::parser::parse_size::parse_size_u64; fuzz_target!(|data: &[u8]| { if let Ok(s) = std::str::from_utf8(data) { diff --git a/fuzz/fuzz_targets/fuzz_parse_time.rs b/fuzz/fuzz_targets/fuzz_parse_time.rs index a643c6d805c..5745e5c8709 100644 --- a/fuzz/fuzz_targets/fuzz_parse_time.rs +++ b/fuzz/fuzz_targets/fuzz_parse_time.rs @@ -1,10 +1,11 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use uucore::parse_time; +use uucore::parser::parse_time; fuzz_target!(|data: &[u8]| { if let Ok(s) = std::str::from_utf8(data) { - _ = parse_time::from_str(s); + _ = parse_time::from_str(s, true); + _ = parse_time::from_str(s, false); } }); diff --git a/fuzz/fuzz_targets/fuzz_printf.rs b/fuzz/fuzz_targets/fuzz_printf.rs index cb2d90ed531..885ebb815bf 100644 --- a/fuzz/fuzz_targets/fuzz_printf.rs +++ b/fuzz/fuzz_targets/fuzz_printf.rs @@ -8,16 +8,13 @@ use libfuzzer_sys::fuzz_target; use uu_printf::uumain; -use rand::seq::SliceRandom; use rand::Rng; +use rand::seq::IndexedRandom; use std::env; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::CommandResult; -use crate::fuzz_common::{ - compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, -}; +use uufuzz::CommandResult; +use uufuzz::{compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd}; static CMD_PATH: &str = "printf"; @@ -44,34 +41,34 @@ fn generate_escape_sequence(rng: &mut impl Rng) -> String { } fn generate_printf() -> String { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let format_specifiers = ["%s", "%d", "%f", "%x", "%o", "%c", "%b", "%q"]; let mut printf_str = String::new(); // Add a 20% chance of generating an invalid format specifier - if rng.gen_bool(0.2) { + if rng.random_bool(0.2) { printf_str.push_str("%z"); // Invalid format specifier } else { let specifier = *format_specifiers.choose(&mut rng).unwrap(); printf_str.push_str(specifier); // Add a 20% chance of introducing complex format strings - if rng.gen_bool(0.2) { - printf_str.push_str(&format!(" %{}", rng.gen_range(1..=1000))); + if rng.random_bool(0.2) { + printf_str.push_str(&format!(" %{}", rng.random_range(1..=1000))); } else { // Add a random string or number after the specifier if specifier == "%s" { printf_str.push_str(&format!( " {}", - generate_random_string(rng.gen_range(1..=10)) + generate_random_string(rng.random_range(1..=10)) )); } else { - printf_str.push_str(&format!(" {}", rng.gen_range(1..=1000))); + printf_str.push_str(&format!(" {}", rng.random_range(1..=1000))); } } } // Add a 10% chance of including an escape sequence - if rng.gen_bool(0.1) { + if rng.random_bool(0.1) { printf_str.push_str(&generate_escape_sequence(&mut rng)); } printf_str @@ -84,7 +81,9 @@ fuzz_target!(|_data: &[u8]| { let rust_result = generate_and_run_uumain(&args, uumain, None); // TODO remove once uutils printf supports localization - env::set_var("LC_ALL", "C"); + unsafe { + env::set_var("LC_ALL", "C"); + } let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false, None) { Ok(result) => result, Err(error_result) => { diff --git a/fuzz/fuzz_targets/fuzz_seq.rs b/fuzz/fuzz_targets/fuzz_seq.rs index 7bb4f8af956..35721865e8c 100644 --- a/fuzz/fuzz_targets/fuzz_seq.rs +++ b/fuzz/fuzz_targets/fuzz_seq.rs @@ -11,31 +11,28 @@ use uu_seq::uumain; use rand::Rng; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::CommandResult; -use crate::fuzz_common::{ - compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, -}; +use uufuzz::CommandResult; +use uufuzz::{compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd}; static CMD_PATH: &str = "seq"; fn generate_seq() -> String { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); // Generate 1 to 3 numbers for seq arguments - let arg_count = rng.gen_range(1..=3); + let arg_count = rng.random_range(1..=3); let mut args = Vec::new(); for _ in 0..arg_count { - if rng.gen_ratio(1, 100) { + if rng.random_ratio(1, 100) { // 1% chance to add a random string - args.push(generate_random_string(rng.gen_range(1..=10))); + args.push(generate_random_string(rng.random_range(1..=10))); } else { // 99% chance to add a numeric value - match rng.gen_range(0..=3) { - 0 => args.push(rng.gen_range(-10000..=10000).to_string()), // Large or small integers - 1 => args.push(rng.gen_range(-100.0..100.0).to_string()), // Floating-point numbers - 2 => args.push(rng.gen_range(-100..0).to_string()), // Negative integers - _ => args.push(rng.gen_range(1..=100).to_string()), // Regular integers + match rng.random_range(0..=3) { + 0 => args.push(rng.random_range(-10000..=10000).to_string()), // Large or small integers + 1 => args.push(rng.random_range(-100.0..100.0).to_string()), // Floating-point numbers + 2 => args.push(rng.random_range(-100..0).to_string()), // Negative integers + _ => args.push(rng.random_range(1..=100).to_string()), // Regular integers } } } diff --git a/fuzz/fuzz_targets/fuzz_seq_parse_number.rs b/fuzz/fuzz_targets/fuzz_seq_parse_number.rs new file mode 100644 index 00000000000..04da6d47f99 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_seq_parse_number.rs @@ -0,0 +1,15 @@ +// 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. +#![no_main] + +use libfuzzer_sys::fuzz_target; +use std::str::FromStr; +use uu_seq::number::PreciseNumber; + +fuzz_target!(|data: &[u8]| { + if let Ok(s) = std::str::from_utf8(data) { + let _ = PreciseNumber::from_str(s); + } +}); diff --git a/fuzz/fuzz_targets/fuzz_sort.rs b/fuzz/fuzz_targets/fuzz_sort.rs index 9bb7df35767..8b38f39ec1b 100644 --- a/fuzz/fuzz_targets/fuzz_sort.rs +++ b/fuzz/fuzz_targets/fuzz_sort.rs @@ -12,26 +12,23 @@ use rand::Rng; use std::env; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::CommandResult; -use crate::fuzz_common::{ - compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, -}; +use uufuzz::CommandResult; +use uufuzz::{compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd}; static CMD_PATH: &str = "sort"; fn generate_sort_args() -> String { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); - let arg_count = rng.gen_range(1..=5); + let arg_count = rng.random_range(1..=5); let mut args = Vec::new(); for _ in 0..arg_count { - match rng.gen_range(0..=4) { + match rng.random_range(0..=4) { 0 => args.push(String::from("-r")), // Reverse the result of comparisons 1 => args.push(String::from("-n")), // Compare according to string numerical value 2 => args.push(String::from("-f")), // Fold lower case to upper case characters - 3 => args.push(generate_random_string(rng.gen_range(1..=10))), // Random string (to simulate file names) - _ => args.push(String::from("-k") + &rng.gen_range(1..=5).to_string()), // Sort via a specified field + 3 => args.push(generate_random_string(rng.random_range(1..=10))), // Random string (to simulate file names) + _ => args.push(String::from("-k") + &rng.random_range(1..=5).to_string()), // Sort via a specified field } } @@ -39,11 +36,11 @@ fn generate_sort_args() -> String { } fn generate_random_lines(count: usize) -> String { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let mut lines = Vec::new(); for _ in 0..count { - lines.push(generate_random_string(rng.gen_range(1..=20))); + lines.push(generate_random_string(rng.random_range(1..=20))); } lines.join("\n") @@ -60,7 +57,9 @@ fuzz_target!(|_data: &[u8]| { let rust_result = generate_and_run_uumain(&args, uumain, Some(&input_lines)); // TODO remove once uutils sort supports localization - env::set_var("LC_ALL", "C"); + unsafe { + env::set_var("LC_ALL", "C"); + } let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false, Some(&input_lines)) { Ok(result) => result, Err(error_result) => { diff --git a/fuzz/fuzz_targets/fuzz_split.rs b/fuzz/fuzz_targets/fuzz_split.rs index 876c8dd21d4..473d86f575f 100644 --- a/fuzz/fuzz_targets/fuzz_split.rs +++ b/fuzz/fuzz_targets/fuzz_split.rs @@ -11,20 +11,19 @@ use uu_split::uumain; use rand::Rng; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::{ - compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, CommandResult, +use uufuzz::{ + CommandResult, compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, }; static CMD_PATH: &str = "split"; fn generate_split_args() -> String { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let mut args = Vec::new(); - match rng.gen_range(0..=9) { + match rng.random_range(0..=9) { 0 => { args.push(String::from("-a")); // Suffix length - args.push(rng.gen_range(1..=8).to_string()); + args.push(rng.random_range(1..=8).to_string()); } 1 => { args.push(String::from("--additional-suffix")); @@ -32,17 +31,17 @@ fn generate_split_args() -> String { } 2 => { args.push(String::from("-b")); // Bytes per output file - args.push(rng.gen_range(1..=1024).to_string() + "K"); + args.push(rng.random_range(1..=1024).to_string() + "K"); } 3 => { args.push(String::from("-C")); // Line bytes - args.push(rng.gen_range(1..=1024).to_string()); + args.push(rng.random_range(1..=1024).to_string()); } 4 => args.push(String::from("-d")), // Use numeric suffixes 5 => args.push(String::from("-x")), // Use hex suffixes 6 => { args.push(String::from("-l")); // Number of lines per output file - args.push(rng.gen_range(1..=1000).to_string()); + args.push(rng.random_range(1..=1000).to_string()); } 7 => { args.push(String::from("--filter")); @@ -59,13 +58,13 @@ fn generate_split_args() -> String { args.join(" ") } -// Function to generate a random string of lines +/// Function to generate a random string of lines fn generate_random_lines(count: usize) -> String { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let mut lines = Vec::new(); for _ in 0..count { - lines.push(generate_random_string(rng.gen_range(1..=20))); + lines.push(generate_random_string(rng.random_range(1..=20))); } lines.join("\n") diff --git a/fuzz/fuzz_targets/fuzz_test.rs b/fuzz/fuzz_targets/fuzz_test.rs index bed7ca77088..894a1dcd56b 100644 --- a/fuzz/fuzz_targets/fuzz_test.rs +++ b/fuzz/fuzz_targets/fuzz_test.rs @@ -8,16 +8,14 @@ use libfuzzer_sys::fuzz_target; use uu_test::uumain; -use rand::seq::SliceRandom; use rand::Rng; +use rand::prelude::IndexedRandom; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::CommandResult; -use crate::fuzz_common::{ - compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, -}; +use uufuzz::CommandResult; +use uufuzz::{compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd}; +#[allow(clippy::upper_case_acronyms)] #[derive(PartialEq, Debug, Clone)] enum ArgType { STRING, @@ -38,7 +36,7 @@ struct TestArg { } fn generate_random_path(rng: &mut dyn rand::RngCore) -> &'static str { - match rng.gen_range(0..=3) { + match rng.random_range(0..=3) { 0 => "/dev/null", 1 => "/dev/random", 2 => "/tmp", @@ -64,6 +62,14 @@ fn generate_test_args() -> Vec { arg: "!=".to_string(), arg_type: ArgType::STRINGSTRING, }, + TestArg { + arg: ">".to_string(), + arg_type: ArgType::STRINGSTRING, + }, + TestArg { + arg: "<".to_string(), + arg_type: ArgType::STRINGSTRING, + }, TestArg { arg: "-eq".to_string(), arg_type: ArgType::INTEGERINTEGER, @@ -112,15 +118,15 @@ fn generate_test_args() -> Vec { } fn generate_test_arg() -> String { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let test_args = generate_test_args(); let mut arg = String::new(); - let choice = rng.gen_range(0..=5); + let choice = rng.random_range(0..=5); match choice { 0 => { - arg.push_str(&rng.gen_range(-100..=100).to_string()); + arg.push_str(&rng.random_range(-100..=100).to_string()); } 1..=3 => { let test_arg = test_args @@ -129,32 +135,29 @@ fn generate_test_arg() -> String { if test_arg.arg_type == ArgType::INTEGER { arg.push_str(&format!( "{} {} {}", - &rng.gen_range(-100..=100).to_string(), + rng.random_range(-100..=100).to_string(), test_arg.arg, - &rng.gen_range(-100..=100).to_string() + rng.random_range(-100..=100).to_string() )); } else if test_arg.arg_type == ArgType::STRINGSTRING { - let random_str = generate_random_string(rng.gen_range(1..=10)); - let random_str2 = generate_random_string(rng.gen_range(1..=10)); + let random_str = generate_random_string(rng.random_range(1..=10)); + let random_str2 = generate_random_string(rng.random_range(1..=10)); - arg.push_str(&format!( - "{} {} {}", - &random_str, test_arg.arg, &random_str2 - )); + arg.push_str(&format!("{random_str} {} {random_str2}", test_arg.arg,)); } else if test_arg.arg_type == ArgType::STRING { - let random_str = generate_random_string(rng.gen_range(1..=10)); - arg.push_str(&format!("{} {}", test_arg.arg, &random_str)); + let random_str = generate_random_string(rng.random_range(1..=10)); + arg.push_str(&format!("{} {random_str}", test_arg.arg)); } else if test_arg.arg_type == ArgType::FILEFILE { let path = generate_random_path(&mut rng); let path2 = generate_random_path(&mut rng); - arg.push_str(&format!("{} {} {}", path, test_arg.arg, path2)); + arg.push_str(&format!("{path} {} {path2}", test_arg.arg)); } else if test_arg.arg_type == ArgType::FILE { let path = generate_random_path(&mut rng); - arg.push_str(&format!("{} {}", test_arg.arg, path)); + arg.push_str(&format!("{} {path}", test_arg.arg)); } } 4 => { - let random_str = generate_random_string(rng.gen_range(1..=10)); + let random_str = generate_random_string(rng.random_range(1..=10)); arg.push_str(&random_str); } _ => { @@ -167,7 +170,7 @@ fn generate_test_arg() -> String { .collect(); if let Some(test_arg) = file_test_args.choose(&mut rng) { - arg.push_str(&format!("{}{}", test_arg.arg, path)); + arg.push_str(&format!("{}{path}", test_arg.arg)); } } } @@ -176,8 +179,8 @@ fn generate_test_arg() -> String { } fuzz_target!(|_data: &[u8]| { - let mut rng = rand::thread_rng(); - let max_args = rng.gen_range(1..=6); + let mut rng = rand::rng(); + let max_args = rng.random_range(1..=6); let mut args = vec![OsString::from("test")]; for _ in 0..max_args { diff --git a/fuzz/fuzz_targets/fuzz_tr.rs b/fuzz/fuzz_targets/fuzz_tr.rs new file mode 100644 index 00000000000..5055ec0d748 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_tr.rs @@ -0,0 +1,72 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +#![no_main] +use libfuzzer_sys::fuzz_target; +use std::ffi::OsString; +use uu_tr::uumain; + +use rand::Rng; + +use uufuzz::{ + CommandResult, compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, +}; +static CMD_PATH: &str = "tr"; + +fn generate_tr_args() -> Vec { + let mut rng = rand::rng(); + let mut args = Vec::new(); + + // Translate, squeeze, and/or delete characters + let opts = ["-c", "-d", "-s", "-t"]; + for opt in &opts { + if rng.random_bool(0.25) { + args.push(opt.to_string()); + } + } + + // Generating STRING1 and optionally STRING2 + let string1 = generate_random_string(rng.random_range(1..=20)); + args.push(string1); + if rng.random_bool(0.7) { + // Higher chance to add STRING2 for translation + let string2 = generate_random_string(rng.random_range(1..=20)); + args.push(string2); + } + + args +} + +fuzz_target!(|_data: &[u8]| { + let tr_args = generate_tr_args(); + let mut args = vec![OsString::from("tr")]; + args.extend(tr_args.iter().map(OsString::from)); + + let input_chars = generate_random_string(100); + + let rust_result = generate_and_run_uumain(&args, uumain, Some(&input_chars)); + let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false, Some(&input_chars)) { + Ok(result) => result, + Err(error_result) => { + eprintln!("Failed to run GNU command:"); + eprintln!("Stderr: {}", error_result.stderr); + eprintln!("Exit Code: {}", error_result.exit_code); + CommandResult { + stdout: String::new(), + stderr: error_result.stderr, + exit_code: error_result.exit_code, + } + } + }; + + compare_result( + "tr", + &format!("{:?}", &args[1..]), + Some(&input_chars), + &rust_result, + &gnu_result, + false, + ); +}); diff --git a/fuzz/fuzz_targets/fuzz_wc.rs b/fuzz/fuzz_targets/fuzz_wc.rs index dc85bbc3541..148ecdda1fc 100644 --- a/fuzz/fuzz_targets/fuzz_wc.rs +++ b/fuzz/fuzz_targets/fuzz_wc.rs @@ -11,23 +11,22 @@ use uu_wc::uumain; use rand::Rng; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::{ - compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, CommandResult, +use uufuzz::{ + CommandResult, compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, }; static CMD_PATH: &str = "wc"; fn generate_wc_args() -> String { - let mut rng = rand::thread_rng(); - let arg_count = rng.gen_range(1..=6); + let mut rng = rand::rng(); + let arg_count = rng.random_range(1..=6); let mut args = Vec::new(); for _ in 0..arg_count { // Introduce a chance to add invalid arguments - if rng.gen_bool(0.1) { - args.push(generate_random_string(rng.gen_range(1..=20))); + if rng.random_bool(0.1) { + args.push(generate_random_string(rng.random_range(1..=20))); } else { - match rng.gen_range(0..=5) { + match rng.random_range(0..=5) { 0 => args.push(String::from("-c")), 1 => args.push(String::from("-m")), 2 => args.push(String::from("-l")), @@ -36,7 +35,7 @@ fn generate_wc_args() -> String { // TODO 5 => { args.push(String::from("--files0-from")); - if rng.gen_bool(0.5) { + if rng.random_bool(0.5) { args.push(generate_random_string(50)); // Longer invalid file name } else { args.push(generate_random_string(5)); @@ -50,16 +49,16 @@ fn generate_wc_args() -> String { args.join(" ") } -// Function to generate a random string of lines, including invalid ones +/// Function to generate a random string of lines, including invalid ones fn generate_random_lines(count: usize) -> String { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let mut lines = Vec::new(); for _ in 0..count { - if rng.gen_bool(0.1) { - lines.push(generate_random_string(rng.gen_range(1000..=5000))); // Very long invalid line + if rng.random_bool(0.1) { + lines.push(generate_random_string(rng.random_range(1000..=5000))); // Very long invalid line } else { - lines.push(generate_random_string(rng.gen_range(1..=20))); + lines.push(generate_random_string(rng.random_range(1..=20))); } } diff --git a/fuzz/uufuzz/Cargo.toml b/fuzz/uufuzz/Cargo.toml new file mode 100644 index 00000000000..971d30265ce --- /dev/null +++ b/fuzz/uufuzz/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "uufuzz" +authors = ["uutils developers"] +description = "uutils ~ 'core' uutils fuzzing library" +repository = "https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz" +version = "0.1.0" +edition.workspace = true +license.workspace = true + +[dependencies] +console = "0.16.0" +libc = "0.2.153" +rand = { version = "0.9.0", features = ["small_rng"] } +similar = "2.5.0" +uucore = { path = "../../src/uucore", features = ["parser"] } +tempfile = "3.15.0" diff --git a/fuzz/fuzz_targets/fuzz_common.rs b/fuzz/uufuzz/src/lib.rs similarity index 78% rename from fuzz/fuzz_targets/fuzz_common.rs rename to fuzz/uufuzz/src/lib.rs index cf56268d75a..e887bfc6755 100644 --- a/fuzz/fuzz_targets/fuzz_common.rs +++ b/fuzz/uufuzz/src/lib.rs @@ -3,19 +3,26 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +use console::Style; use libc::STDIN_FILENO; -use libc::{close, dup, dup2, pipe, STDERR_FILENO, STDOUT_FILENO}; -use rand::prelude::SliceRandom; +use libc::{STDERR_FILENO, STDOUT_FILENO, close, dup, dup2, pipe}; +use pretty_print::{ + print_diff, print_end_with_status, print_or_empty, print_section, print_with_style, +}; use rand::Rng; -use similar::TextDiff; +use rand::prelude::IndexedRandom; +use std::env::temp_dir; use std::ffi::OsString; +use std::fs::File; use std::io::{Seek, SeekFrom, Write}; use std::os::fd::{AsRawFd, RawFd}; use std::process::{Command, Stdio}; use std::sync::atomic::Ordering; -use std::sync::{atomic::AtomicBool, Once}; +use std::sync::{Once, atomic::AtomicBool}; use std::{io, thread}; +pub mod pretty_print; + /// Represents the result of running a command, including its standard output, /// standard error, and exit code. pub struct CommandResult { @@ -36,7 +43,7 @@ pub fn is_gnu_cmd(cmd_path: &str) -> Result<(), std::io::Error> { CHECK_GNU.call_once(|| { let version_output = Command::new(cmd_path).arg("--version").output().unwrap(); - println!("version_output {:#?}", version_output); + println!("version_output {version_output:#?}"); let version_str = String::from_utf8_lossy(&version_output.stdout).to_string(); if version_str.contains("GNU coreutils") { @@ -105,7 +112,7 @@ where let original_stdin_fd = if let Some(input_str) = pipe_input { // we have pipe input let mut input_file = tempfile::tempfile().unwrap(); - write!(input_file, "{}", input_str).unwrap(); + write!(input_file, "{input_str}").unwrap(); input_file.seek(SeekFrom::Start(0)).unwrap(); // Redirect stdin to read from the in-memory file @@ -125,6 +132,8 @@ where let (uumain_exit_status, captured_stdout, captured_stderr) = thread::scope(|s| { let out = s.spawn(|| read_from_fd(pipe_stdout_fds[0])); let err = s.spawn(|| read_from_fd(pipe_stderr_fds[0])); + #[allow(clippy::unnecessary_to_owned)] + // TODO: clippy wants us to use args.iter().cloned() ? let status = uumain_function(args.to_owned().into_iter()); // Reset the exit code global variable in case we run another test after this one // See https://github.com/uutils/coreutils/issues/5777 @@ -231,6 +240,10 @@ pub fn run_gnu_cmd( command.arg(arg); } + // See https://github.com/uutils/coreutils/issues/6794 + // uutils' coreutils is not locale-aware, and aims to mirror/be compatible with GNU Core Utilities's LC_ALL=C behavior + command.env("LC_ALL", "C"); + let output = if let Some(input_str) = pipe_input { // We have an pipe input command @@ -309,10 +322,10 @@ pub fn compare_result( gnu_result: &CommandResult, fail_on_stderr_diff: bool, ) { - println!("Test Type: {}", test_type); - println!("Input: {}", input); + print_section(format!("Compare result for: {test_type} {input}")); + if let Some(pipe) = pipe_input { - println!("Pipe: {}", pipe); + println!("Pipe: {pipe}"); } let mut discrepancies = Vec::new(); @@ -320,62 +333,68 @@ pub fn compare_result( if rust_result.stdout.trim() != gnu_result.stdout.trim() { discrepancies.push("stdout differs"); - println!("Rust stdout: {}", rust_result.stdout); - println!("GNU stdout: {}", gnu_result.stdout); + println!("Rust stdout:"); + print_or_empty(rust_result.stdout.as_str()); + println!("GNU stdout:"); + print_or_empty(gnu_result.stdout.as_ref()); print_diff(&rust_result.stdout, &gnu_result.stdout); should_panic = true; } + if rust_result.stderr.trim() != gnu_result.stderr.trim() { discrepancies.push("stderr differs"); - println!("Rust stderr: {}", rust_result.stderr); - println!("GNU stderr: {}", gnu_result.stderr); + println!("Rust stderr:"); + print_or_empty(rust_result.stderr.as_str()); + println!("GNU stderr:"); + print_or_empty(gnu_result.stderr.as_str()); print_diff(&rust_result.stderr, &gnu_result.stderr); if fail_on_stderr_diff { should_panic = true; } } + if rust_result.exit_code != gnu_result.exit_code { discrepancies.push("exit code differs"); - println!("Rust exit code: {}", rust_result.exit_code); - println!("GNU exit code: {}", gnu_result.exit_code); + println!( + "Different exit code: (Rust: {}, GNU: {})", + rust_result.exit_code, gnu_result.exit_code + ); should_panic = true; } if discrepancies.is_empty() { - println!("All outputs and exit codes matched."); + print_end_with_status("Same behavior", true); } else { - println!("Discrepancy detected: {}", discrepancies.join(", ")); + print_with_style( + format!("Discrepancies detected: {}", discrepancies.join(", ")), + Style::new().red(), + ); if should_panic { - panic!("Test failed for {}: {}", test_type, input); + print_end_with_status( + format!("Test failed and will panic for: {test_type} {input}"), + false, + ); + panic!("Test failed for: {test_type} {input}"); } else { - println!( - "Test completed with discrepancies for {}: {}", - test_type, input + print_end_with_status( + format!("Test completed with discrepancies for: {test_type} {input}"), + false, ); } } -} - -/// When we have different outputs, print the diff -fn print_diff(rust_output: &str, gnu_output: &str) { - println!("Diff="); - let diff = TextDiff::from_lines(rust_output, gnu_output); - for change in diff.iter_all_changes() { - print!("{}{}", change.tag(), change); - } println!(); } pub fn generate_random_string(max_length: usize) -> String { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let valid_utf8: Vec = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" .chars() .collect(); let invalid_utf8 = [0xC3, 0x28]; // Invalid UTF-8 sequence let mut result = String::new(); - for _ in 0..rng.gen_range(1..=max_length) { - if rng.gen_bool(0.9) { + for _ in 0..rng.random_range(0..=max_length) { + if rng.random_bool(0.9) { let ch = valid_utf8.choose(&mut rng).unwrap(); result.push(*ch); } else { @@ -388,3 +407,32 @@ pub fn generate_random_string(max_length: usize) -> String { result } + +#[allow(dead_code)] +pub fn generate_random_file() -> Result { + let mut rng = rand::rng(); + let file_name: String = (0..10) + .map(|_| rng.random_range(b'a'..=b'z') as char) + .collect(); + let mut file_path = temp_dir(); + file_path.push(file_name); + + let mut file = File::create(&file_path)?; + + let content_length = rng.random_range(10..1000); + let content: String = (0..content_length) + .map(|_| (rng.random_range(b' '..=b'~') as char)) + .collect(); + + file.write_all(content.as_bytes())?; + + Ok(file_path.to_str().unwrap().to_string()) +} + +#[allow(dead_code)] +pub fn replace_fuzz_binary_name(cmd: &str, result: &mut CommandResult) { + let fuzz_bin_name = format!("fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_{cmd}"); + + result.stdout = result.stdout.replace(&fuzz_bin_name, cmd); + result.stderr = result.stderr.replace(&fuzz_bin_name, cmd); +} diff --git a/fuzz/uufuzz/src/pretty_print.rs b/fuzz/uufuzz/src/pretty_print.rs new file mode 100644 index 00000000000..ecdfccfd035 --- /dev/null +++ b/fuzz/uufuzz/src/pretty_print.rs @@ -0,0 +1,69 @@ +// 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 std::fmt; + +use console::{Style, style}; +use similar::TextDiff; + +pub fn print_section(s: S) { + println!("{}", style(format!("=== {s}")).bold()); +} + +pub fn print_subsection(s: S) { + println!("{}", style(format!("--- {s}")).bright()); +} + +#[allow(dead_code)] +pub fn print_test_begin(msg: S) { + println!( + "{} {} {}", + style("===").bold(), // Kind of gray + style("TEST").black().on_yellow().bold(), + style(msg).bold() + ); +} + +pub fn print_end_with_status(msg: S, ok: bool) { + let ok = if ok { + style(" OK ").black().on_green().bold() + } else { + style(" KO ").black().on_red().bold() + }; + + println!( + "{} {ok} {}", + style("===").bold(), // Kind of gray + style(msg).bold() + ); +} + +pub fn print_or_empty(s: &str) { + let to_print = if s.is_empty() { "(empty)" } else { s }; + + println!("{}", style(to_print).dim()); +} + +pub fn print_with_style(msg: S, style: Style) { + println!("{}", style.apply_to(msg)); +} + +pub fn print_diff(got: &str, expected: &str) { + let diff = TextDiff::from_lines(got, expected); + + print_subsection("START diff"); + + for change in diff.iter_all_changes() { + let (sign, style) = match change.tag() { + similar::ChangeTag::Equal => (" ", Style::new().dim()), + similar::ChangeTag::Delete => ("-", Style::new().red()), + similar::ChangeTag::Insert => ("+", Style::new().green()), + }; + print!("{}{}", style.apply_to(sign).bold(), style.apply_to(change)); + } + + print_subsection("END diff"); + println!(); +} diff --git a/renovate.json b/renovate.json index 9dc8c691977..8f60343a8ef 100644 --- a/renovate.json +++ b/renovate.json @@ -1,5 +1,5 @@ { "extends": [ - "config:base" + "config:recommended" ] } diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index fc2cd16add2..64a79a3fd1e 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.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 manpages mangen +// spell-checker:ignore manpages mangen prefixcat testcat use clap::{Arg, Command}; use clap_complete::Shell; @@ -14,6 +14,7 @@ use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::process; use uucore::display::Quotable; +use uucore::locale; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -21,7 +22,10 @@ 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...]]\n"); + println!("Usage: {name} [function [arguments...]]"); + println!(" {name} --list\n"); + println!("Options:"); + println!(" --list lists all defined functions, one per row\n"); println!("Currently defined functions:\n"); #[allow(clippy::map_clone)] let mut utils: Vec<&str> = utils.keys().map(|&s| s).collect(); @@ -34,6 +38,8 @@ fn usage(utils: &UtilityMap, name: &str) { ); } +/// # Panics +/// Panics if the binary path cannot be determined fn binary_path(args: &mut impl Iterator) -> PathBuf { match args.next() { Some(ref s) if !s.is_empty() => PathBuf::from(s), @@ -45,6 +51,48 @@ fn name(binary_path: &Path) -> Option<&str> { binary_path.file_stem()?.to_str() } +fn get_canonical_util_name(util_name: &str) -> &str { + match util_name { + // uu_test aliases - '[' is an alias for test + "[" => "test", + + // hashsum aliases - all these hash commands are aliases for hashsum + "md5sum" | "sha1sum" | "sha224sum" | "sha256sum" | "sha384sum" | "sha512sum" + | "sha3sum" | "sha3-224sum" | "sha3-256sum" | "sha3-384sum" | "sha3-512sum" + | "shake128sum" | "shake256sum" | "b2sum" | "b3sum" => "hashsum", + + "dir" => "ls", // dir is an alias for ls + + // Default case - return the util name as is + _ => util_name, + } +} + +fn find_prefixed_util<'a>( + binary_name: &str, + mut util_keys: impl Iterator, +) -> Option<&'a str> { + util_keys.find(|util| { + binary_name.ends_with(*util) + && binary_name.len() > util.len() // Ensure there's actually a prefix + && !binary_name[..binary_name.len() - (*util).len()] + .ends_with(char::is_alphanumeric) + }) +} + +fn setup_localization_or_exit(util_name: &str) { + locale::setup_localization(get_canonical_util_name(util_name)).unwrap_or_else(|err| { + match err { + uucore::locale::LocalizationError::ParseResource { + error: err_msg, + snippet, + } => eprintln!("Localization parse error at {snippet}: {err_msg}"), + other => eprintln!("Could not init the localization system: {other}"), + } + process::exit(99) + }); +} + #[allow(clippy::cognitive_complexity)] fn main() { uucore::panic::mute_sigpipe_panic(); @@ -60,18 +108,16 @@ fn main() { // binary name equals util name? if let Some(&(uumain, _)) = utils.get(binary_as_util) { - process::exit(uumain((vec![binary.into()].into_iter()).chain(args))); + setup_localization_or_exit(binary_as_util); + process::exit(uumain(vec![binary.into()].into_iter().chain(args))); } // binary name equals prefixed util name? // * prefix/stem may be any string ending in a non-alphanumeric character - let util_name = if let Some(util) = utils.keys().find(|util| { - binary_as_util.ends_with(*util) - && !binary_as_util[..binary_as_util.len() - (*util).len()] - .ends_with(char::is_alphanumeric) - }) { + // For example, if the binary is named `uu_test`, it will match `test` as a utility. + let util_name = if let Some(util) = find_prefixed_util(binary_as_util, utils.keys().copied()) { // prefixed util => replace 0th (aka, executable name) argument - Some(OsString::from(*util)) + Some(OsString::from(util)) } else { // unmatched binary name => regard as multi-binary container and advance argument list uucore::set_utility_is_second_arg(); @@ -85,36 +131,52 @@ fn main() { process::exit(1); } - let util = match util_os.to_str() { - Some(util) => util, - None => not_found(&util_os), + let Some(util) = util_os.to_str() else { + not_found(&util_os) }; - if util == "completion" { - gen_completions(args, &utils); - } - - if util == "manpage" { - gen_manpage(args, &utils); + match util { + "completion" => gen_completions(args, &utils), + "manpage" => gen_manpage(args, &utils), + "--list" => { + let mut utils: Vec<_> = utils.keys().collect(); + utils.sort(); + for util in utils { + println!("{util}"); + } + process::exit(0); + } + "--version" | "-V" => { + println!("{binary_as_util} {VERSION} (multi-call binary)"); + process::exit(0); + } + // Not a special command: fallthrough to calling a util + _ => {} } match utils.get(util) { Some(&(uumain, _)) => { - process::exit(uumain((vec![util_os].into_iter()).chain(args))); + // TODO: plug the deactivation of the translation + // and load the English strings directly at compilation time in the + // binary to avoid the load of the flt + // Could be something like: + // #[cfg(not(feature = "only_english"))] + setup_localization_or_exit(util); + process::exit(uumain(vec![util_os].into_iter().chain(args))); } None => { if util == "--help" || util == "-h" { // see if they want help on a specific util if let Some(util_os) = args.next() { - let util = match util_os.to_str() { - Some(util) => util, - None => not_found(&util_os), + let Some(util) = util_os.to_str() else { + not_found(&util_os) }; match utils.get(util) { Some(&(uumain, _)) => { let code = uumain( - (vec![util_os, OsString::from("--help")].into_iter()) + vec![util_os, OsString::from("--help")] + .into_iter() .chain(args), ); io::stdout().flush().expect("could not flush stdout"); @@ -138,6 +200,8 @@ fn main() { } /// Prints completions for the utility in the first parameter for the shell in the second parameter to stdout +/// # Panics +/// Panics if the utility map is empty fn gen_completions( args: impl Iterator, util_map: &UtilityMap, @@ -176,6 +240,8 @@ fn gen_completions( } /// Generate the manpage for the utility in the first parameter +/// # Panics +/// Panics if the utility map is empty fn gen_manpage( args: impl Iterator, util_map: &UtilityMap, @@ -198,6 +264,7 @@ fn gen_manpage( let command = if utility == "coreutils" { gen_coreutils_app(util_map) } else { + setup_localization_or_exit(utility); util_map.get(utility).unwrap().1() }; @@ -208,6 +275,8 @@ fn gen_manpage( process::exit(0); } +/// # Panics +/// Panics if the utility map is empty fn gen_coreutils_app(util_map: &UtilityMap) -> Command { let mut command = Command::new("coreutils"); for (name, (_, sub_app)) in util_map { @@ -222,3 +291,70 @@ fn gen_coreutils_app(util_map: &UtilityMap) -> Command { } command } + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + #[test] + fn test_get_canonical_util_name() { + // Test a few key aliases + assert_eq!(get_canonical_util_name("["), "test"); + assert_eq!(get_canonical_util_name("md5sum"), "hashsum"); + assert_eq!(get_canonical_util_name("dir"), "ls"); + + // Test passthrough case + assert_eq!(get_canonical_util_name("cat"), "cat"); + } + + #[test] + fn test_name() { + // Test normal executable name + assert_eq!(name(Path::new("/usr/bin/ls")), Some("ls")); + assert_eq!(name(Path::new("cat")), Some("cat")); + assert_eq!( + name(Path::new("./target/debug/coreutils")), + Some("coreutils") + ); + + // Test with extensions + assert_eq!(name(Path::new("program.exe")), Some("program")); + assert_eq!(name(Path::new("/path/to/utility.bin")), Some("utility")); + + // Test edge cases + assert_eq!(name(Path::new("")), None); + assert_eq!(name(Path::new("/")), None); + } + + #[test] + fn test_find_prefixed_util() { + let utils = ["test", "cat", "ls", "cp"]; + + // Test exact prefixed matches + assert_eq!( + find_prefixed_util("uu_test", utils.iter().copied()), + Some("test") + ); + assert_eq!( + find_prefixed_util("my-cat", utils.iter().copied()), + Some("cat") + ); + assert_eq!( + find_prefixed_util("prefix_ls", utils.iter().copied()), + Some("ls") + ); + + // Test non-alphanumeric separator requirement + assert_eq!(find_prefixed_util("prefixcat", utils.iter().copied()), None); // no separator + assert_eq!(find_prefixed_util("testcat", utils.iter().copied()), None); // no separator + + // Test no match + assert_eq!(find_prefixed_util("unknown", utils.iter().copied()), None); + assert_eq!(find_prefixed_util("", utils.iter().copied()), None); + + // Test exact util name (should not match as prefixed) + assert_eq!(find_prefixed_util("test", utils.iter().copied()), None); + assert_eq!(find_prefixed_util("cat", utils.iter().copied()), None); + } +} diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index 77c7a2fcfdd..6a215a4ada4 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -13,6 +13,9 @@ use zip::ZipArchive; include!(concat!(env!("OUT_DIR"), "/uutils_map.rs")); +/// # Errors +/// Returns an error if the writer fails. +#[allow(clippy::too_many_lines)] fn main() -> io::Result<()> { let mut tldr_zip = File::open("docs/tldr.zip") .ok() @@ -20,7 +23,9 @@ fn main() -> io::Result<()> { if tldr_zip.is_none() { println!("Warning: No tldr archive found, so the documentation will not include examples."); - println!("To include examples in the documentation, download the tldr archive and put it in the docs/ folder."); + println!( + "To include examples in the documentation, download the tldr archive and put it in the docs/ folder." + ); println!(); println!(" curl https://tldr.sh/assets/tldr.zip -o docs/tldr.zip"); println!(); @@ -43,7 +48,9 @@ fn main() -> io::Result<()> { * [Installation](installation.md)\n\ * [Build from source](build.md)\n\ * [Platform support](platforms.md)\n\ - * [Contributing](contributing.md)\n\ + * [Contributing](CONTRIBUTING.md)\n\ + \t* [Development](DEVELOPMENT.md)\n\ + \t* [Code of Conduct](CODE_OF_CONDUCT.md)\n\ * [GNU test coverage](test_coverage.md)\n\ * [Extensions](extensions.md)\n\ \n\ @@ -57,7 +64,7 @@ fn main() -> io::Result<()> { for platform in ["unix", "macos", "windows", "unix_android"] { let platform_utils: Vec = String::from_utf8( std::process::Command::new("./util/show-utils.sh") - .arg(format!("--features=feat_os_{}", platform)) + .arg(format!("--features=feat_os_{platform}")) .output()? .stdout, ) @@ -107,7 +114,7 @@ fn main() -> io::Result<()> { "| util | Linux | macOS | Windows | FreeBSD | Android |\n\ | ---------------- | ----- | ----- | ------- | ------- | ------- |" )?; - for (&name, _) in &utils { + for &(&name, _) in &utils { if name == "[" { continue; } @@ -131,7 +138,7 @@ fn main() -> io::Result<()> { if name == "[" { continue; } - let p = format!("docs/src/utils/{}.md", name); + let p = format!("docs/src/utils/{name}.md"); let markdown = File::open(format!("src/uu/{name}/{name}.md")) .and_then(|mut f: File| { @@ -151,11 +158,11 @@ fn main() -> io::Result<()> { markdown, } .markdown()?; - println!("Wrote to '{}'", p); + println!("Wrote to '{p}'"); } else { - println!("Error writing to {}", p); + println!("Error writing to {p}"); } - writeln!(summary, "* [{0}](utils/{0}.md)", name)?; + writeln!(summary, "* [{name}](utils/{name}.md)")?; } Ok(()) } @@ -169,7 +176,9 @@ struct MDWriter<'a, 'b> { markdown: Option, } -impl<'a, 'b> MDWriter<'a, 'b> { +impl MDWriter<'_, '_> { + /// # Errors + /// Returns an error if the writer fails. fn markdown(&mut self) -> io::Result<()> { write!(self.w, "# {}\n\n", self.name)?; self.additional()?; @@ -180,6 +189,8 @@ impl<'a, 'b> MDWriter<'a, 'b> { self.examples() } + /// # Errors + /// Returns an error if the writer fails. fn additional(&mut self) -> io::Result<()> { writeln!(self.w, "
")?; self.platforms()?; @@ -187,6 +198,8 @@ impl<'a, 'b> MDWriter<'a, 'b> { writeln!(self.w, "
") } + /// # Errors + /// Returns an error if the writer fails. fn platforms(&mut self) -> io::Result<()> { writeln!(self.w, "
")?; for (feature, icon) in [ @@ -201,7 +214,7 @@ impl<'a, 'b> MDWriter<'a, 'b> { .iter() .any(|u| u == self.name) { - writeln!(self.w, "", icon)?; + writeln!(self.w, "")?; } } writeln!(self.w, "
")?; @@ -209,6 +222,10 @@ impl<'a, 'b> MDWriter<'a, 'b> { Ok(()) } + /// # Errors + /// Returns an error if the writer fails. + /// # Panics + /// Panics if the version is not found. fn version(&mut self) -> io::Result<()> { writeln!( self.w, @@ -217,19 +234,23 @@ impl<'a, 'b> MDWriter<'a, 'b> { ) } + /// # Errors + /// Returns an error if the writer fails. fn usage(&mut self) -> io::Result<()> { if let Some(markdown) = &self.markdown { let usage = uuhelp_parser::parse_usage(markdown); let usage = usage.replace("{}", self.name); writeln!(self.w, "\n```")?; - writeln!(self.w, "{}", usage)?; + writeln!(self.w, "{usage}")?; writeln!(self.w, "```") } else { Ok(()) } } + /// # Errors + /// Returns an error if the writer fails. fn about(&mut self) -> io::Result<()> { if let Some(markdown) = &self.markdown { writeln!(self.w, "{}", uuhelp_parser::parse_about(markdown)) @@ -238,6 +259,8 @@ impl<'a, 'b> MDWriter<'a, 'b> { } } + /// # Errors + /// Returns an error if the writer fails. fn after_help(&mut self) -> io::Result<()> { if let Some(markdown) = &self.markdown { if let Some(after_help) = uuhelp_parser::parse_section("after help", markdown) { @@ -248,6 +271,8 @@ impl<'a, 'b> MDWriter<'a, 'b> { Ok(()) } + /// # Errors + /// Returns an error if the writer fails. fn examples(&mut self) -> io::Result<()> { if let Some(zip) = self.tldr_zip { let content = if let Some(f) = @@ -268,14 +293,14 @@ impl<'a, 'b> MDWriter<'a, 'b> { writeln!(self.w)?; for line in content.lines().skip_while(|l| !l.starts_with('-')) { if let Some(l) = line.strip_prefix("- ") { - writeln!(self.w, "{}", l)?; + writeln!(self.w, "{l}")?; } else if line.starts_with('`') { writeln!(self.w, "```shell\n{}\n```", line.trim_matches('`'))?; } else if line.is_empty() { writeln!(self.w)?; } else { println!("Not sure what to do with this line:"); - println!("{}", line); + println!("{line}"); } } writeln!(self.w)?; @@ -292,6 +317,8 @@ impl<'a, 'b> MDWriter<'a, 'b> { Ok(()) } + /// # Errors + /// Returns an error if the writer fails. fn options(&mut self) -> io::Result<()> { writeln!(self.w, "

Options

")?; write!(self.w, "
")?; @@ -305,14 +332,14 @@ impl<'a, 'b> MDWriter<'a, 'b> { write!(self.w, ", ")?; } write!(self.w, "")?; - write!(self.w, "--{}", l)?; + write!(self.w, "--{l}")?; if let Some(names) = arg.get_value_names() { write!( self.w, "={}", names .iter() - .map(|x| format!("<{}>", x)) + .map(|x| format!("<{x}>")) .collect::>() .join(" ") )?; @@ -326,14 +353,14 @@ impl<'a, 'b> MDWriter<'a, 'b> { write!(self.w, ", ")?; } write!(self.w, "")?; - write!(self.w, "-{}", s)?; + write!(self.w, "-{s}")?; if let Some(names) = arg.get_value_names() { write!( self.w, " {}", names .iter() - .map(|x| format!("<{}>", x)) + .map(|x| format!("<{x}>")) .collect::>() .join(" ") )?; @@ -354,6 +381,8 @@ impl<'a, 'b> MDWriter<'a, 'b> { } } +/// # Panics +/// Panics if the archive is not ok fn get_zip_content(archive: &mut ZipArchive, name: &str) -> Option { let mut s = String::new(); archive.by_name(name).ok()?.read_to_string(&mut s).unwrap(); diff --git a/src/uu/arch/Cargo.toml b/src/uu/arch/Cargo.toml index fa2940acde1..39a2410baae 100644 --- a/src/uu/arch/Cargo.toml +++ b/src/uu/arch/Cargo.toml @@ -1,15 +1,18 @@ [package] name = "uu_arch" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "arch ~ (uutils) display machine architecture" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/arch" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/arch.rs" @@ -18,6 +21,7 @@ path = "src/arch.rs" platform-info = { workspace = true } clap = { workspace = true } uucore = { workspace = true } +fluent = { workspace = true } [[bin]] name = "arch" diff --git a/src/uu/arch/arch.md b/src/uu/arch/arch.md deleted file mode 100644 index a4ba2e75fce..00000000000 --- a/src/uu/arch/arch.md +++ /dev/null @@ -1,11 +0,0 @@ -# arch - -``` -arch -``` - -Display machine architecture - -## After Help - -Determine architecture name for current machine. diff --git a/src/uu/arch/locales/en-US.ftl b/src/uu/arch/locales/en-US.ftl new file mode 100644 index 00000000000..1646e50030d --- /dev/null +++ b/src/uu/arch/locales/en-US.ftl @@ -0,0 +1,5 @@ +# Error message when system architecture information cannot be retrieved +cannot-get-system = cannot get system name + +arch-about = Display machine architecture +arch-after-help = Determine architecture name for current machine. diff --git a/src/uu/arch/locales/fr-FR.ftl b/src/uu/arch/locales/fr-FR.ftl new file mode 100644 index 00000000000..f08462a87b4 --- /dev/null +++ b/src/uu/arch/locales/fr-FR.ftl @@ -0,0 +1,5 @@ +# Error message when system architecture information cannot be retrieved +cannot-get-system = impossible d'obtenir le nom du système + +arch-about = Afficher l'architecture de la machine +arch-after-help = Déterminer le nom de l'architecture pour la machine actuelle. diff --git a/src/uu/arch/src/arch.rs b/src/uu/arch/src/arch.rs index 0d71a818379..8fe80b602aa 100644 --- a/src/uu/arch/src/arch.rs +++ b/src/uu/arch/src/arch.rs @@ -5,18 +5,17 @@ use platform_info::*; -use clap::{crate_version, Command}; +use clap::Command; +use uucore::LocalizedCommand; use uucore::error::{UResult, USimpleError}; -use uucore::{help_about, help_section}; - -static ABOUT: &str = help_about!("arch.md"); -static SUMMARY: &str = help_section!("after help", "arch.md"); +use uucore::translate; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - uu_app().try_get_matches_from(args)?; + uu_app().get_matches_from_localized(args); - let uts = PlatformInfo::new().map_err(|_e| USimpleError::new(1, "cannot get system name"))?; + let uts = + PlatformInfo::new().map_err(|_e| USimpleError::new(1, translate!("cannot-get-system")))?; println!("{}", uts.machine().to_string_lossy().trim()); Ok(()) @@ -24,8 +23,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) - .about(ABOUT) - .after_help(SUMMARY) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) + .about(translate!("arch-about")) + .after_help(translate!("arch-after-help")) .infer_long_args(true) } diff --git a/src/uu/base32/Cargo.toml b/src/uu/base32/Cargo.toml index 5c8b23cba1f..2318911b517 100644 --- a/src/uu/base32/Cargo.toml +++ b/src/uu/base32/Cargo.toml @@ -1,15 +1,18 @@ [package] name = "uu_base32" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "base32 ~ (uutils) decode/encode input (base32-encoding)" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/base32" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/base32.rs" @@ -17,6 +20,7 @@ path = "src/base32.rs" [dependencies] clap = { workspace = true } uucore = { workspace = true, features = ["encoding"] } +fluent = { workspace = true } [[bin]] name = "base32" diff --git a/src/uu/base32/base32.md b/src/uu/base32/base32.md deleted file mode 100644 index 1805d433bd3..00000000000 --- a/src/uu/base32/base32.md +++ /dev/null @@ -1,14 +0,0 @@ -# base32 - -``` -base32 [OPTION]... [FILE] -``` - -encode/decode data and print to standard output -With no FILE, or when FILE is -, read standard input. - -The data are encoded as described for the base32 alphabet in RFC 4648. -When decoding, the input may contain newlines in addition -to the bytes of the formal base32 alphabet. Use --ignore-garbage -to attempt to recover from any other non-alphabet bytes in the -encoded stream. diff --git a/src/uu/base32/locales/en-US.ftl b/src/uu/base32/locales/en-US.ftl new file mode 100644 index 00000000000..c083d892804 --- /dev/null +++ b/src/uu/base32/locales/en-US.ftl @@ -0,0 +1,58 @@ +# This file contains base32, base64 and basenc strings +# This is because we have some common strings for all these tools +# and it is easier to have a single file than one file for program +# and loading several bundles at the same time. + +base32-about = encode/decode data and print to standard output + With no FILE, or when FILE is -, read standard input. + + The data are encoded as described for the base32 alphabet in RFC 4648. + When decoding, the input may contain newlines in addition + to the bytes of the formal base32 alphabet. Use --ignore-garbage + to attempt to recover from any other non-alphabet bytes in the + encoded stream. +base32-usage = base32 [OPTION]... [FILE] + +base64-about = encode/decode data and print to standard output + With no FILE, or when FILE is -, read standard input. + + The data are encoded as described for the base64 alphabet in RFC 3548. + When decoding, the input may contain newlines in addition + to the bytes of the formal base64 alphabet. Use --ignore-garbage + to attempt to recover from any other non-alphabet bytes in the + encoded stream. +base64-usage = base64 [OPTION]... [FILE] + +basenc-about = Encode/decode data and print to standard output + With no FILE, or when FILE is -, read standard input. + + When decoding, the input may contain newlines in addition to the bytes of + the formal alphabet. Use --ignore-garbage to attempt to recover + from any other non-alphabet bytes in the encoded stream. +basenc-usage = basenc [OPTION]... [FILE] + +# Help messages for encoding formats +basenc-help-base64 = same as 'base64' program +basenc-help-base64url = file- and url-safe base64 +basenc-help-base32 = same as 'base32' program +basenc-help-base32hex = extended hex alphabet base32 +basenc-help-base16 = hex encoding +basenc-help-base2lsbf = bit string with least significant bit (lsb) first +basenc-help-base2msbf = bit string with most significant bit (msb) first +basenc-help-z85 = ascii85-like encoding; + when encoding, input length must be a multiple of 4; + when decoding, input length must be a multiple of 5 + +# Error messages +basenc-error-missing-encoding-type = missing encoding type + +# Shared base_common error messages (used by base32, base64, basenc) +base-common-extra-operand = extra operand {$operand} +base-common-no-such-file = {$file}: No such file or directory +base-common-invalid-wrap-size = invalid wrap size: {$size} +base-common-read-error = read error: {$error} + +# Shared base_common help messages +base-common-help-decode = decode data +base-common-help-ignore-garbage = when decoding, ignore non-alphabetic characters +base-common-help-wrap = wrap encoded lines after COLS character (default {$default}, 0 to disable wrapping) diff --git a/src/uu/base32/locales/fr-FR.ftl b/src/uu/base32/locales/fr-FR.ftl new file mode 100644 index 00000000000..98c554bfbb7 --- /dev/null +++ b/src/uu/base32/locales/fr-FR.ftl @@ -0,0 +1,53 @@ +base32-about = encoder/décoder les données et les imprimer sur la sortie standard + Sans FICHIER, ou quand FICHIER est -, lire l'entrée standard. + + Les données sont encodées comme décrit pour l'alphabet base32 dans RFC 4648. + Lors du décodage, l'entrée peut contenir des retours à la ligne en plus + des octets de l'alphabet base32 formel. Utilisez --ignore-garbage + pour tenter de récupérer des autres octets non-alphabétiques dans + le flux encodé. +base32-usage = base32 [OPTION]... [FICHIER] + +base64-about = encoder/décoder les données et les imprimer sur la sortie standard + Sans FICHIER, ou quand FICHIER est -, lire l'entrée standard. + + Les données sont encodées comme décrit pour l'alphabet base64 dans RFC 3548. + Lors du décodage, l'entrée peut contenir des retours à la ligne en plus + des octets de l'alphabet base64 formel. Utilisez --ignore-garbage + pour tenter de récupérer des autres octets non-alphabétiques dans + le flux encodé. +base64-usage = base64 [OPTION]... [FICHIER] + +basenc-about = Encoder/décoder des données et afficher vers la sortie standard + Sans FICHIER, ou lorsque FICHIER est -, lire l'entrée standard. + + Lors du décodage, l'entrée peut contenir des nouvelles lignes en plus des octets de + l'alphabet formel. Utilisez --ignore-garbage pour tenter de récupérer + depuis tout autre octet non-alphabétique dans le flux encodé. +basenc-usage = basenc [OPTION]... [FICHIER] + +# Messages d'aide pour les formats d'encodage +basenc-help-base64 = identique au programme 'base64' +basenc-help-base64url = base64 sécurisé pour fichiers et URLs +basenc-help-base32 = identique au programme 'base32' +basenc-help-base32hex = base32 avec alphabet hexadécimal étendu +basenc-help-base16 = encodage hexadécimal +basenc-help-base2lsbf = chaîne de bits avec le bit de poids faible (lsb) en premier +basenc-help-base2msbf = chaîne de bits avec le bit de poids fort (msb) en premier +basenc-help-z85 = encodage de type ascii85 ; + lors de l'encodage, la longueur d'entrée doit être un multiple de 4 ; + lors du décodage, la longueur d'entrée doit être un multiple de 5 + +# Messages d'erreur +basenc-error-missing-encoding-type = type d'encodage manquant + +# Messages d'erreur partagés de base_common (utilisés par base32, base64, basenc) +base-common-extra-operand = opérande supplémentaire {$operand} +base-common-no-such-file = {$file} : Aucun fichier ou répertoire de ce type +base-common-invalid-wrap-size = taille de retour à la ligne invalide : {$size} +base-common-read-error = erreur de lecture : {$error} + +# Messages d'aide partagés de base_common +base-common-help-decode = décoder les données +base-common-help-ignore-garbage = lors du décodage, ignorer les caractères non-alphabétiques +base-common-help-wrap = retour à la ligne des lignes encodées après COLS caractères (par défaut {$default}, 0 pour désactiver le retour à la ligne) diff --git a/src/uu/base32/src/base32.rs b/src/uu/base32/src/base32.rs index 09250421c25..c88caa651b3 100644 --- a/src/uu/base32/src/base32.rs +++ b/src/uu/base32/src/base32.rs @@ -3,36 +3,27 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use std::io::{stdin, Read}; - -use clap::Command; -use uucore::{encoding::Format, error::UResult, help_about, help_usage}; - pub mod base_common; -const ABOUT: &str = help_about!("base32.md"); -const USAGE: &str = help_usage!("base32.md"); +use clap::Command; +use uucore::{encoding::Format, error::UResult, translate}; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let format = Format::Base32; - - let config: base_common::Config = base_common::parse_base_cmd_args(args, ABOUT, USAGE)?; - - // Create a reference to stdin so we can return a locked stdin from - // parse_base_cmd_args - let stdin_raw = stdin(); - let mut input: Box = base_common::get_input(&config, &stdin_raw)?; - - base_common::handle_input( - &mut input, - format, - config.wrap_cols, - config.ignore_garbage, - config.decode, - ) + let (about, usage) = get_info(); + let config = base_common::parse_base_cmd_args(args, about, usage)?; + let mut input = base_common::get_input(&config)?; + base_common::handle_input(&mut input, format, config) } pub fn uu_app() -> Command { - base_common::base_app(ABOUT, USAGE) + let (about, usage) = get_info(); + base_common::base_app(about, usage) +} + +fn get_info() -> (&'static str, &'static str) { + let about: &'static str = Box::leak(translate!("base32-about").into_boxed_str()); + let usage: &'static str = Box::leak(translate!("base32-usage").into_boxed_str()); + (about, usage) } diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index 897722dd36e..5c5dd983d8a 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -3,27 +3,37 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use std::io::{stdout, Read, Write}; +// spell-checker:ignore hexupper lsbf msbf unpadded nopad aGVsbG8sIHdvcmxkIQ +use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; +use std::fs::File; +use std::io::{self, ErrorKind, Read, Seek, SeekFrom}; +use std::path::{Path, PathBuf}; +use uucore::LocalizedCommand; use uucore::display::Quotable; -use uucore::encoding::{wrap_print, Data, EncodeError, Format}; +use uucore::encoding::{ + BASE2LSBF, BASE2MSBF, EncodingWrapper, Format, SupportsFastDecodeAndEncode, Z85Wrapper, + for_base_common::{BASE32, BASE32HEX, BASE64, BASE64_NOPAD, BASE64URL, HEXUPPER_PERMISSIVE}, +}; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::format_usage; +use uucore::translate; -use std::fs::File; -use std::io::{BufReader, Stdin}; -use std::path::Path; - -use clap::{crate_version, Arg, ArgAction, Command}; +pub const BASE_CMD_PARSE_ERROR: i32 = 1; -pub static BASE_CMD_PARSE_ERROR: i32 = 1; +/// Encoded output will be formatted in lines of this length (the last line can be shorter) +/// +/// Other implementations default to 76 +/// +/// This default is only used if no "-w"/"--wrap" argument is passed +pub const WRAP_DEFAULT: usize = 76; -// Config. pub struct Config { pub decode: bool, pub ignore_garbage: bool, pub wrap_cols: Option, - pub to_read: Option, + pub to_read: Option, } pub mod options { @@ -35,38 +45,42 @@ pub mod options { impl Config { pub fn from(options: &clap::ArgMatches) -> UResult { - let file: Option = match options.get_many::(options::FILE) { + let to_read = match options.get_many::(options::FILE) { Some(mut values) => { let name = values.next().unwrap(); + if let Some(extra_op) = values.next() { return Err(UUsageError::new( BASE_CMD_PARSE_ERROR, - format!("extra operand {}", extra_op.quote(),), + translate!("base-common-extra-operand", "operand" => extra_op.to_string_lossy().quote()), )); } if name == "-" { None } else { - if !Path::exists(Path::new(name)) { + let path = Path::new(name); + + if !path.exists() { return Err(USimpleError::new( BASE_CMD_PARSE_ERROR, - format!("{}: No such file or directory", name.maybe_quote()), + translate!("base-common-no-such-file", "file" => path.maybe_quote()), )); } - Some(name.clone()) + + Some(path.to_owned()) } } None => None, }; - let cols = options + let wrap_cols = options .get_one::(options::WRAP) .map(|num| { num.parse::().map_err(|_| { USimpleError::new( BASE_CMD_PARSE_ERROR, - format!("invalid wrap size: {}", num.quote()), + translate!("base-common-invalid-wrap-size", "size" => num.quote()), ) }) }) @@ -75,8 +89,8 @@ impl Config { Ok(Self { decode: options.get_flag(options::DECODE), ignore_garbage: options.get_flag(options::IGNORE_GARBAGE), - wrap_cols: cols, - to_read: file, + wrap_cols, + to_read, }) } } @@ -87,12 +101,14 @@ pub fn parse_base_cmd_args( usage: &str, ) -> UResult { let command = base_app(about, usage); - Config::from(&command.try_get_matches_from(args)?) + let matches = command.get_matches_from_localized(args); + Config::from(&matches) } pub fn base_app(about: &'static str, usage: &str) -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) .about(about) .override_usage(format_usage(usage)) .infer_long_args(true) @@ -100,8 +116,9 @@ pub fn base_app(about: &'static str, usage: &str) -> Command { .arg( Arg::new(options::DECODE) .short('d') + .visible_short_alias('D') .long(options::DECODE) - .help("decode data") + .help(translate!("base-common-help-decode")) .action(ArgAction::SetTrue) .overrides_with(options::DECODE), ) @@ -109,7 +126,7 @@ pub fn base_app(about: &'static str, usage: &str) -> Command { Arg::new(options::IGNORE_GARBAGE) .short('i') .long(options::IGNORE_GARBAGE) - .help("when decoding, ignore non-alphabetic characters") + .help(translate!("base-common-help-ignore-garbage")) .action(ArgAction::SetTrue) .overrides_with(options::IGNORE_GARBAGE), ) @@ -118,7 +135,7 @@ pub fn base_app(about: &'static str, usage: &str) -> Command { .short('w') .long(options::WRAP) .value_name("COLS") - .help("wrap encoded lines after COLS character (default 76, 0 to disable wrapping)") + .help(translate!("base-common-help-wrap", "default" => WRAP_DEFAULT)) .overrides_with(options::WRAP), ) // "multiple" arguments are used to check whether there is more than one @@ -126,60 +143,709 @@ pub fn base_app(about: &'static str, usage: &str) -> Command { .arg( Arg::new(options::FILE) .index(1) - .action(clap::ArgAction::Append) + .action(ArgAction::Append) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::FilePath), ) } -pub fn get_input<'a>(config: &Config, stdin_ref: &'a Stdin) -> UResult> { +/// A trait alias for types that implement both `Read` and `Seek`. +pub trait ReadSeek: Read + Seek {} + +/// Automatically implement the `ReadSeek` trait for any type that implements both `Read` and `Seek`. +impl ReadSeek for T {} + +pub fn get_input(config: &Config) -> UResult> { match &config.to_read { - Some(name) => { - let file_buf = - File::open(Path::new(name)).map_err_context(|| name.maybe_quote().to_string())?; - Ok(Box::new(BufReader::new(file_buf))) // as Box + Some(path_buf) => { + // Do not buffer input, because buffering is handled by `fast_decode` and `fast_encode` + let file = + File::open(path_buf).map_err_context(|| path_buf.maybe_quote().to_string())?; + Ok(Box::new(file)) } None => { - Ok(Box::new(stdin_ref.lock())) // as Box + let mut buffer = Vec::new(); + io::stdin().read_to_end(&mut buffer)?; + Ok(Box::new(io::Cursor::new(buffer))) } } } -pub fn handle_input( - input: &mut R, +/// Determines if the input buffer ends with padding ('=') after trimming trailing whitespace. +fn has_padding(input: &mut R) -> UResult { + let mut buf = Vec::new(); + input + .read_to_end(&mut buf) + .map_err(|err| USimpleError::new(1, format_read_error(err.kind())))?; + + // Reverse iterator and skip trailing whitespace without extra collections + let has_padding = buf + .iter() + .rfind(|&&byte| !byte.is_ascii_whitespace()) + .is_some_and(|&byte| byte == b'='); + + input.seek(SeekFrom::Start(0))?; + Ok(has_padding) +} + +pub fn handle_input(input: &mut R, format: Format, config: Config) -> UResult<()> { + let has_padding = has_padding(input)?; + + let supports_fast_decode_and_encode = + get_supports_fast_decode_and_encode(format, config.decode, has_padding); + + let supports_fast_decode_and_encode_ref = supports_fast_decode_and_encode.as_ref(); + + let mut stdout_lock = io::stdout().lock(); + + if config.decode { + fast_decode::fast_decode( + input, + &mut stdout_lock, + supports_fast_decode_and_encode_ref, + config.ignore_garbage, + ) + } else { + fast_encode::fast_encode( + input, + &mut stdout_lock, + supports_fast_decode_and_encode_ref, + config.wrap_cols, + ) + } +} + +pub fn get_supports_fast_decode_and_encode( format: Format, - line_wrap: Option, - ignore_garbage: bool, decode: bool, -) -> UResult<()> { - let mut data = Data::new(input, format).ignore_garbage(ignore_garbage); - if let Some(wrap) = line_wrap { - data = data.line_wrap(wrap); - } - - if decode { - match data.decode() { - Ok(s) => { - // Silent the warning as we want to the error message - #[allow(clippy::question_mark)] - if stdout().write_all(&s).is_err() { - // on windows console, writing invalid utf8 returns an error - return Err(USimpleError::new(1, "error: cannot write non-utf8 data")); + has_padding: bool, +) -> Box { + const BASE16_VALID_DECODING_MULTIPLE: usize = 2; + const BASE2_VALID_DECODING_MULTIPLE: usize = 8; + const BASE32_VALID_DECODING_MULTIPLE: usize = 8; + const BASE64_VALID_DECODING_MULTIPLE: usize = 4; + + const BASE16_UNPADDED_MULTIPLE: usize = 1; + const BASE2_UNPADDED_MULTIPLE: usize = 1; + const BASE32_UNPADDED_MULTIPLE: usize = 5; + const BASE64_UNPADDED_MULTIPLE: usize = 3; + + match format { + Format::Base16 => Box::from(EncodingWrapper::new( + HEXUPPER_PERMISSIVE, + BASE16_VALID_DECODING_MULTIPLE, + BASE16_UNPADDED_MULTIPLE, + // spell-checker:disable-next-line + b"0123456789ABCDEFabcdef", + )), + Format::Base2Lsbf => Box::from(EncodingWrapper::new( + BASE2LSBF, + BASE2_VALID_DECODING_MULTIPLE, + BASE2_UNPADDED_MULTIPLE, + // spell-checker:disable-next-line + b"01", + )), + Format::Base2Msbf => Box::from(EncodingWrapper::new( + BASE2MSBF, + BASE2_VALID_DECODING_MULTIPLE, + BASE2_UNPADDED_MULTIPLE, + // spell-checker:disable-next-line + b"01", + )), + Format::Base32 => Box::from(EncodingWrapper::new( + BASE32, + BASE32_VALID_DECODING_MULTIPLE, + BASE32_UNPADDED_MULTIPLE, + // spell-checker:disable-next-line + b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=", + )), + Format::Base32Hex => Box::from(EncodingWrapper::new( + BASE32HEX, + BASE32_VALID_DECODING_MULTIPLE, + BASE32_UNPADDED_MULTIPLE, + // spell-checker:disable-next-line + b"0123456789ABCDEFGHIJKLMNOPQRSTUV=", + )), + Format::Base64 => { + let alphabet: &[u8] = if has_padding { + &b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/="[..] + } else { + &b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/"[..] + }; + let wrapper = if decode && !has_padding { + BASE64_NOPAD + } else { + BASE64 + }; + Box::from(EncodingWrapper::new( + wrapper, + BASE64_VALID_DECODING_MULTIPLE, + BASE64_UNPADDED_MULTIPLE, + alphabet, + )) + } + Format::Base64Url => Box::from(EncodingWrapper::new( + BASE64URL, + BASE64_VALID_DECODING_MULTIPLE, + BASE64_UNPADDED_MULTIPLE, + // spell-checker:disable-next-line + b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789=_-", + )), + Format::Z85 => Box::from(Z85Wrapper {}), + } +} + +pub mod fast_encode { + use crate::base_common::{WRAP_DEFAULT, format_read_error}; + use std::{ + collections::VecDeque, + io::{self, ErrorKind, Read, Write}, + num::NonZeroUsize, + }; + use uucore::{ + encoding::SupportsFastDecodeAndEncode, + error::{UResult, USimpleError}, + }; + + struct LineWrapping { + line_length: NonZeroUsize, + print_buffer: Vec, + } + + // Start of helper functions + fn encode_in_chunks_to_buffer( + supports_fast_decode_and_encode: &dyn SupportsFastDecodeAndEncode, + encode_in_chunks_of_size: usize, + bytes_to_steal: usize, + read_buffer: &[u8], + encoded_buffer: &mut VecDeque, + leftover_buffer: &mut VecDeque, + ) -> UResult<()> { + let bytes_to_chunk = if bytes_to_steal > 0 { + let (stolen_bytes, rest_of_read_buffer) = read_buffer.split_at(bytes_to_steal); + + leftover_buffer.extend(stolen_bytes); + + // After appending the stolen bytes to `leftover_buffer`, it should be the right size + assert_eq!(leftover_buffer.len(), encode_in_chunks_of_size); + + // Encode the old unencoded data and the stolen bytes, and add the result to + // `encoded_buffer` + supports_fast_decode_and_encode + .encode_to_vec_deque(leftover_buffer.make_contiguous(), encoded_buffer)?; + + // Reset `leftover_buffer` + leftover_buffer.clear(); + + rest_of_read_buffer + } else { + // Do not need to steal bytes from `read_buffer` + read_buffer + }; + + let chunks_exact = bytes_to_chunk.chunks_exact(encode_in_chunks_of_size); + + let remainder = chunks_exact.remainder(); + + for sl in chunks_exact { + assert_eq!(sl.len(), encode_in_chunks_of_size); + + supports_fast_decode_and_encode.encode_to_vec_deque(sl, encoded_buffer)?; + } + + leftover_buffer.extend(remainder); + + Ok(()) + } + + fn write_without_line_breaks( + encoded_buffer: &mut VecDeque, + output: &mut dyn Write, + is_cleanup: bool, + empty_wrap: bool, + ) -> io::Result<()> { + // TODO + // `encoded_buffer` only has to be a VecDeque if line wrapping is enabled + // (`make_contiguous` should be a no-op here) + // Refactoring could avoid this call + output.write_all(encoded_buffer.make_contiguous())?; + + if is_cleanup { + if !empty_wrap { + output.write_all(b"\n")?; + } + } else { + encoded_buffer.clear(); + } + + Ok(()) + } + + fn write_with_line_breaks( + &mut LineWrapping { + ref line_length, + ref mut print_buffer, + }: &mut LineWrapping, + encoded_buffer: &mut VecDeque, + output: &mut dyn Write, + is_cleanup: bool, + ) -> io::Result<()> { + let line_length = line_length.get(); + + let make_contiguous_result = encoded_buffer.make_contiguous(); + + let chunks_exact = make_contiguous_result.chunks_exact(line_length); + + let mut bytes_added_to_print_buffer = 0; + + for sl in chunks_exact { + bytes_added_to_print_buffer += sl.len(); + + print_buffer.extend_from_slice(sl); + print_buffer.push(b'\n'); + } + + output.write_all(print_buffer)?; + + // Remove the bytes that were just printed from `encoded_buffer` + drop(encoded_buffer.drain(..bytes_added_to_print_buffer)); + + if is_cleanup { + if encoded_buffer.is_empty() { + // Do not write a newline in this case, because two trailing newlines should never be printed + } else { + // Print the partial line, since this is cleanup and no more data is coming + output.write_all(encoded_buffer.make_contiguous())?; + output.write_all(b"\n")?; + } + } else { + print_buffer.clear(); + } + + Ok(()) + } + + fn write_to_output( + line_wrapping: &mut Option, + encoded_buffer: &mut VecDeque, + output: &mut dyn Write, + is_cleanup: bool, + empty_wrap: bool, + ) -> io::Result<()> { + // Write all data in `encoded_buffer` to `output` + if let &mut Some(ref mut li) = line_wrapping { + write_with_line_breaks(li, encoded_buffer, output, is_cleanup)?; + } else { + write_without_line_breaks(encoded_buffer, output, is_cleanup, empty_wrap)?; + } + + Ok(()) + } + // End of helper functions + + pub fn fast_encode( + input: &mut dyn Read, + output: &mut dyn Write, + supports_fast_decode_and_encode: &dyn SupportsFastDecodeAndEncode, + wrap: Option, + ) -> UResult<()> { + // Based on performance testing + const INPUT_BUFFER_SIZE: usize = 32 * 1_024; + + const ENCODE_IN_CHUNKS_OF_SIZE_MULTIPLE: usize = 1_024; + + let encode_in_chunks_of_size = + supports_fast_decode_and_encode.unpadded_multiple() * ENCODE_IN_CHUNKS_OF_SIZE_MULTIPLE; + + assert!(encode_in_chunks_of_size > 0); + + // The "data-encoding" crate supports line wrapping, but not arbitrary line wrapping, only certain widths, so + // line wrapping must be handled here. + // https://github.com/ia0/data-encoding/blob/4f42ad7ef242f6d243e4de90cd1b46a57690d00e/lib/src/lib.rs#L1710 + let mut line_wrapping = match wrap { + // Line wrapping is disabled because "-w"/"--wrap" was passed with "0" + Some(0) => None, + // A custom line wrapping value was passed + Some(an) => Some(LineWrapping { + line_length: NonZeroUsize::new(an).unwrap(), + print_buffer: Vec::::new(), + }), + // Line wrapping was not set, so the default is used + None => Some(LineWrapping { + line_length: NonZeroUsize::new(WRAP_DEFAULT).unwrap(), + print_buffer: Vec::::new(), + }), + }; + + // Start of buffers + // Data that was read from `input` + let mut input_buffer = vec![0; INPUT_BUFFER_SIZE]; + + assert!(!input_buffer.is_empty()); + + // Data that was read from `input` but has not been encoded yet + let mut leftover_buffer = VecDeque::::new(); + + // Encoded data that needs to be written to `output` + let mut encoded_buffer = VecDeque::::new(); + // End of buffers + + loop { + match input.read(&mut input_buffer) { + Ok(bytes_read_from_input) => { + if bytes_read_from_input == 0 { + break; + } + + // The part of `input_buffer` that was actually filled by the call to `read` + let read_buffer = &input_buffer[..bytes_read_from_input]; + + // How many bytes to steal from `read_buffer` to get `leftover_buffer` to the right size + let bytes_to_steal = encode_in_chunks_of_size - leftover_buffer.len(); + + if bytes_to_steal > bytes_read_from_input { + // Do not have enough data to encode a chunk, so copy data to `leftover_buffer` and read more + leftover_buffer.extend(read_buffer); + + assert!(leftover_buffer.len() < encode_in_chunks_of_size); + + continue; + } + + // Encode data in chunks, then place it in `encoded_buffer` + encode_in_chunks_to_buffer( + supports_fast_decode_and_encode, + encode_in_chunks_of_size, + bytes_to_steal, + read_buffer, + &mut encoded_buffer, + &mut leftover_buffer, + )?; + + assert!(leftover_buffer.len() < encode_in_chunks_of_size); + // Write all data in `encoded_buffer` to `output` + write_to_output( + &mut line_wrapping, + &mut encoded_buffer, + output, + false, + wrap == Some(0), + )?; + } + Err(er) => { + let kind = er.kind(); + + if kind == ErrorKind::Interrupted { + // Retry reading + continue; + } + + return Err(USimpleError::new(1, format_read_error(kind))); } - Ok(()) } - Err(_) => Err(USimpleError::new(1, "error: invalid input")), } - } else { - match data.encode() { - Ok(s) => { - wrap_print(&data, &s); - Ok(()) + + // Cleanup + // `input` has finished producing data, so the data remaining in the buffers needs to be encoded and printed + { + // Encode all remaining unencoded bytes, placing them in `encoded_buffer` + supports_fast_decode_and_encode + .encode_to_vec_deque(leftover_buffer.make_contiguous(), &mut encoded_buffer)?; + + // Write all data in `encoded_buffer` to output + // `is_cleanup` triggers special cleanup-only logic + write_to_output( + &mut line_wrapping, + &mut encoded_buffer, + output, + true, + wrap == Some(0), + )?; + } + + Ok(()) + } +} + +pub mod fast_decode { + use crate::base_common::format_read_error; + use std::io::{self, ErrorKind, Read, Write}; + use uucore::{ + encoding::SupportsFastDecodeAndEncode, + error::{UResult, USimpleError}, + }; + + // Start of helper functions + fn alphabet_to_table(alphabet: &[u8], ignore_garbage: bool) -> [bool; 256] { + // If `ignore_garbage` is enabled, all characters outside the alphabet are ignored + // If it is not enabled, only '\n' and '\r' are ignored + if ignore_garbage { + // Note: "false" here + let mut table = [false; 256]; + + // Pass through no characters except those in the alphabet + for ue in alphabet { + let us = usize::from(*ue); + + // Should not have been set yet + assert!(!table[us]); + + table[us] = true; + } + + table + } else { + // Note: "true" here + let mut table = [true; 256]; + + // Pass through all characters except '\n' and '\r' + for ue in [b'\n', b'\r'] { + let us = usize::from(ue); + + // Should not have been set yet + assert!(table[us]); + + table[us] = false; + } + + table + } + } + + fn decode_in_chunks_to_buffer( + supports_fast_decode_and_encode: &dyn SupportsFastDecodeAndEncode, + decode_in_chunks_of_size: usize, + bytes_to_steal: usize, + read_buffer_filtered: &[u8], + decoded_buffer: &mut Vec, + leftover_buffer: &mut Vec, + ) -> UResult<()> { + let bytes_to_chunk = if bytes_to_steal > 0 { + let (stolen_bytes, rest_of_read_buffer_filtered) = + read_buffer_filtered.split_at(bytes_to_steal); + + leftover_buffer.extend(stolen_bytes); + + // After appending the stolen bytes to `leftover_buffer`, it should be the right size + assert_eq!(leftover_buffer.len(), decode_in_chunks_of_size); + + // Decode the old un-decoded data and the stolen bytes, and add the result to + // `decoded_buffer` + supports_fast_decode_and_encode.decode_into_vec(leftover_buffer, decoded_buffer)?; + + // Reset `leftover_buffer` + leftover_buffer.clear(); + + rest_of_read_buffer_filtered + } else { + // Do not need to steal bytes from `read_buffer` + read_buffer_filtered + }; + + let chunks_exact = bytes_to_chunk.chunks_exact(decode_in_chunks_of_size); + + let remainder = chunks_exact.remainder(); + + for sl in chunks_exact { + assert_eq!(sl.len(), decode_in_chunks_of_size); + + supports_fast_decode_and_encode.decode_into_vec(sl, decoded_buffer)?; + } + + leftover_buffer.extend(remainder); + + Ok(()) + } + + fn write_to_output(decoded_buffer: &mut Vec, output: &mut dyn Write) -> io::Result<()> { + // Write all data in `decoded_buffer` to `output` + output.write_all(decoded_buffer.as_slice())?; + + decoded_buffer.clear(); + + Ok(()) + } + // End of helper functions + + pub fn fast_decode( + input: &mut dyn Read, + output: &mut dyn Write, + supports_fast_decode_and_encode: &dyn SupportsFastDecodeAndEncode, + ignore_garbage: bool, + ) -> UResult<()> { + // Based on performance testing + const INPUT_BUFFER_SIZE: usize = 32 * 1_024; + + const DECODE_IN_CHUNKS_OF_SIZE_MULTIPLE: usize = 1_024; + + let alphabet = supports_fast_decode_and_encode.alphabet(); + let decode_in_chunks_of_size = supports_fast_decode_and_encode.valid_decoding_multiple() + * DECODE_IN_CHUNKS_OF_SIZE_MULTIPLE; + + assert!(decode_in_chunks_of_size > 0); + + // Note that it's not worth using "data-encoding"'s ignore functionality if `ignore_garbage` is true, because + // "data-encoding"'s ignore functionality cannot discard non-ASCII bytes. The data has to be filtered before + // passing it to "data-encoding", so there is no point in doing any filtering in "data-encoding". This also + // allows execution to stay on the happy path in "data-encoding": + // https://github.com/ia0/data-encoding/blob/4f42ad7ef242f6d243e4de90cd1b46a57690d00e/lib/src/lib.rs#L754-L756 + // It is also not worth using "data-encoding"'s ignore functionality when `ignore_garbage` is + // false. + // Note that the alphabet constants above already include the padding characters + // TODO + // Precompute this + let table = alphabet_to_table(alphabet, ignore_garbage); + + // Start of buffers + // Data that was read from `input` + let mut input_buffer = vec![0; INPUT_BUFFER_SIZE]; + + assert!(!input_buffer.is_empty()); + + // Data that was read from `input` but has not been decoded yet + let mut leftover_buffer = Vec::::new(); + + // Decoded data that needs to be written to `output` + let mut decoded_buffer = Vec::::new(); + + // Buffer that will be used when `ignore_garbage` is true, and the chunk read from `input` contains garbage + // data + let mut non_garbage_buffer = Vec::::new(); + // End of buffers + + loop { + match input.read(&mut input_buffer) { + Ok(bytes_read_from_input) => { + if bytes_read_from_input == 0 { + break; + } + + let read_buffer_filtered = { + // The part of `input_buffer` that was actually filled by the call to `read` + let read_buffer = &input_buffer[..bytes_read_from_input]; + + // First just scan the data for the happy path + // Yields significant speedup when the input does not contain line endings + let found_garbage = read_buffer.iter().any(|ue| { + // Garbage, since it was not found in the table + !table[usize::from(*ue)] + }); + + if found_garbage { + non_garbage_buffer.clear(); + + for ue in read_buffer { + if table[usize::from(*ue)] { + // Not garbage, since it was found in the table + non_garbage_buffer.push(*ue); + } + } + + non_garbage_buffer.as_slice() + } else { + read_buffer + } + }; + + // How many bytes to steal from `read_buffer` to get `leftover_buffer` to the right size + let bytes_to_steal = decode_in_chunks_of_size - leftover_buffer.len(); + + if bytes_to_steal > read_buffer_filtered.len() { + // Do not have enough data to decode a chunk, so copy data to `leftover_buffer` and read more + leftover_buffer.extend(read_buffer_filtered); + + assert!(leftover_buffer.len() < decode_in_chunks_of_size); + + continue; + } + + // Decode data in chunks, then place it in `decoded_buffer` + decode_in_chunks_to_buffer( + supports_fast_decode_and_encode, + decode_in_chunks_of_size, + bytes_to_steal, + read_buffer_filtered, + &mut decoded_buffer, + &mut leftover_buffer, + )?; + + assert!(leftover_buffer.len() < decode_in_chunks_of_size); + + // Write all data in `decoded_buffer` to `output` + write_to_output(&mut decoded_buffer, output)?; + } + Err(er) => { + let kind = er.kind(); + + if kind == ErrorKind::Interrupted { + // Retry reading + continue; + } + + return Err(USimpleError::new(1, format_read_error(kind))); + } + } + } + + // Cleanup + // `input` has finished producing data, so the data remaining in the buffers needs to be decoded and printed + { + // Decode all remaining encoded bytes, placing them in `decoded_buffer` + supports_fast_decode_and_encode + .decode_into_vec(&leftover_buffer, &mut decoded_buffer)?; + + // Write all data in `decoded_buffer` to `output` + write_to_output(&mut decoded_buffer, output)?; + } + + Ok(()) + } +} + +fn format_read_error(kind: ErrorKind) -> String { + let kind_string = kind.to_string(); + + // e.g. "is a directory" -> "Is a directory" + let mut kind_string_capitalized = String::with_capacity(kind_string.len()); + + for (index, ch) in kind_string.char_indices() { + if index == 0 { + for cha in ch.to_uppercase() { + kind_string_capitalized.push(cha); } - Err(EncodeError::InvalidInput) => Err(USimpleError::new(1, "error: invalid input")), - Err(_) => Err(USimpleError::new( - 1, - "error: invalid input (length must be multiple of 4 characters)", - )), + } else { + kind_string_capitalized.push(ch); + } + } + + translate!("base-common-read-error", "error" => kind_string_capitalized) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + #[test] + fn test_has_padding() { + let test_cases = vec![ + ("aGVsbG8sIHdvcmxkIQ==", true), + ("aGVsbG8sIHdvcmxkIQ== ", true), + ("aGVsbG8sIHdvcmxkIQ==\n", true), + ("aGVsbG8sIHdvcmxkIQ== \n", true), + ("aGVsbG8sIHdvcmxkIQ=", true), + ("aGVsbG8sIHdvcmxkIQ= ", true), + ("aGVsbG8sIHdvcmxkIQ \n", false), + ("aGVsbG8sIHdvcmxkIQ", false), + ]; + + for (input, expected) in test_cases { + let mut cursor = Cursor::new(input.as_bytes()); + assert_eq!( + has_padding(&mut cursor).unwrap(), + expected, + "Failed for input: '{input}'" + ); } } } diff --git a/src/uu/base64/Cargo.toml b/src/uu/base64/Cargo.toml index 5df285f89a5..8226b5877f5 100644 --- a/src/uu/base64/Cargo.toml +++ b/src/uu/base64/Cargo.toml @@ -1,22 +1,27 @@ [package] name = "uu_base64" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "base64 ~ (uutils) decode/encode input (base64-encoding)" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/base64" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/base64.rs" [dependencies] +clap = { workspace = true } uucore = { workspace = true, features = ["encoding"] } uu_base32 = { workspace = true } +fluent = { workspace = true } [[bin]] name = "base64" diff --git a/src/uu/base64/base64.md b/src/uu/base64/base64.md deleted file mode 100644 index ed3aa4f7638..00000000000 --- a/src/uu/base64/base64.md +++ /dev/null @@ -1,14 +0,0 @@ -# base64 - -``` -base64 [OPTION]... [FILE] -``` - -encode/decode data and print to standard output -With no FILE, or when FILE is -, read standard input. - -The data are encoded as described for the base64 alphabet in RFC 3548. -When decoding, the input may contain newlines in addition -to the bytes of the formal base64 alphabet. Use --ignore-garbage -to attempt to recover from any other non-alphabet bytes in the -encoded stream. diff --git a/src/uu/base64/locales b/src/uu/base64/locales new file mode 120000 index 00000000000..4d6838e66c4 --- /dev/null +++ b/src/uu/base64/locales @@ -0,0 +1 @@ +../base32/locales \ No newline at end of file diff --git a/src/uu/base64/src/base64.rs b/src/uu/base64/src/base64.rs index 6544638bdae..854fd91820b 100644 --- a/src/uu/base64/src/base64.rs +++ b/src/uu/base64/src/base64.rs @@ -3,32 +3,27 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +use clap::Command; use uu_base32::base_common; -pub use uu_base32::uu_app; - -use uucore::{encoding::Format, error::UResult, help_about, help_usage}; - -use std::io::{stdin, Read}; - -const ABOUT: &str = help_about!("base64.md"); -const USAGE: &str = help_usage!("base64.md"); +use uucore::translate; +use uucore::{encoding::Format, error::UResult}; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let format = Format::Base64; + let (about, usage) = get_info(); + let config = base_common::parse_base_cmd_args(args, about, usage)?; + let mut input = base_common::get_input(&config)?; + base_common::handle_input(&mut input, format, config) +} - let config: base_common::Config = base_common::parse_base_cmd_args(args, ABOUT, USAGE)?; - - // Create a reference to stdin so we can return a locked stdin from - // parse_base_cmd_args - let stdin_raw = stdin(); - let mut input: Box = base_common::get_input(&config, &stdin_raw)?; +pub fn uu_app() -> Command { + let (about, usage) = get_info(); + base_common::base_app(about, usage) +} - base_common::handle_input( - &mut input, - format, - config.wrap_cols, - config.ignore_garbage, - config.decode, - ) +fn get_info() -> (&'static str, &'static str) { + let about: &'static str = Box::leak(translate!("base64-about").into_boxed_str()); + let usage: &'static str = Box::leak(translate!("base64-usage").into_boxed_str()); + (about, usage) } diff --git a/src/uu/basename/Cargo.toml b/src/uu/basename/Cargo.toml index 9262b483a3f..9fe4adc03e3 100644 --- a/src/uu/basename/Cargo.toml +++ b/src/uu/basename/Cargo.toml @@ -1,15 +1,18 @@ [package] name = "uu_basename" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "basename ~ (uutils) display PATHNAME with leading directory components removed" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/basename" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/basename.rs" @@ -17,6 +20,7 @@ path = "src/basename.rs" [dependencies] clap = { workspace = true } uucore = { workspace = true } +fluent = { workspace = true } [[bin]] name = "basename" diff --git a/src/uu/basename/basename.md b/src/uu/basename/basename.md deleted file mode 100644 index ee87fa76d4e..00000000000 --- a/src/uu/basename/basename.md +++ /dev/null @@ -1,9 +0,0 @@ -# basename - -``` -basename [-z] NAME [SUFFIX] -basename OPTION... NAME... -``` - -Print NAME with any leading directory components removed -If specified, also remove a trailing SUFFIX diff --git a/src/uu/basename/locales/en-US.ftl b/src/uu/basename/locales/en-US.ftl new file mode 100644 index 00000000000..6ab5f364f71 --- /dev/null +++ b/src/uu/basename/locales/en-US.ftl @@ -0,0 +1,13 @@ +basename-about = Print NAME with any leading directory components removed + If specified, also remove a trailing SUFFIX +basename-usage = basename [-z] NAME [SUFFIX] + basename OPTION... NAME... + +# Error messages +basename-error-missing-operand = missing operand +basename-error-extra-operand = extra operand { $operand } + +# Help text for command-line arguments +basename-help-multiple = support multiple arguments and treat each as a NAME +basename-help-suffix = remove a trailing SUFFIX; implies -a +basename-help-zero = end each output line with NUL, not newline diff --git a/src/uu/basename/locales/fr-FR.ftl b/src/uu/basename/locales/fr-FR.ftl new file mode 100644 index 00000000000..69b03e6db72 --- /dev/null +++ b/src/uu/basename/locales/fr-FR.ftl @@ -0,0 +1,13 @@ +basename-about = Affiche NOM sans les composants de répertoire précédents + Si spécifié, supprime également un SUFFIXE final +basename-usage = basename [-z] NOM [SUFFIXE] + basename OPTION... NOM... + +# Messages d'erreur +basename-error-missing-operand = opérande manquant +basename-error-extra-operand = opérande supplémentaire { $operand } + +# Texte d'aide pour les arguments de ligne de commande +basename-help-multiple = prend en charge plusieurs arguments et traite chacun comme un NOM +basename-help-suffix = supprime un SUFFIXE final ; implique -a +basename-help-zero = termine chaque ligne de sortie avec NUL, pas nouvelle ligne diff --git a/src/uu/basename/src/basename.rs b/src/uu/basename/src/basename.rs index f502fb23466..61ac6928995 100644 --- a/src/uu/basename/src/basename.rs +++ b/src/uu/basename/src/basename.rs @@ -5,16 +5,18 @@ // spell-checker:ignore (ToDO) fullname -use clap::{crate_version, Arg, ArgAction, Command}; -use std::path::{is_separator, PathBuf}; +use clap::builder::ValueParser; +use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; +use std::io::{Write, stdout}; +use std::path::PathBuf; use uucore::display::Quotable; use uucore::error::{UResult, UUsageError}; +use uucore::format_usage; use uucore::line_ending::LineEnding; -use uucore::{format_usage, help_about, help_usage}; -static ABOUT: &str = help_about!("basename.md"); - -const USAGE: &str = help_usage!("basename.md"); +use uucore::LocalizedCommand; +use uucore::translate; pub mod options { pub static MULTIPLE: &str = "multiple"; @@ -25,39 +27,41 @@ pub mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let args = args.collect_lossy(); - // // Argument parsing // - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); let mut name_args = matches - .get_many::(options::NAME) + .get_many::(options::NAME) .unwrap_or_default() .collect::>(); if name_args.is_empty() { - return Err(UUsageError::new(1, "missing operand".to_string())); + return Err(UUsageError::new( + 1, + translate!("basename-error-missing-operand"), + )); } - let multiple_paths = - matches.get_one::(options::SUFFIX).is_some() || matches.get_flag(options::MULTIPLE); + let multiple_paths = matches.get_one::(options::SUFFIX).is_some() + || matches.get_flag(options::MULTIPLE); let suffix = if multiple_paths { matches - .get_one::(options::SUFFIX) + .get_one::(options::SUFFIX) .cloned() .unwrap_or_default() } else { // "simple format" match name_args.len() { 0 => panic!("already checked"), - 1 => String::default(), + 1 => OsString::default(), 2 => name_args.pop().unwrap().clone(), _ => { return Err(UUsageError::new( 1, - format!("extra operand {}", name_args[2].quote(),), + translate!("basename-error-extra-operand", + "operand" => name_args[2].quote()), )); } } @@ -68,7 +72,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // for path in name_args { - print!("{}{}", basename(path, &suffix), line_ending); + stdout().write_all(&basename(path, &suffix)?)?; + print!("{line_ending}"); } Ok(()) @@ -76,21 +81,23 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) + .about(translate!("basename-about")) + .override_usage(format_usage(&translate!("basename-usage"))) .infer_long_args(true) .arg( Arg::new(options::MULTIPLE) .short('a') .long(options::MULTIPLE) - .help("support multiple arguments and treat each as a NAME") + .help(translate!("basename-help-multiple")) .action(ArgAction::SetTrue) .overrides_with(options::MULTIPLE), ) .arg( Arg::new(options::NAME) - .action(clap::ArgAction::Append) + .action(ArgAction::Append) + .value_parser(ValueParser::os_string()) .value_hint(clap::ValueHint::AnyPath) .hide(true) .trailing_var_arg(true), @@ -100,41 +107,44 @@ pub fn uu_app() -> Command { .short('s') .long(options::SUFFIX) .value_name("SUFFIX") - .help("remove a trailing SUFFIX; implies -a") + .value_parser(ValueParser::os_string()) + .help(translate!("basename-help-suffix")) .overrides_with(options::SUFFIX), ) .arg( Arg::new(options::ZERO) .short('z') .long(options::ZERO) - .help("end each output line with NUL, not newline") + .help(translate!("basename-help-zero")) .action(ArgAction::SetTrue) .overrides_with(options::ZERO), ) } -fn basename(fullname: &str, suffix: &str) -> String { - // Remove all platform-specific path separators from the end. - let path = fullname.trim_end_matches(is_separator); +// We return a Vec. Returning a seemingly more proper `OsString` would +// require back and forth conversions as we need a &[u8] for printing anyway. +fn basename(fullname: &OsString, suffix: &OsString) -> UResult> { + let fullname_bytes = uucore::os_str_as_bytes(fullname)?; - // If the path contained *only* suffix characters (for example, if - // `fullname` were "///" and `suffix` were "/"), then `path` would - // be left with the empty string. In that case, we set `path` to be - // the original `fullname` to avoid returning the empty path. - let path = if path.is_empty() { fullname } else { path }; + // Handle special case where path ends with /. + if fullname_bytes.ends_with(b"/.") { + return Ok(b".".into()); + } // Convert to path buffer and get last path component - let pb = PathBuf::from(path); - match pb.components().last() { - Some(c) => { - let name = c.as_os_str().to_str().unwrap(); - if name == suffix { - name.to_string() - } else { - name.strip_suffix(suffix).unwrap_or(name).to_string() - } + let pb = PathBuf::from(fullname); + + pb.components().next_back().map_or(Ok([].into()), |c| { + let name = c.as_os_str(); + let name_bytes = uucore::os_str_as_bytes(name)?; + if name == suffix { + Ok(name_bytes.into()) + } else { + let suffix_bytes = uucore::os_str_as_bytes(suffix)?; + Ok(name_bytes + .strip_suffix(suffix_bytes) + .unwrap_or(name_bytes) + .into()) } - - None => String::new(), - } + }) } diff --git a/src/uu/basenc/BENCHMARKING.md b/src/uu/basenc/BENCHMARKING.md new file mode 100644 index 00000000000..8248cbbc53b --- /dev/null +++ b/src/uu/basenc/BENCHMARKING.md @@ -0,0 +1,177 @@ + + +# Benchmarking base32, base64, and basenc + +Note that the functionality of the `base32` and `base64` programs is identical to that of the `basenc` program, using +the "--base32" and "--base64" options, respectively. For that reason, it is only necessary to benchmark `basenc`. + +To compare the runtime performance of the uutils implementation with the GNU Core Utilities implementation, you can +use a benchmarking tool like [hyperfine][0]. + +hyperfine currently does not measure maximum memory usage. Memory usage can be benchmarked using [poop][2], or +[toybox][3]'s "time" subcommand (both are Linux only). + +Build the `basenc` binary using the release profile: + +```Shell +cargo build --package uu_basenc --profile release +``` + +## Expected performance + +uutils' `basenc` performs streaming decoding and encoding, and therefore should perform all operations with a constant +maximum memory usage, regardless of the size of the input. Release builds currently use less than 3 mebibytes of +memory, and memory usage greater than 10 mebibytes should be considered a bug. + +As of September 2024, uutils' `basenc` has runtime performance equal to or superior to GNU Core Utilities' `basenc` in +in most scenarios. uutils' `basenc` uses slightly more memory, but given how small these quantities are in absolute +terms (see above), this is highly unlikely to be practically relevant to users. + +## Benchmark results (2024-09-27) + +### Setup + +```Shell +# Use uutils' dd to create a 1 gibibyte in-memory file filled with random bytes (Linux only). +# On other platforms, you can use /tmp instead of /dev/shm, but note that /tmp is not guaranteed to be in-memory. +coreutils dd if=/dev/urandom of=/dev/shm/one-random-gibibyte bs=1024 count=1048576 + +# Encode this file for use in decoding performance testing +/usr/bin/basenc --base32hex -- /dev/shm/one-random-gibibyte 1>/dev/shm/one-random-gibibyte-base32hex-encoded +/usr/bin/basenc --z85 -- /dev/shm/one-random-gibibyte 1>/dev/shm/one-random-gibibyte-z85-encoded +``` + +### Programs being tested + +uutils' `basenc`: + +``` +⯠git rev-list HEAD | coreutils head -n 1 -- - +a0718ef0ffd50539a2e2bc0095c9fadcd70ab857 +``` + +GNU Core Utilities' `basenc`: + +``` +⯠/usr/bin/basenc --version | coreutils head -n 1 -- - +basenc (GNU coreutils) 9.4 +``` + +### Encoding performance + +#### "--base64", default line wrapping (76 characters) + +➕ Faster than GNU Core Utilities + +``` +⯠hyperfine \ + --sort \ + command \ + -- \ + '/usr/bin/basenc --base64 -- /dev/shm/one-random-gibibyte 1>/dev/null' \ + './target/release/basenc --base64 -- /dev/shm/one-random-gibibyte 1>/dev/null' + +Benchmark 1: /usr/bin/basenc --base64 -- /dev/shm/one-random-gibibyte 1>/dev/null + Time (mean ± σ): 965.1 ms ± 7.9 ms [User: 766.2 ms, System: 193.4 ms] + Range (min … max): 950.2 ms … 976.9 ms 10 runs + +Benchmark 2: ./target/release/basenc --base64 -- /dev/shm/one-random-gibibyte 1>/dev/null + Time (mean ± σ): 696.6 ms ± 9.1 ms [User: 574.9 ms, System: 117.3 ms] + Range (min … max): 683.1 ms … 713.5 ms 10 runs + +Relative speed comparison + 1.39 ± 0.02 /usr/bin/basenc --base64 -- /dev/shm/one-random-gibibyte 1>/dev/null + 1.00 ./target/release/basenc --base64 -- /dev/shm/one-random-gibibyte 1>/dev/null +``` + +#### "--base16", no line wrapping + +➖ Slower than GNU Core Utilities + +``` +⯠poop \ + '/usr/bin/basenc --base16 --wrap 0 -- /dev/shm/one-random-gibibyte' \ + './target/release/basenc --base16 --wrap 0 -- /dev/shm/one-random-gibibyte' + +Benchmark 1 (6 runs): /usr/bin/basenc --base16 --wrap 0 -- /dev/shm/one-random-gibibyte + measurement mean ± σ min … max outliers delta + wall_time 836ms ± 13.3ms 822ms … 855ms 0 ( 0%) 0% + peak_rss 2.05MB ± 73.0KB 1.94MB … 2.12MB 0 ( 0%) 0% + cpu_cycles 2.85G ± 32.8M 2.82G … 2.91G 0 ( 0%) 0% + instructions 14.0G ± 58.7 14.0G … 14.0G 0 ( 0%) 0% + cache_references 70.0M ± 6.48M 63.7M … 78.8M 0 ( 0%) 0% + cache_misses 582K ± 172K 354K … 771K 0 ( 0%) 0% + branch_misses 667K ± 4.55K 662K … 674K 0 ( 0%) 0% +Benchmark 2 (6 runs): ./target/release/basenc --base16 --wrap 0 -- /dev/shm/one-random-gibibyte + measurement mean ± σ min … max outliers delta + wall_time 884ms ± 6.38ms 878ms … 895ms 0 ( 0%) 💩+ 5.7% ± 1.6% + peak_rss 2.65MB ± 66.8KB 2.55MB … 2.74MB 0 ( 0%) 💩+ 29.3% ± 4.4% + cpu_cycles 3.15G ± 8.61M 3.14G … 3.16G 0 ( 0%) 💩+ 10.6% ± 1.1% + instructions 10.5G ± 275 10.5G … 10.5G 0 ( 0%) ⚡- 24.9% ± 0.0% + cache_references 93.5M ± 6.10M 87.2M … 104M 0 ( 0%) 💩+ 33.7% ± 11.6% + cache_misses 415K ± 52.3K 363K … 474K 0 ( 0%) - 28.8% ± 28.0% + branch_misses 1.43M ± 4.82K 1.42M … 1.43M 0 ( 0%) 💩+113.9% ± 0.9% +``` + +### Decoding performance + +#### "--base32hex" + +➕ Faster than GNU Core Utilities + +``` +⯠hyperfine \ + --sort \ + command \ + -- \ + '/usr/bin/basenc --base32hex --decode -- /dev/shm/one-random-gibibyte-base32hex-encoded 1>/dev/null' \ + './target/release/basenc --base32hex --decode -- /dev/shm/one-random-gibibyte-base32hex-encoded 1>/dev/null' + +Benchmark 1: /usr/bin/basenc --base32hex --decode -- /dev/shm/one-random-gibibyte-base32hex-encoded 1>/dev/null + Time (mean ± σ): 7.154 s ± 0.082 s [User: 6.802 s, System: 0.323 s] + Range (min … max): 7.051 s … 7.297 s 10 runs + +Benchmark 2: ./target/release/basenc --base32hex --decode -- /dev/shm/one-random-gibibyte-base32hex-encoded 1>/dev/null + Time (mean ± σ): 2.679 s ± 0.025 s [User: 2.446 s, System: 0.221 s] + Range (min … max): 2.649 s … 2.718 s 10 runs + +Relative speed comparison + 2.67 ± 0.04 /usr/bin/basenc --base32hex --decode -- /dev/shm/one-random-gibibyte-base32hex-encoded 1>/dev/null + 1.00 ./target/release/basenc --base32hex --decode -- /dev/shm/one-random-gibibyte-base32hex-encoded 1>/dev/null +``` + +#### "--z85", with "--ignore-garbage" + +➕ Faster than GNU Core Utilities + +``` +⯠poop \ + '/usr/bin/basenc --decode --ignore-garbage --z85 -- /dev/shm/one-random-gibibyte-z85-encoded' \ + './target/release/basenc --decode --ignore-garbage --z85 -- /dev/shm/one-random-gibibyte-z85-encoded' + +Benchmark 1 (3 runs): /usr/bin/basenc --decode --ignore-garbage --z85 -- /dev/shm/one-random-gibibyte-z85-encoded + measurement mean ± σ min … max outliers delta + wall_time 14.4s ± 68.4ms 14.3s … 14.4s 0 ( 0%) 0% + peak_rss 1.98MB ± 10.8KB 1.97MB … 1.99MB 0 ( 0%) 0% + cpu_cycles 58.4G ± 211M 58.3G … 58.7G 0 ( 0%) 0% + instructions 74.7G ± 64.0 74.7G … 74.7G 0 ( 0%) 0% + cache_references 41.8M ± 624K 41.2M … 42.4M 0 ( 0%) 0% + cache_misses 693K ± 118K 567K … 802K 0 ( 0%) 0% + branch_misses 1.24G ± 183K 1.24G … 1.24G 0 ( 0%) 0% +Benchmark 2 (3 runs): ./target/release/basenc --decode --ignore-garbage --z85 -- /dev/shm/one-random-gibibyte-z85-encoded + measurement mean ± σ min … max outliers delta + wall_time 2.80s ± 17.9ms 2.79s … 2.82s 0 ( 0%) ⚡- 80.5% ± 0.8% + peak_rss 2.61MB ± 67.4KB 2.57MB … 2.69MB 0 ( 0%) 💩+ 31.9% ± 5.5% + cpu_cycles 10.8G ± 27.9M 10.8G … 10.9G 0 ( 0%) ⚡- 81.5% ± 0.6% + instructions 39.0G ± 353 39.0G … 39.0G 0 ( 0%) ⚡- 47.7% ± 0.0% + cache_references 114M ± 2.43M 112M … 116M 0 ( 0%) 💩+173.3% ± 9.6% + cache_misses 1.06M ± 288K 805K … 1.37M 0 ( 0%) + 52.6% ± 72.0% + branch_misses 1.18M ± 14.7K 1.16M … 1.19M 0 ( 0%) ⚡- 99.9% ± 0.0% +``` + +[0]: https://github.com/sharkdp/hyperfine +[1]: https://github.com/sharkdp/hyperfine?tab=readme-ov-file#installation +[2]: https://github.com/andrewrk/poop +[3]: https://landley.net/toybox/ diff --git a/src/uu/basenc/Cargo.toml b/src/uu/basenc/Cargo.toml index 842d80876d2..f68a0f11086 100644 --- a/src/uu/basenc/Cargo.toml +++ b/src/uu/basenc/Cargo.toml @@ -1,15 +1,18 @@ [package] name = "uu_basenc" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "basenc ~ (uutils) decode/encode input" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/basenc" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/basenc.rs" @@ -18,6 +21,7 @@ path = "src/basenc.rs" clap = { workspace = true } uucore = { workspace = true, features = ["encoding"] } uu_base32 = { workspace = true } +fluent = { workspace = true } [[bin]] name = "basenc" diff --git a/src/uu/basenc/basenc.md b/src/uu/basenc/basenc.md deleted file mode 100644 index 001babe9e6b..00000000000 --- a/src/uu/basenc/basenc.md +++ /dev/null @@ -1,12 +0,0 @@ -# basenc - -``` -basenc [OPTION]... [FILE] -``` - -Encode/decode data and print to standard output -With no FILE, or when FILE is -, read standard input. - -When decoding, the input may contain newlines in addition to the bytes of -the formal alphabet. Use --ignore-garbage to attempt to recover -from any other non-alphabet bytes in the encoded stream. diff --git a/src/uu/basenc/locales b/src/uu/basenc/locales new file mode 120000 index 00000000000..4d6838e66c4 --- /dev/null +++ b/src/uu/basenc/locales @@ -0,0 +1 @@ +../base32/locales \ No newline at end of file diff --git a/src/uu/basenc/src/basenc.rs b/src/uu/basenc/src/basenc.rs index ed117b22a0d..c6cd48c2acd 100644 --- a/src/uu/basenc/src/basenc.rs +++ b/src/uu/basenc/src/basenc.rs @@ -3,61 +3,59 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -//spell-checker:ignore (args) lsbf msbf +// spell-checker:ignore lsbf msbf use clap::{Arg, ArgAction, Command}; -use uu_base32::base_common::{self, Config, BASE_CMD_PARSE_ERROR}; - +use uu_base32::base_common::{self, BASE_CMD_PARSE_ERROR, Config}; +use uucore::error::UClapError; +use uucore::translate; use uucore::{ encoding::Format, error::{UResult, UUsageError}, }; -use std::io::{stdin, Read}; -use uucore::error::UClapError; - -use uucore::{help_about, help_usage}; +fn get_encodings() -> Vec<(&'static str, Format, String)> { + vec![ + ("base64", Format::Base64, translate!("basenc-help-base64")), + ( + "base64url", + Format::Base64Url, + translate!("basenc-help-base64url"), + ), + ("base32", Format::Base32, translate!("basenc-help-base32")), + ( + "base32hex", + Format::Base32Hex, + translate!("basenc-help-base32hex"), + ), + ("base16", Format::Base16, translate!("basenc-help-base16")), + ( + "base2lsbf", + Format::Base2Lsbf, + translate!("basenc-help-base2lsbf"), + ), + ( + "base2msbf", + Format::Base2Msbf, + translate!("basenc-help-base2msbf"), + ), + ("z85", Format::Z85, translate!("basenc-help-z85")), + ] +} -const ABOUT: &str = help_about!("basenc.md"); -const USAGE: &str = help_usage!("basenc.md"); +pub fn uu_app() -> Command { + let about: &'static str = Box::leak(translate!("basenc-about").into_boxed_str()); + let usage: &'static str = Box::leak(translate!("basenc-usage").into_boxed_str()); -const ENCODINGS: &[(&str, Format, &str)] = &[ - ("base64", Format::Base64, "same as 'base64' program"), - ("base64url", Format::Base64Url, "file- and url-safe base64"), - ("base32", Format::Base32, "same as 'base32' program"), - ( - "base32hex", - Format::Base32Hex, - "extended hex alphabet base32", - ), - ("base16", Format::Base16, "hex encoding"), - ( - "base2lsbf", - Format::Base2Lsbf, - "bit string with least significant bit (lsb) first", - ), - ( - "base2msbf", - Format::Base2Msbf, - "bit string with most significant bit (msb) first", - ), - ( - "z85", - Format::Z85, - "ascii85-like encoding;\n\ - when encoding, input length must be a multiple of 4;\n\ - when decoding, input length must be a multiple of 5", - ), -]; + let encodings = get_encodings(); + let mut command = base_common::base_app(about, usage); -pub fn uu_app() -> Command { - let mut command = base_common::base_app(ABOUT, USAGE); - for encoding in ENCODINGS { + for encoding in &encodings { let raw_arg = Arg::new(encoding.0) .long(encoding.0) - .help(encoding.2) + .help(&encoding.2) .action(ArgAction::SetTrue); - let overriding_arg = ENCODINGS + let overriding_arg = encodings .iter() .fold(raw_arg, |arg, enc| arg.overrides_with(enc.0)); command = command.arg(overriding_arg); @@ -69,10 +67,17 @@ fn parse_cmd_args(args: impl uucore::Args) -> UResult<(Config, Format)> { let matches = uu_app() .try_get_matches_from(args.collect_lossy()) .with_exit_code(1)?; - let format = ENCODINGS + + let encodings = get_encodings(); + let format = encodings .iter() .find(|encoding| matches.get_flag(encoding.0)) - .ok_or_else(|| UUsageError::new(BASE_CMD_PARSE_ERROR, "missing encoding type"))? + .ok_or_else(|| { + UUsageError::new( + BASE_CMD_PARSE_ERROR, + translate!("basenc-error-missing-encoding-type"), + ) + })? .1; let config = Config::from(&matches)?; Ok((config, format)) @@ -81,16 +86,8 @@ fn parse_cmd_args(args: impl uucore::Args) -> UResult<(Config, Format)> { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let (config, format) = parse_cmd_args(args)?; - // Create a reference to stdin so we can return a locked stdin from - // parse_base_cmd_args - let stdin_raw = stdin(); - let mut input: Box = base_common::get_input(&config, &stdin_raw)?; - base_common::handle_input( - &mut input, - format, - config.wrap_cols, - config.ignore_garbage, - config.decode, - ) + let mut input = base_common::get_input(&config)?; + + base_common::handle_input(&mut input, format, config) } diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index 79609c1cedd..632a0d97b87 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -1,27 +1,39 @@ [package] name = "uu_cat" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "cat ~ (uutils) concatenate and display input" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/cat" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/cat.rs" [dependencies] clap = { workspace = true } +memchr = { workspace = true } thiserror = { workspace = true } -uucore = { workspace = true, features = ["fs", "pipes"] } +uucore = { workspace = true, features = ["fast-inc", "fs", "pipes"] } +fluent = { workspace = true } [target.'cfg(unix)'.dependencies] nix = { workspace = true } +[target.'cfg(windows)'.dependencies] +winapi-util = { workspace = true } +windows-sys = { workspace = true, features = ["Win32_Storage_FileSystem"] } + +[dev-dependencies] +tempfile = { workspace = true } + [[bin]] name = "cat" path = "src/main.rs" diff --git a/src/uu/cat/cat.md b/src/uu/cat/cat.md deleted file mode 100644 index efcd317eb41..00000000000 --- a/src/uu/cat/cat.md +++ /dev/null @@ -1,8 +0,0 @@ -# cat - -``` -cat [OPTION]... [FILE]... -``` - -Concatenate FILE(s), or standard input, to standard output -With no FILE, or when FILE is -, read standard input. diff --git a/src/uu/cat/locales/en-US.ftl b/src/uu/cat/locales/en-US.ftl new file mode 100644 index 00000000000..108000d5aa1 --- /dev/null +++ b/src/uu/cat/locales/en-US.ftl @@ -0,0 +1,3 @@ +cat-about = Concatenate FILE(s), or standard input, to standard output + With no FILE, or when FILE is -, read standard input. +cat-usage = cat [OPTION]... [FILE]... diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index b239dc87a41..89c9f211132 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -4,32 +4,88 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) nonprint nonblank nonprinting ELOOP -use clap::{crate_version, Arg, ArgAction, Command}; -use std::fs::{metadata, File}; -use std::io::{self, IsTerminal, Read, Write}; -use thiserror::Error; -use uucore::display::Quotable; -use uucore::error::UResult; -use uucore::fs::FileInformation; -#[cfg(unix)] -use std::os::unix::io::AsRawFd; - -/// Linux splice support -#[cfg(any(target_os = "linux", target_os = "android"))] -mod splice; +mod platform; +use crate::platform::is_unsafe_overwrite; +use clap::{Arg, ArgAction, Command}; +use memchr::memchr2; +use std::ffi::OsString; +use std::fs::{File, metadata}; +use std::io::{self, BufWriter, ErrorKind, IsTerminal, Read, Write}; /// Unix domain socket support #[cfg(unix)] use std::net::Shutdown; #[cfg(unix)] +use std::os::fd::AsFd; +#[cfg(unix)] use std::os::unix::fs::FileTypeExt; #[cfg(unix)] use std::os::unix::net::UnixStream; -use uucore::{format_usage, help_about, help_usage}; +use thiserror::Error; +use uucore::LocalizedCommand; +use uucore::display::Quotable; +use uucore::error::UResult; +#[cfg(not(target_os = "windows"))] +use uucore::libc; +use uucore::translate; +use uucore::{fast_inc::fast_inc_one, format_usage}; + +/// Linux splice support +#[cfg(any(target_os = "linux", target_os = "android"))] +mod splice; + +// Allocate 32 digits for the line number. +// An estimate is that we can print about 1e8 lines/seconds, so 32 digits +// would be enough for billions of universe lifetimes. +const LINE_NUMBER_BUF_SIZE: usize = 32; + +struct LineNumber { + buf: [u8; LINE_NUMBER_BUF_SIZE], + print_start: usize, + num_start: usize, + num_end: usize, +} -const USAGE: &str = help_usage!("cat.md"); -const ABOUT: &str = help_about!("cat.md"); +// Logic to store a string for the line number. Manually incrementing the value +// represented in a buffer like this is significantly faster than storing +// a `usize` and using the standard Rust formatting macros to format a `usize` +// to a string each time it's needed. +// Buffer is initialized to " 1\t" and incremented each time `increment` is +// called, using uucore's fast_inc function that operates on strings. +impl LineNumber { + fn new() -> Self { + let mut buf = [b'0'; LINE_NUMBER_BUF_SIZE]; + + let init_str = " 1\t"; + let print_start = buf.len() - init_str.len(); + let num_start = buf.len() - 2; + let num_end = buf.len() - 1; + + buf[print_start..].copy_from_slice(init_str.as_bytes()); + + LineNumber { + buf, + print_start, + num_start, + num_end, + } + } + + fn increment(&mut self) { + fast_inc_one(&mut self.buf, &mut self.num_start, self.num_end); + self.print_start = self.print_start.min(self.num_start); + } + + #[inline] + fn to_str(&self) -> &[u8] { + &self.buf[self.print_start..] + } + + fn write(&self, writer: &mut impl Write) -> io::Result<()> { + writer.write_all(self.to_str()) + } +} #[derive(Error, Debug)] enum CatError { @@ -41,7 +97,7 @@ enum CatError { #[error("{0}")] Nix(#[from] nix::Error), /// Unknown file type; it's not a regular file, socket, etc. - #[error("unknown filetype: {}", ft_debug)] + #[error("unknown filetype: {ft_debug}")] UnknownFiletype { /// A debug print of the file type ft_debug: String, @@ -82,19 +138,11 @@ struct OutputOptions { impl OutputOptions { fn tab(&self) -> &'static str { - if self.show_tabs { - "^I" - } else { - "\t" - } + if self.show_tabs { "^I" } else { "\t" } } fn end_of_line(&self) -> &'static str { - if self.show_ends { - "$\n" - } else { - "\n" - } + if self.show_ends { "$\n" } else { "\n" } } /// We can write fast if we can simply copy the contents of the file to @@ -112,7 +160,7 @@ impl OutputOptions { /// when we can't write fast. struct OutputState { /// The current line number - line_number: usize, + line_number: LineNumber, /// Whether the output cursor is at the beginning of a new line at_line_start: bool, @@ -125,12 +173,12 @@ struct OutputState { } #[cfg(unix)] -trait FdReadable: Read + AsRawFd {} +trait FdReadable: Read + AsFd {} #[cfg(not(unix))] trait FdReadable: Read {} #[cfg(unix)] -impl FdReadable for T where T: Read + AsRawFd {} +impl FdReadable for T where T: Read + AsFd {} #[cfg(not(unix))] impl FdReadable for T where T: Read {} @@ -143,7 +191,7 @@ struct InputHandle { /// Concrete enum of recognized file types. /// /// *Note*: `cat`-ing a directory should result in an -/// CatError::IsDirectory +/// [`CatError::IsDirectory`] enum InputType { Directory, File, @@ -175,7 +223,16 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + // When we receive a SIGPIPE signal, we want to terminate the process so + // that we don't print any error messages to stderr. Rust ignores SIGPIPE + // (see https://github.com/rust-lang/rust/issues/62569), so we restore it's + // default action here. + #[cfg(not(target_os = "windows"))] + unsafe { + libc::signal(libc::SIGPIPE, libc::SIG_DFL); + } + + let matches = uu_app().get_matches_from_localized(args); let number_mode = if matches.get_flag(options::NUMBER_NONBLANK) { NumberingMode::NonEmpty @@ -211,9 +268,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .any(|v| matches.get_flag(v)); let squeeze_blank = matches.get_flag(options::SQUEEZE_BLANK); - let files: Vec = match matches.get_many::(options::FILE) { + let files: Vec = match matches.get_many::(options::FILE) { Some(v) => v.cloned().collect(), - None => vec!["-".to_owned()], + None => vec![OsString::from("-")], }; let options = OutputOptions { @@ -228,15 +285,17 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) - .override_usage(format_usage(USAGE)) - .about(ABOUT) + .version(uucore::crate_version!()) + .override_usage(format_usage(&translate!("cat-usage"))) + .about(translate!("cat-about")) + .help_template(uucore::localized_help_template(uucore::util_name())) .infer_long_args(true) .args_override_self(true) .arg( Arg::new(options::FILE) .hide(true) - .action(clap::ArgAction::Append) + .action(ArgAction::Append) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::FilePath), ) .arg( @@ -322,18 +381,16 @@ fn cat_handle( } } -fn cat_path( - path: &str, - options: &OutputOptions, - state: &mut OutputState, - out_info: Option<&FileInformation>, -) -> CatResult<()> { +fn cat_path(path: &OsString, options: &OutputOptions, state: &mut OutputState) -> CatResult<()> { match get_input_type(path)? { InputType::StdIn => { let stdin = io::stdin(); + if is_unsafe_overwrite(&stdin, &io::stdout()) { + return Err(CatError::OutputIsInput); + } let mut handle = InputHandle { reader: stdin, - is_interactive: std::io::stdin().is_terminal(), + is_interactive: io::stdin().is_terminal(), }; cat_handle(&mut handle, options, state) } @@ -350,15 +407,9 @@ fn cat_path( } _ => { let file = File::open(path)?; - - if let Some(out_info) = out_info { - if out_info.file_size() != 0 - && FileInformation::from_file(&file).ok().as_ref() == Some(out_info) - { - return Err(CatError::OutputIsInput); - } + if is_unsafe_overwrite(&file, &io::stdout()) { + return Err(CatError::OutputIsInput); } - let mut handle = InputHandle { reader: file, is_interactive: false, @@ -368,11 +419,9 @@ fn cat_path( } } -fn cat_files(files: &[String], options: &OutputOptions) -> UResult<()> { - let out_info = FileInformation::from_file(&std::io::stdout()).ok(); - +fn cat_files(files: &[OsString], options: &OutputOptions) -> UResult<()> { let mut state = OutputState { - line_number: 1, + line_number: LineNumber::new(), at_line_start: true, skipped_carriage_return: false, one_blank_kept: false, @@ -380,8 +429,8 @@ fn cat_files(files: &[String], options: &OutputOptions) -> UResult<()> { let mut error_messages: Vec = Vec::new(); for path in files { - if let Err(err) = cat_path(path, options, &mut state, out_info.as_ref()) { - error_messages.push(format!("{}: {}", path.maybe_quote(), err)); + if let Err(err) = cat_path(path, options, &mut state) { + error_messages.push(format!("{}: {err}", path.maybe_quote())); } } if state.skipped_carriage_return { @@ -405,7 +454,7 @@ fn cat_files(files: &[String], options: &OutputOptions) -> UResult<()> { /// # Arguments /// /// * `path` - Path on a file system to classify metadata -fn get_input_type(path: &str) -> CatResult { +fn get_input_type(path: &OsString) -> CatResult { if path == "-" { return Ok(InputType::StdIn); } @@ -461,12 +510,26 @@ fn write_fast(handle: &mut InputHandle) -> CatResult<()> { // If we're not on Linux or Android, or the splice() call failed, // fall back on slower writing. let mut buf = [0; 1024 * 64]; - while let Ok(n) = handle.reader.read(&mut buf) { - if n == 0 { - break; + loop { + match handle.reader.read(&mut buf) { + Ok(n) => { + if n == 0 { + break; + } + stdout_lock + .write_all(&buf[..n]) + .inspect_err(handle_broken_pipe)?; + } + Err(e) => return Err(e.into()), } - stdout_lock.write_all(&buf[..n])?; } + + // If the splice() call failed and there has been some data written to + // stdout via while loop above AND there will be second splice() call + // that will succeed, data pushed through splice will be output before + // the data buffered in stdout.lock. Therefore additional explicit flush + // is required here. + stdout_lock.flush().inspect_err(handle_broken_pipe)?; Ok(()) } @@ -479,7 +542,9 @@ fn write_lines( ) -> CatResult<()> { let mut in_buf = [0; 1024 * 31]; let stdout = io::stdout(); - let mut writer = stdout.lock(); + let stdout = stdout.lock(); + // Add a 32K buffer for stdout - this greatly improves performance. + let mut writer = BufWriter::with_capacity(32 * 1024, stdout); while let Ok(n) = handle.reader.read(&mut in_buf) { if n == 0 { @@ -502,8 +567,8 @@ fn write_lines( } state.one_blank_kept = false; if state.at_line_start && options.number != NumberingMode::None { - write!(writer, "{0:6}\t", state.line_number)?; - state.line_number += 1; + state.line_number.write(&mut writer)?; + state.line_number.increment(); } // print to end of line or end of buffer @@ -528,32 +593,44 @@ fn write_lines( } pos += offset + 1; } + // We need to flush the buffer each time around the loop in order to pass GNU tests. + // When we are reading the input from a pipe, the `handle.reader.read` call at the top + // of this loop will block (indefinitely) whist waiting for more data. The expectation + // however is that anything that's ready for output should show up in the meantime, + // and not be buffered internally to the `cat` process. + // Hence it's necessary to flush our buffer before every time we could potentially block + // on a `std::io::Read::read` call. + writer.flush().inspect_err(handle_broken_pipe)?; } Ok(()) } -// \r followed by \n is printed as ^M when show_ends is enabled, so that \r\n prints as ^M$ +/// `\r` followed by `\n` is printed as `^M` when `show_ends` is enabled, so that `\r\n` prints as `^M$` fn write_new_line( writer: &mut W, options: &OutputOptions, state: &mut OutputState, is_interactive: bool, ) -> CatResult<()> { - if state.skipped_carriage_return && options.show_ends { - writer.write_all(b"^M")?; + if state.skipped_carriage_return { + if options.show_ends { + writer.write_all(b"^M")?; + } else { + writer.write_all(b"\r")?; + } state.skipped_carriage_return = false; + + write_end_of_line(writer, options.end_of_line().as_bytes(), is_interactive)?; + return Ok(()); } if !state.at_line_start || !options.squeeze_blank || !state.one_blank_kept { state.one_blank_kept = true; if state.at_line_start && options.number == NumberingMode::All { - write!(writer, "{0:6}\t", state.line_number)?; - state.line_number += 1; - } - writer.write_all(options.end_of_line().as_bytes())?; - if is_interactive { - writer.flush()?; + state.line_number.write(writer)?; + state.line_number.increment(); } + write_end_of_line(writer, options.end_of_line().as_bytes(), is_interactive)?; } Ok(()) } @@ -573,8 +650,10 @@ fn write_end(writer: &mut W, in_buf: &[u8], options: &OutputOptions) - // We need to stop at \r because it may be written as ^M depending on the byte after and settings; // however, write_nonprint_to_end doesn't need to stop at \r because it will always write \r as ^M. // Return the number of written symbols + fn write_to_end(in_buf: &[u8], writer: &mut W) -> usize { - match in_buf.iter().position(|c| *c == b'\n' || *c == b'\r') { + // using memchr2 significantly improves performances + match memchr2(b'\n', b'\r', in_buf) { Some(p) => { writer.write_all(&in_buf[..p]).unwrap(); p @@ -606,9 +685,9 @@ fn write_tab_to_end(mut in_buf: &[u8], writer: &mut W) -> usize { } None => { writer.write_all(in_buf).unwrap(); - return in_buf.len(); + return in_buf.len() + count; } - }; + } } } @@ -623,10 +702,10 @@ fn write_nonprint_to_end(in_buf: &[u8], writer: &mut W, tab: &[u8]) -> 9 => writer.write_all(tab), 0..=8 | 10..=31 => writer.write_all(&[b'^', byte + 64]), 32..=126 => writer.write_all(&[byte]), - 127 => writer.write_all(&[b'^', b'?']), + 127 => writer.write_all(b"^?"), 128..=159 => writer.write_all(&[b'M', b'-', b'^', byte - 64]), 160..=254 => writer.write_all(&[b'M', b'-', byte - 128]), - _ => writer.write_all(&[b'M', b'-', b'^', b'?']), + _ => writer.write_all(b"M-^?"), } .unwrap(); count += 1; @@ -641,14 +720,35 @@ fn write_end_of_line( ) -> CatResult<()> { writer.write_all(end_of_line)?; if is_interactive { - writer.flush()?; + writer.flush().inspect_err(handle_broken_pipe)?; } Ok(()) } +fn handle_broken_pipe(error: &io::Error) { + // SIGPIPE is not available on Windows. + if cfg!(target_os = "windows") && error.kind() == ErrorKind::BrokenPipe { + std::process::exit(13); + } +} + #[cfg(test)] mod tests { - use std::io::{stdout, BufWriter}; + use std::io::{BufWriter, stdout}; + + #[test] + fn test_write_tab_to_end_with_newline() { + let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); + let in_buf = b"a\tb\tc\n"; + assert_eq!(super::write_tab_to_end(in_buf, &mut writer), 5); + } + + #[test] + fn test_write_tab_to_end_no_newline() { + let mut writer = BufWriter::with_capacity(1024 * 64, stdout()); + let in_buf = b"a\tb\tc"; + assert_eq!(super::write_tab_to_end(in_buf, &mut writer), 5); + } #[test] fn test_write_nonprint_to_end_new_line() { @@ -689,4 +789,25 @@ mod tests { assert_eq!(writer.buffer(), [b'^', byte + 64]); } } + + #[test] + fn test_incrementing_string() { + let mut incrementing_string = super::LineNumber::new(); + assert_eq!(b" 1\t", incrementing_string.to_str()); + incrementing_string.increment(); + assert_eq!(b" 2\t", incrementing_string.to_str()); + // Run through to 100 + for _ in 3..=100 { + incrementing_string.increment(); + } + assert_eq!(b" 100\t", incrementing_string.to_str()); + // Run through until we overflow the original size. + for _ in 101..=1_000_000 { + incrementing_string.increment(); + } + // Confirm that the start position moves when we overflow the original size. + assert_eq!(b"1000000\t", incrementing_string.to_str()); + incrementing_string.increment(); + assert_eq!(b"1000001\t", incrementing_string.to_str()); + } } diff --git a/src/uu/uptime/src/platform/mod.rs b/src/uu/cat/src/platform/mod.rs similarity index 50% rename from src/uu/uptime/src/platform/mod.rs rename to src/uu/cat/src/platform/mod.rs index e0e87dca1bf..3fa27a27686 100644 --- a/src/uu/uptime/src/platform/mod.rs +++ b/src/uu/cat/src/platform/mod.rs @@ -3,12 +3,14 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -#[cfg(not(target_os = "openbsd"))] +#[cfg(unix)] +pub use self::unix::is_unsafe_overwrite; + +#[cfg(windows)] +pub use self::windows::is_unsafe_overwrite; + +#[cfg(unix)] mod unix; -#[cfg(not(target_os = "openbsd"))] -pub use self::unix::*; -#[cfg(target_os = "openbsd")] -mod openbsd; -#[cfg(target_os = "openbsd")] -pub use self::openbsd::*; +#[cfg(windows)] +mod windows; diff --git a/src/uu/cat/src/platform/unix.rs b/src/uu/cat/src/platform/unix.rs new file mode 100644 index 00000000000..8c55c9a4209 --- /dev/null +++ b/src/uu/cat/src/platform/unix.rs @@ -0,0 +1,108 @@ +// 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 lseek seekable + +use nix::fcntl::{FcntlArg, OFlag, fcntl}; +use nix::unistd::{Whence, lseek}; +use std::os::fd::AsFd; +use uucore::fs::FileInformation; + +/// An unsafe overwrite occurs when the same nonempty file is used as both stdin and stdout, +/// and the file offset of stdin is positioned earlier than that of stdout. +/// In this scenario, bytes read from stdin are written to a later part of the file +/// via stdout, which can then be read again by stdin and written again by stdout, +/// causing an infinite loop and potential file corruption. +pub fn is_unsafe_overwrite(input: &I, output: &O) -> bool { + // `FileInformation::from_file` returns an error if the file descriptor is closed, invalid, + // or refers to a non-regular file (e.g., socket, pipe, or special device). + let Ok(input_info) = FileInformation::from_file(input) else { + return false; + }; + let Ok(output_info) = FileInformation::from_file(output) else { + return false; + }; + if input_info != output_info || output_info.file_size() == 0 { + return false; + } + if is_appending(output) { + return true; + } + // `lseek` 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 Ok(input_pos) = lseek(input.as_fd(), 0, Whence::SeekCur) else { + return false; + }; + let Ok(output_pos) = lseek(output.as_fd(), 0, Whence::SeekCur) else { + return false; + }; + input_pos < output_pos +} + +/// 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) +} + +#[cfg(test)] +mod tests { + use crate::platform::unix::{is_appending, is_unsafe_overwrite}; + use std::fs::OpenOptions; + use std::io::{Seek, SeekFrom, Write}; + use tempfile::NamedTempFile; + + #[test] + fn test_is_appending() { + let temp_file = NamedTempFile::new().unwrap(); + assert!(!is_appending(&temp_file)); + + let read_file = OpenOptions::new().read(true).open(&temp_file).unwrap(); + assert!(!is_appending(&read_file)); + + let write_file = OpenOptions::new().write(true).open(&temp_file).unwrap(); + assert!(!is_appending(&write_file)); + + let append_file = OpenOptions::new().append(true).open(&temp_file).unwrap(); + assert!(is_appending(&append_file)); + } + + #[test] + fn test_is_unsafe_overwrite() { + // Create two temp files one of which is empty + let empty = NamedTempFile::new().unwrap(); + let mut nonempty = NamedTempFile::new().unwrap(); + nonempty.write_all(b"anything").unwrap(); + nonempty.seek(SeekFrom::Start(0)).unwrap(); + + // Using a different file as input and output does not result in an overwrite + assert!(!is_unsafe_overwrite(&empty, &nonempty)); + + // Overwriting an empty file is always safe + assert!(!is_unsafe_overwrite(&empty, &empty)); + + // Overwriting a nonempty file with itself is safe + assert!(!is_unsafe_overwrite(&nonempty, &nonempty)); + + // Overwriting an empty file opened in append mode is safe + let empty_append = OpenOptions::new().append(true).open(&empty).unwrap(); + assert!(!is_unsafe_overwrite(&empty, &empty_append)); + + // Overwriting a nonempty file opened in append mode is unsafe + let nonempty_append = OpenOptions::new().append(true).open(&nonempty).unwrap(); + assert!(is_unsafe_overwrite(&nonempty, &nonempty_append)); + + // Overwriting a file opened in write mode is safe + let mut nonempty_write = OpenOptions::new().write(true).open(&nonempty).unwrap(); + assert!(!is_unsafe_overwrite(&nonempty, &nonempty_write)); + + // Overwriting a file when the input and output file descriptors are pointing to + // different offsets is safe if the input offset is further than the output offset + nonempty_write.seek(SeekFrom::Start(1)).unwrap(); + assert!(!is_unsafe_overwrite(&nonempty_write, &nonempty)); + assert!(is_unsafe_overwrite(&nonempty, &nonempty_write)); + } +} diff --git a/src/uu/cat/src/platform/windows.rs b/src/uu/cat/src/platform/windows.rs new file mode 100644 index 00000000000..ebf375b324e --- /dev/null +++ b/src/uu/cat/src/platform/windows.rs @@ -0,0 +1,56 @@ +// 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 std::ffi::OsString; +use std::os::windows::ffi::OsStringExt; +use std::path::PathBuf; +use uucore::fs::FileInformation; +use winapi_util::AsHandleRef; +use windows_sys::Win32::Storage::FileSystem::{ + FILE_NAME_NORMALIZED, GetFinalPathNameByHandleW, VOLUME_NAME_NT, +}; + +/// An unsafe overwrite occurs when the same file is used as both stdin and stdout +/// and the stdout file is not empty. +pub fn is_unsafe_overwrite(input: &I, output: &O) -> bool { + if !is_same_file_by_path(input, output) { + return false; + } + + // Check if the output file is empty + FileInformation::from_file(output) + .map(|info| info.file_size() > 0) + .unwrap_or(false) +} + +/// Get the file path for a file handle +fn get_file_path_from_handle(file: &F) -> Option { + let handle = file.as_raw(); + let mut path_buf = vec![0u16; 4096]; + + // SAFETY: We should check how many bytes was written to `path_buf` + // and only read that many bytes from it. + let len = unsafe { + GetFinalPathNameByHandleW( + handle, + path_buf.as_mut_ptr(), + path_buf.len() as u32, + FILE_NAME_NORMALIZED | VOLUME_NAME_NT, + ) + }; + if len == 0 { + return None; + } + let path = OsString::from_wide(&path_buf[..len as usize]); + Some(PathBuf::from(path)) +} + +/// Compare two file handles if they correspond to the same file +fn is_same_file_by_path(a: &A, b: &B) -> bool { + match (get_file_path_from_handle(a), get_file_path_from_handle(b)) { + (Some(path1), Some(path2)) => path1 == path2, + _ => false, + } +} diff --git a/src/uu/cat/src/splice.rs b/src/uu/cat/src/splice.rs index 13daae84d7f..ca5265d2bf8 100644 --- a/src/uu/cat/src/splice.rs +++ b/src/uu/cat/src/splice.rs @@ -5,10 +5,7 @@ use super::{CatResult, FdReadable, InputHandle}; use nix::unistd; -use std::os::{ - fd::AsFd, - unix::io::{AsRawFd, RawFd}, -}; +use std::os::{fd::AsFd, unix::io::AsRawFd}; use uucore::pipes::{pipe, splice, splice_exact}; @@ -41,7 +38,7 @@ pub(super) fn write_fast_using_splice( // we can recover by copying the data that we have from the // intermediate pipe to stdout using normal read/write. Then // we tell the caller to fall back. - copy_exact(pipe_rd.as_raw_fd(), write_fd, n)?; + copy_exact(&pipe_rd, write_fd, n)?; return Ok(true); } } @@ -55,7 +52,7 @@ pub(super) fn write_fast_using_splice( /// Move exactly `num_bytes` bytes from `read_fd` to `write_fd`. /// /// Panics if not enough bytes can be read. -fn copy_exact(read_fd: RawFd, write_fd: &impl AsFd, num_bytes: usize) -> nix::Result<()> { +fn copy_exact(read_fd: &impl AsFd, write_fd: &impl AsFd, num_bytes: usize) -> nix::Result<()> { let mut left = num_bytes; let mut buf = [0; BUF_SIZE]; while left > 0 { diff --git a/src/uu/chcon/Cargo.toml b/src/uu/chcon/Cargo.toml index 7498b7cc99d..ab05ed53c0c 100644 --- a/src/uu/chcon/Cargo.toml +++ b/src/uu/chcon/Cargo.toml @@ -1,14 +1,18 @@ [package] name = "uu_chcon" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "chcon ~ (uutils) change file security context" -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/chcon" keywords = ["coreutils", "uutils", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/chcon.rs" @@ -20,6 +24,7 @@ selinux = { workspace = true } thiserror = { workspace = true } libc = { workspace = true } fts-sys = { workspace = true } +fluent = { workspace = true } [[bin]] name = "chcon" diff --git a/src/uu/chcon/chcon.md b/src/uu/chcon/chcon.md deleted file mode 100644 index 64c64f47be7..00000000000 --- a/src/uu/chcon/chcon.md +++ /dev/null @@ -1,11 +0,0 @@ - -# chcon - -``` -chcon [OPTION]... CONTEXT FILE... -chcon [OPTION]... [-u USER] [-r ROLE] [-l RANGE] [-t TYPE] FILE... -chcon [OPTION]... --reference=RFILE FILE... -``` - -Change the SELinux security context of each FILE to CONTEXT. -With --reference, change the security context of each FILE to that of RFILE. diff --git a/src/uu/chcon/locales/en-US.ftl b/src/uu/chcon/locales/en-US.ftl new file mode 100644 index 00000000000..0d18484b01e --- /dev/null +++ b/src/uu/chcon/locales/en-US.ftl @@ -0,0 +1,58 @@ +chcon-about = Change the SELinux security context of each FILE to CONTEXT. + With --reference, change the security context of each FILE to that of RFILE. +chcon-usage = chcon [OPTION]... CONTEXT FILE... + chcon [OPTION]... [-u USER] [-r ROLE] [-l RANGE] [-t TYPE] FILE... + chcon [OPTION]... --reference=RFILE FILE... + +# Help messages +chcon-help-help = Print help information. +chcon-help-dereference = Affect the referent of each symbolic link (this is the default), rather than the symbolic link itself. +chcon-help-no-dereference = Affect symbolic links instead of any referenced file. +chcon-help-preserve-root = Fail to operate recursively on '/'. +chcon-help-no-preserve-root = Do not treat '/' specially (the default). +chcon-help-reference = Use security context of RFILE, rather than specifying a CONTEXT value. +chcon-help-user = Set user USER in the target security context. +chcon-help-role = Set role ROLE in the target security context. +chcon-help-type = Set type TYPE in the target security context. +chcon-help-range = Set range RANGE in the target security context. +chcon-help-recursive = Operate on files and directories recursively. +chcon-help-follow-arg-dir-symlink = If a command line argument is a symbolic link to a directory, traverse it. Only valid when -R is specified. +chcon-help-follow-dir-symlinks = Traverse every symbolic link to a directory encountered. Only valid when -R is specified. +chcon-help-no-follow-symlinks = Do not traverse any symbolic links (default). Only valid when -R is specified. +chcon-help-verbose = Output a diagnostic for every file processed. + +# Error messages - basic validation +chcon-error-no-context-specified = No context is specified +chcon-error-no-files-specified = No files are specified +chcon-error-data-out-of-range = Data is out of range +chcon-error-operation-failed = { $operation } failed +chcon-error-operation-failed-on = { $operation } failed on { $operand } + +# Error messages - argument validation +chcon-error-invalid-context = Invalid security context '{ $context }'. +chcon-error-recursive-no-dereference-require-p = '--recursive' with '--no-dereference' require '-P' +chcon-error-recursive-dereference-require-h-or-l = '--recursive' with '--dereference' require either '-H' or '-L' + +# Operation strings for error context +chcon-op-getting-security-context = Getting security context +chcon-op-file-name-validation = File name validation +chcon-op-getting-meta-data = Getting meta data +chcon-op-modifying-root-path = Modifying root path +chcon-op-accessing = Accessing +chcon-op-reading-directory = Reading directory +chcon-op-reading-cyclic-directory = Reading cyclic directory +chcon-op-applying-partial-context = Applying partial security context to unlabeled file +chcon-op-creating-security-context = Creating security context +chcon-op-setting-security-context-user = Setting security context user +chcon-op-setting-security-context = Setting security context + +# Verbose output +chcon-verbose-changing-context = { $util_name }: changing security context of { $file } + +# Warning messages +chcon-warning-dangerous-recursive-root = It is dangerous to operate recursively on '/'. Use --{ $option } to override this failsafe. +chcon-warning-dangerous-recursive-dir = It is dangerous to operate recursively on { $dir } (same as '/'). Use --{ $option } to override this failsafe. +chcon-warning-circular-directory = Circular directory structure. + This almost certainly means that you have a corrupted file system. + NOTIFY YOUR SYSTEM MANAGER. + The following directory is part of the cycle { $file }. diff --git a/src/uu/chcon/locales/fr-FR.ftl b/src/uu/chcon/locales/fr-FR.ftl new file mode 100644 index 00000000000..3fd2cd09a8f --- /dev/null +++ b/src/uu/chcon/locales/fr-FR.ftl @@ -0,0 +1,58 @@ +chcon-about = Changer le contexte de sécurité SELinux de chaque FICHIER vers CONTEXTE. + Avec --reference, changer le contexte de sécurité de chaque FICHIER vers celui de RFICHIER. +chcon-usage = chcon [OPTION]... CONTEXTE FICHIER... + chcon [OPTION]... [-u UTILISATEUR] [-r RÔLE] [-l PLAGE] [-t TYPE] FICHIER... + chcon [OPTION]... --reference=RFICHIER FICHIER... + +# Messages d'aide +chcon-help-help = Afficher les informations d'aide. +chcon-help-dereference = Affecter la cible de chaque lien symbolique (par défaut), plutôt que le lien symbolique lui-même. +chcon-help-no-dereference = Affecter les liens symboliques au lieu de tout fichier référencé. +chcon-help-preserve-root = Échouer lors de l'opération récursive sur '/'. +chcon-help-no-preserve-root = Ne pas traiter '/' spécialement (par défaut). +chcon-help-reference = Utiliser le contexte de sécurité de RFICHIER, plutôt que de spécifier une valeur CONTEXTE. +chcon-help-user = Définir l'utilisateur UTILISATEUR dans le contexte de sécurité cible. +chcon-help-role = Définir le rôle RÔLE dans le contexte de sécurité cible. +chcon-help-type = Définir le type TYPE dans le contexte de sécurité cible. +chcon-help-range = Définir la plage PLAGE dans le contexte de sécurité cible. +chcon-help-recursive = Opérer sur les fichiers et répertoires de manière récursive. +chcon-help-follow-arg-dir-symlink = Si un argument de ligne de commande est un lien symbolique vers un répertoire, le traverser. Valide uniquement quand -R est spécifié. +chcon-help-follow-dir-symlinks = Traverser chaque lien symbolique vers un répertoire rencontré. Valide uniquement quand -R est spécifié. +chcon-help-no-follow-symlinks = Ne traverser aucun lien symbolique (par défaut). Valide uniquement quand -R est spécifié. +chcon-help-verbose = Afficher un diagnostic pour chaque fichier traité. + +# Messages d'erreur - validation de base +chcon-error-no-context-specified = Aucun contexte n'est spécifié +chcon-error-no-files-specified = Aucun fichier n'est spécifié +chcon-error-data-out-of-range = Données hors limites +chcon-error-operation-failed = { $operation } a échoué +chcon-error-operation-failed-on = { $operation } a échoué sur { $operand } + +# Messages d'erreur - validation des arguments +chcon-error-invalid-context = Contexte de sécurité invalide '{ $context }'. +chcon-error-recursive-no-dereference-require-p = '--recursive' avec '--no-dereference' nécessite '-P' +chcon-error-recursive-dereference-require-h-or-l = '--recursive' avec '--dereference' nécessite soit '-H' soit '-L' + +# Chaînes d'opération pour le contexte d'erreur +chcon-op-getting-security-context = Obtention du contexte de sécurité +chcon-op-file-name-validation = Validation du nom de fichier +chcon-op-getting-meta-data = Obtention des métadonnées +chcon-op-modifying-root-path = Modification du chemin racine +chcon-op-accessing = Accès +chcon-op-reading-directory = Lecture du répertoire +chcon-op-reading-cyclic-directory = Lecture du répertoire cyclique +chcon-op-applying-partial-context = Application d'un contexte de sécurité partiel à un fichier non étiqueté +chcon-op-creating-security-context = Création du contexte de sécurité +chcon-op-setting-security-context-user = Définition de l'utilisateur du contexte de sécurité +chcon-op-setting-security-context = Définition du contexte de sécurité + +# Sortie détaillée +chcon-verbose-changing-context = { $util_name } : changement du contexte de sécurité de { $file } + +# Messages d'avertissement +chcon-warning-dangerous-recursive-root = Il est dangereux d'opérer récursivement sur '/'. Utilisez --{ $option } pour outrepasser cette protection. +chcon-warning-dangerous-recursive-dir = Il est dangereux d'opérer récursivement sur { $dir } (identique à '/'). Utilisez --{ $option } pour outrepasser cette protection. +chcon-warning-circular-directory = Structure de répertoire circulaire. + Cela signifie presque certainement que vous avez un système de fichiers corrompu. + NOTIFIEZ VOTRE ADMINISTRATEUR SYSTÈME. + Le répertoire suivant fait partie du cycle { $file }. diff --git a/src/uu/chcon/src/chcon.rs b/src/uu/chcon/src/chcon.rs index 1a804bd3bbf..25c2d099e5f 100644 --- a/src/uu/chcon/src/chcon.rs +++ b/src/uu/chcon/src/chcon.rs @@ -3,13 +3,16 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore (vars) RFILE +#![cfg(target_os = "linux")] #![allow(clippy::upper_case_acronyms)] use clap::builder::ValueParser; +use uucore::LocalizedCommand; use uucore::error::{UResult, USimpleError, UUsageError}; -use uucore::{display::Quotable, format_usage, help_about, help_usage, show_error, show_warning}; +use uucore::translate; +use uucore::{display::Quotable, format_usage, show_error, show_warning}; -use clap::{crate_version, Arg, ArgAction, Command}; +use clap::{Arg, ArgAction, Command}; use selinux::{OpaqueSecurityContext, SecurityContext}; use std::borrow::Cow; @@ -23,9 +26,6 @@ mod fts; use errors::*; -const ABOUT: &str = help_about!("chcon.md"); -const USAGE: &str = help_usage!("chcon.md"); - pub mod options { pub static HELP: &str = "help"; pub static VERBOSE: &str = "verbose"; @@ -78,10 +78,17 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Ok(None) => { let err = io::Error::from_raw_os_error(libc::ENODATA); - Err(Error::from_io1("Getting security context", reference, err)) + Err(Error::from_io1( + translate!("chcon-op-getting-security-context"), + reference, + err, + )) } - Err(r) => Err(Error::from_selinux("Getting security context", r)), + Err(r) => Err(Error::from_selinux( + translate!("chcon-op-getting-security-context"), + r, + )), }; match result { @@ -103,7 +110,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Err(_r) => { return Err(USimpleError::new( libc::EXIT_FAILURE, - format!("Invalid security context {}.", context.quote()), + translate!("chcon-error-invalid-context", "context" => context.quote()), )); } }; @@ -111,7 +118,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if SecurityContext::from_c_str(&c_context, false).check() == Some(false) { return Err(USimpleError::new( libc::EXIT_FAILURE, - format!("Invalid security context {}.", context.quote()), + translate!("chcon-error-invalid-context", "context" => context.quote()), )); } @@ -149,46 +156,44 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) + .about(translate!("chcon-about")) + .override_usage(format_usage(&translate!("chcon-usage"))) .infer_long_args(true) .disable_help_flag(true) .args_override_self(true) .arg( Arg::new(options::HELP) .long(options::HELP) - .help("Print help information.") + .help(translate!("chcon-help-help")) .action(ArgAction::Help), ) .arg( Arg::new(options::dereference::DEREFERENCE) .long(options::dereference::DEREFERENCE) .overrides_with(options::dereference::NO_DEREFERENCE) - .help( - "Affect the referent of each symbolic link (this is the default), \ - rather than the symbolic link itself.", - ) + .help(translate!("chcon-help-dereference")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::dereference::NO_DEREFERENCE) .short('h') .long(options::dereference::NO_DEREFERENCE) - .help("Affect symbolic links instead of any referenced file.") + .help(translate!("chcon-help-no-dereference")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::preserve_root::PRESERVE_ROOT) .long(options::preserve_root::PRESERVE_ROOT) .overrides_with(options::preserve_root::NO_PRESERVE_ROOT) - .help("Fail to operate recursively on '/'.") + .help(translate!("chcon-help-preserve-root")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::preserve_root::NO_PRESERVE_ROOT) .long(options::preserve_root::NO_PRESERVE_ROOT) - .help("Do not treat '/' specially (the default).") + .help(translate!("chcon-help-no-preserve-root")) .action(ArgAction::SetTrue), ) .arg( @@ -197,10 +202,7 @@ pub fn uu_app() -> Command { .value_name("RFILE") .value_hint(clap::ValueHint::FilePath) .conflicts_with_all([options::USER, options::ROLE, options::TYPE, options::RANGE]) - .help( - "Use security context of RFILE, rather than specifying \ - a CONTEXT value.", - ) + .help(translate!("chcon-help-reference")) .value_parser(ValueParser::os_string()), ) .arg( @@ -209,7 +211,7 @@ pub fn uu_app() -> Command { .long(options::USER) .value_name("USER") .value_hint(clap::ValueHint::Username) - .help("Set user USER in the target security context.") + .help(translate!("chcon-help-user")) .value_parser(ValueParser::os_string()), ) .arg( @@ -217,7 +219,7 @@ pub fn uu_app() -> Command { .short('r') .long(options::ROLE) .value_name("ROLE") - .help("Set role ROLE in the target security context.") + .help(translate!("chcon-help-role")) .value_parser(ValueParser::os_string()), ) .arg( @@ -225,7 +227,7 @@ pub fn uu_app() -> Command { .short('t') .long(options::TYPE) .value_name("TYPE") - .help("Set type TYPE in the target security context.") + .help(translate!("chcon-help-type")) .value_parser(ValueParser::os_string()), ) .arg( @@ -233,14 +235,14 @@ pub fn uu_app() -> Command { .short('l') .long(options::RANGE) .value_name("RANGE") - .help("Set range RANGE in the target security context.") + .help(translate!("chcon-help-range")) .value_parser(ValueParser::os_string()), ) .arg( Arg::new(options::RECURSIVE) .short('R') .long(options::RECURSIVE) - .help("Operate on files and directories recursively.") + .help(translate!("chcon-help-recursive")) .action(ArgAction::SetTrue), ) .arg( @@ -251,10 +253,7 @@ pub fn uu_app() -> Command { options::sym_links::FOLLOW_DIR_SYM_LINKS, options::sym_links::NO_FOLLOW_SYM_LINKS, ]) - .help( - "If a command line argument is a symbolic link to a directory, \ - traverse it. Only valid when -R is specified.", - ) + .help(translate!("chcon-help-follow-arg-dir-symlink")) .action(ArgAction::SetTrue), ) .arg( @@ -265,10 +264,7 @@ pub fn uu_app() -> Command { options::sym_links::FOLLOW_ARG_DIR_SYM_LINK, options::sym_links::NO_FOLLOW_SYM_LINKS, ]) - .help( - "Traverse every symbolic link to a directory encountered. \ - Only valid when -R is specified.", - ) + .help(translate!("chcon-help-follow-dir-symlinks")) .action(ArgAction::SetTrue), ) .arg( @@ -279,17 +275,14 @@ pub fn uu_app() -> Command { options::sym_links::FOLLOW_ARG_DIR_SYM_LINK, options::sym_links::FOLLOW_DIR_SYM_LINKS, ]) - .help( - "Do not traverse any symbolic links (default). \ - Only valid when -R is specified.", - ) + .help(translate!("chcon-help-no-follow-symlinks")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::VERBOSE) .short('v') .long(options::VERBOSE) - .help("Output a diagnostic for every file processed.") + .help(translate!("chcon-help-verbose")) .action(ArgAction::SetTrue), ) .arg( @@ -311,38 +304,32 @@ struct Options { files: Vec, } -fn parse_command_line(config: clap::Command, args: impl uucore::Args) -> Result { - let matches = config.try_get_matches_from(args)?; +fn parse_command_line(config: Command, args: impl uucore::Args) -> Result { + let matches = config.get_matches_from_localized(args); let verbose = matches.get_flag(options::VERBOSE); let (recursive_mode, affect_symlink_referent) = if matches.get_flag(options::RECURSIVE) { if matches.get_flag(options::sym_links::FOLLOW_DIR_SYM_LINKS) { if matches.get_flag(options::dereference::NO_DEREFERENCE) { - return Err(Error::ArgumentsMismatch(format!( - "'--{}' with '--{}' require '-P'", - options::RECURSIVE, - options::dereference::NO_DEREFERENCE + return Err(Error::ArgumentsMismatch(translate!( + "chcon-error-recursive-no-dereference-require-p" ))); } (RecursiveMode::RecursiveAndFollowAllDirSymLinks, true) } else if matches.get_flag(options::sym_links::FOLLOW_ARG_DIR_SYM_LINK) { if matches.get_flag(options::dereference::NO_DEREFERENCE) { - return Err(Error::ArgumentsMismatch(format!( - "'--{}' with '--{}' require '-P'", - options::RECURSIVE, - options::dereference::NO_DEREFERENCE + return Err(Error::ArgumentsMismatch(translate!( + "chcon-error-recursive-no-dereference-require-p" ))); } (RecursiveMode::RecursiveAndFollowArgDirSymLinks, true) } else { if matches.get_flag(options::dereference::DEREFERENCE) { - return Err(Error::ArgumentsMismatch(format!( - "'--{}' with '--{}' require either '-H' or '-L'", - options::RECURSIVE, - options::dereference::DEREFERENCE + return Err(Error::ArgumentsMismatch(translate!( + "chcon-error-recursive-dereference-require-h-or-l" ))); } @@ -516,12 +503,19 @@ fn process_file( let mut entry = fts.last_entry_ref().unwrap(); let file_full_name = entry.path().map(PathBuf::from).ok_or_else(|| { - Error::from_io("File name validation", io::ErrorKind::InvalidInput.into()) + Error::from_io( + translate!("chcon-op-file-name-validation"), + io::ErrorKind::InvalidInput.into(), + ) })?; let fts_access_path = entry.access_path().ok_or_else(|| { let err = io::ErrorKind::InvalidInput.into(); - Error::from_io1("File name validation", &file_full_name, err) + Error::from_io1( + translate!("chcon-op-file-name-validation"), + &file_full_name, + err, + ) })?; let err = |s, k: io::ErrorKind| Error::from_io1(s, &file_full_name, k.into()); @@ -535,7 +529,10 @@ fn process_file( let file_dev_ino: DeviceAndINode = if let Some(st) = entry.stat() { st.try_into()? } else { - return Err(err("Getting meta data", io::ErrorKind::InvalidInput)); + return Err(err( + translate!("chcon-op-getting-meta-data"), + io::ErrorKind::InvalidInput, + )); }; let mut result = Ok(()); @@ -554,7 +551,10 @@ fn process_file( // Ensure that we do not process "/" on the second visit. let _ignored = fts.read_next_entry(); - return Err(err("Modifying root path", io::ErrorKind::PermissionDenied)); + return Err(err( + translate!("chcon-op-modifying-root-path"), + io::ErrorKind::PermissionDenied, + )); } return Ok(()); @@ -579,17 +579,20 @@ fn process_file( return Ok(()); } - result = fts_err("Accessing"); + result = fts_err(translate!("chcon-op-accessing")); } - fts_sys::FTS_ERR => result = fts_err("Accessing"), + fts_sys::FTS_ERR => result = fts_err(translate!("chcon-op-accessing")), - fts_sys::FTS_DNR => result = fts_err("Reading directory"), + fts_sys::FTS_DNR => result = fts_err(translate!("chcon-op-reading-directory")), fts_sys::FTS_DC => { if cycle_warning_required(options.recursive_mode.fts_open_options(), &entry) { emit_cycle_warning(&file_full_name); - return Err(err("Reading cyclic directory", io::ErrorKind::InvalidData)); + return Err(err( + translate!("chcon-op-reading-cyclic-directory"), + io::ErrorKind::InvalidData, + )); } } @@ -601,15 +604,17 @@ fn process_file( && root_dev_ino_check(root_dev_ino, file_dev_ino) { root_dev_ino_warn(&file_full_name); - result = Err(err("Modifying root path", io::ErrorKind::PermissionDenied)); + result = Err(err( + translate!("chcon-op-modifying-root-path"), + io::ErrorKind::PermissionDenied, + )); } if result.is_ok() { if options.verbose { println!( - "{}: Changing security context of: {}", - uucore::util_name(), - file_full_name.quote() + "{}", + translate!("chcon-verbose-changing-context", "util_name" => uucore::util_name(), "file" => file_full_name.quote()) ); } @@ -637,7 +642,7 @@ fn change_file_context( let err0 = || -> Result<()> { // If the file doesn't have a context, and we're not setting all of the context // components, there isn't really an obvious default. Thus, we just give up. - let op = "Applying partial security context to unlabeled file"; + let op = translate!("chcon-op-applying-partial-context"); let err = io::ErrorKind::InvalidInput.into(); Err(Error::from_io1(op, path, err)) }; @@ -647,20 +652,30 @@ fn change_file_context( Ok(Some(context)) => context, Ok(None) => return err0(), - Err(r) => return Err(Error::from_selinux("Getting security context", r)), + Err(r) => { + return Err(Error::from_selinux( + translate!("chcon-op-getting-security-context"), + r, + )); + } }; let c_file_context = match file_context.to_c_string() { Ok(Some(context)) => context, Ok(None) => return err0(), - Err(r) => return Err(Error::from_selinux("Getting security context", r)), + Err(r) => { + return Err(Error::from_selinux( + translate!("chcon-op-getting-security-context"), + r, + )); + } }; let se_context = OpaqueSecurityContext::from_c_str(c_file_context.as_ref()).map_err(|_r| { let err = io::ErrorKind::InvalidInput.into(); - Error::from_io1("Creating security context", path, err) + Error::from_io1(translate!("chcon-op-creating-security-context"), path, err) })?; type SetValueProc = fn(&OpaqueSecurityContext, &CStr) -> selinux::errors::Result<()>; @@ -676,24 +691,27 @@ fn change_file_context( if let Some(new_value) = new_value { let c_new_value = os_str_to_c_string(new_value).map_err(|_r| { let err = io::ErrorKind::InvalidInput.into(); - Error::from_io1("Creating security context", path, err) + Error::from_io1(translate!("chcon-op-creating-security-context"), path, err) })?; - set_value_proc(&se_context, &c_new_value) - .map_err(|r| Error::from_selinux("Setting security context user", r))?; + set_value_proc(&se_context, &c_new_value).map_err(|r| { + Error::from_selinux(translate!("chcon-op-setting-security-context-user"), r) + })?; } } - let context_string = se_context - .to_c_string() - .map_err(|r| Error::from_selinux("Getting security context", r))?; + let context_string = se_context.to_c_string().map_err(|r| { + Error::from_selinux(translate!("chcon-op-getting-security-context"), r) + })?; if c_file_context.as_ref().to_bytes() == context_string.as_ref().to_bytes() { Ok(()) // Nothing to change. } else { SecurityContext::from_c_str(&context_string, false) .set_for_path(path, options.affect_symlink_referent, false) - .map_err(|r| Error::from_selinux("Setting security context", r)) + .map_err(|r| { + Error::from_selinux(translate!("chcon-op-setting-security-context"), r) + }) } } @@ -701,10 +719,16 @@ fn change_file_context( if let Some(c_context) = context.to_c_string()? { SecurityContext::from_c_str(c_context.as_ref(), false) .set_for_path(path, options.affect_symlink_referent, false) - .map_err(|r| Error::from_selinux("Setting security context", r)) + .map_err(|r| { + Error::from_selinux(translate!("chcon-op-setting-security-context"), r) + }) } else { let err = io::ErrorKind::InvalidInput.into(); - Err(Error::from_io1("Setting security context", path, err)) + Err(Error::from_io1( + translate!("chcon-op-setting-security-context"), + path, + err, + )) } } } @@ -727,33 +751,30 @@ fn get_root_dev_ino() -> Result { } fn root_dev_ino_check(root_dev_ino: Option, dir_dev_ino: DeviceAndINode) -> bool { - root_dev_ino.map_or(false, |root_dev_ino| root_dev_ino == dir_dev_ino) + root_dev_ino == Some(dir_dev_ino) } fn root_dev_ino_warn(dir_name: &Path) { if dir_name.as_os_str() == "/" { show_warning!( - "It is dangerous to operate recursively on '/'. \ - Use --{} to override this failsafe.", - options::preserve_root::NO_PRESERVE_ROOT, + "{}", + translate!("chcon-warning-dangerous-recursive-root", "option" => options::preserve_root::NO_PRESERVE_ROOT) ); } else { show_warning!( - "It is dangerous to operate recursively on {} (same as '/'). \ - Use --{} to override this failsafe.", - dir_name.quote(), - options::preserve_root::NO_PRESERVE_ROOT, + "{}", + translate!("chcon-warning-dangerous-recursive-dir", "dir" => dir_name.to_string_lossy(), "option" => options::preserve_root::NO_PRESERVE_ROOT) ); } } -// When fts_read returns FTS_DC to indicate a directory cycle, it may or may not indicate -// a real problem. -// When a program like chgrp performs a recursive traversal that requires traversing symbolic links, -// it is *not* a problem. -// However, when invoked with "-P -R", it deserves a warning. -// The fts_options parameter records the options that control this aspect of fts behavior, -// so test that. +/// When `fts_read` returns [`fts_sys::FTS_DC`] to indicate a directory cycle, it may or may not indicate +/// a real problem. +/// When a program like chgrp performs a recursive traversal that requires traversing symbolic links, +/// it is *not* a problem. +/// However, when invoked with "-P -R", it deserves a warning. +/// The `fts_options` parameter records the options that control this aspect of fts behavior, +/// so test that. fn cycle_warning_required(fts_options: c_int, entry: &fts::EntryRef) -> bool { // When dereferencing no symlinks, or when dereferencing only those listed on the command line // and we're not processing a command-line argument, then a cycle is a serious problem. @@ -763,11 +784,8 @@ fn cycle_warning_required(fts_options: c_int, entry: &fts::EntryRef) -> bool { fn emit_cycle_warning(file_name: &Path) { show_warning!( - "Circular directory structure.\n\ -This almost certainly means that you have a corrupted file system.\n\ -NOTIFY YOUR SYSTEM MANAGER.\n\ -The following directory is part of the cycle {}.", - file_name.quote() + "{}", + translate!("chcon-warning-circular-directory", "file" => file_name.to_string_lossy()) ); } @@ -777,8 +795,8 @@ enum SELinuxSecurityContext<'t> { String(Option), } -impl<'t> SELinuxSecurityContext<'t> { - fn to_c_string(&self) -> Result>> { +impl SELinuxSecurityContext<'_> { + fn to_c_string(&self) -> Result>> { match self { Self::File(context) => context .to_c_string() diff --git a/src/uu/chcon/src/errors.rs b/src/uu/chcon/src/errors.rs index 10d5735a0c6..76ffeeb6ad5 100644 --- a/src/uu/chcon/src/errors.rs +++ b/src/uu/chcon/src/errors.rs @@ -2,23 +2,27 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +#![cfg(target_os = "linux")] + use std::ffi::OsString; use std::fmt::Write; use std::io; +use thiserror::Error; use uucore::display::Quotable; +use uucore::translate; pub(crate) type Result = std::result::Result; -#[derive(thiserror::Error, Debug)] +#[derive(Error, Debug)] pub(crate) enum Error { - #[error("No context is specified")] + #[error("{}", translate!("chcon-error-no-context-specified"))] MissingContext, - #[error("No files are specified")] + #[error("{}", translate!("chcon-error-no-files-specified"))] MissingFiles, - #[error("Data is out of range")] + #[error("{}", translate!("chcon-error-data-out-of-range"))] OutOfRange, #[error("{0}")] @@ -27,45 +31,57 @@ pub(crate) enum Error { #[error(transparent)] CommandLine(#[from] clap::Error), - #[error("{operation} failed")] + #[error("{}", translate!("chcon-error-operation-failed", "operation" => operation.clone()))] SELinux { - operation: &'static str, + operation: String, + #[source] source: selinux::errors::Error, }, - #[error("{operation} failed")] + #[error("{}", translate!("chcon-error-operation-failed", "operation" => operation.clone()))] Io { - operation: &'static str, + operation: String, + #[source] source: io::Error, }, - #[error("{operation} failed on {}", .operand1.quote())] + #[error("{}", translate!("chcon-error-operation-failed-on", "operation" => operation.clone(), "operand" => operand1.quote()))] Io1 { - operation: &'static str, + operation: String, operand1: OsString, + #[source] source: io::Error, }, } impl Error { - pub(crate) fn from_io(operation: &'static str, source: io::Error) -> Self { - Self::Io { operation, source } + pub(crate) fn from_io(operation: impl Into, source: io::Error) -> Self { + Self::Io { + operation: operation.into(), + source, + } } pub(crate) fn from_io1( - operation: &'static str, + operation: impl Into, operand1: impl Into, source: io::Error, ) -> Self { Self::Io1 { - operation, + operation: operation.into(), operand1: operand1.into(), source, } } - pub(crate) fn from_selinux(operation: &'static str, source: selinux::errors::Error) -> Self { - Self::SELinux { operation, source } + pub(crate) fn from_selinux( + operation: impl Into, + source: selinux::errors::Error, + ) -> Self { + Self::SELinux { + operation: operation.into(), + source, + } } } diff --git a/src/uu/chcon/src/fts.rs b/src/uu/chcon/src/fts.rs index a81cb39b658..b60ac7d3ace 100644 --- a/src/uu/chcon/src/fts.rs +++ b/src/uu/chcon/src/fts.rs @@ -2,11 +2,12 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +#![cfg(target_os = "linux")] + use std::ffi::{CStr, CString, OsStr}; use std::marker::PhantomData; use std::os::raw::{c_int, c_long, c_short}; use std::path::Path; -use std::ptr::NonNull; use std::{io, iter, ptr, slice}; use crate::errors::{Error, Result}; @@ -60,7 +61,7 @@ impl FTS { }) } - pub(crate) fn last_entry_ref(&mut self) -> Option { + pub(crate) fn last_entry_ref(&mut self) -> Option> { self.entry.map(move |entry| EntryRef::new(self, entry)) } @@ -69,7 +70,7 @@ impl FTS { // pointer assumed to be valid. let new_entry = unsafe { fts_sys::fts_read(self.fts.as_ptr()) }; - self.entry = NonNull::new(new_entry); + self.entry = ptr::NonNull::new(new_entry); if self.entry.is_none() { let r = io::Error::last_os_error(); if let Some(0) = r.raw_os_error() { @@ -159,7 +160,7 @@ impl<'fts> EntryRef<'fts> { return None; } - NonNull::new(entry.fts_path) + ptr::NonNull::new(entry.fts_path) .map(|path_ptr| { let path_size = usize::from(entry.fts_pathlen).saturating_add(1); diff --git a/src/uu/chcon/src/main.rs b/src/uu/chcon/src/main.rs index d93d7d1da2b..d1354d840af 100644 --- a/src/uu/chcon/src/main.rs +++ b/src/uu/chcon/src/main.rs @@ -1 +1,2 @@ +#![cfg(target_os = "linux")] uucore::bin!(uu_chcon); diff --git a/src/uu/chgrp/Cargo.toml b/src/uu/chgrp/Cargo.toml index 21745317e9c..2a629542b29 100644 --- a/src/uu/chgrp/Cargo.toml +++ b/src/uu/chgrp/Cargo.toml @@ -1,15 +1,18 @@ [package] name = "uu_chgrp" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "chgrp ~ (uutils) change the group ownership of FILE" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/chgrp" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/chgrp.rs" @@ -17,6 +20,7 @@ path = "src/chgrp.rs" [dependencies] clap = { workspace = true } uucore = { workspace = true, features = ["entries", "fs", "perms"] } +fluent = { workspace = true } [[bin]] name = "chgrp" diff --git a/src/uu/chgrp/chgrp.md b/src/uu/chgrp/chgrp.md deleted file mode 100644 index 79bc068d2d3..00000000000 --- a/src/uu/chgrp/chgrp.md +++ /dev/null @@ -1,10 +0,0 @@ - - -# chgrp - -``` -chgrp [OPTION]... GROUP FILE... -chgrp [OPTION]... --reference=RFILE FILE... -``` - -Change the group of each FILE to GROUP. diff --git a/src/uu/chgrp/locales/en-US.ftl b/src/uu/chgrp/locales/en-US.ftl new file mode 100644 index 00000000000..c9bcf6c866a --- /dev/null +++ b/src/uu/chgrp/locales/en-US.ftl @@ -0,0 +1,20 @@ +chgrp-about = Change the group of each FILE to GROUP. +chgrp-usage = chgrp [OPTION]... GROUP FILE... + chgrp [OPTION]... --reference=RFILE FILE... + +# Help messages +chgrp-help-print-help = Print help information. +chgrp-help-changes = like verbose but report only when a change is made +chgrp-help-quiet = suppress most error messages +chgrp-help-verbose = output a diagnostic for every file processed +chgrp-help-preserve-root = fail to operate recursively on '/' +chgrp-help-no-preserve-root = do not treat '/' specially (the default) +chgrp-help-reference = use RFILE's group rather than specifying GROUP values +chgrp-help-from = change the group only if its current group matches GROUP +chgrp-help-recursive = operate on files and directories recursively + +# Error messages +chgrp-error-invalid-group-id = invalid group id: '{ $gid_str }' +chgrp-error-invalid-group = invalid group: '{ $group }' +chgrp-error-failed-to-get-attributes = failed to get attributes of { $file } +chgrp-error-invalid-user = invalid user: '{ $from_group }' diff --git a/src/uu/chgrp/locales/fr-FR.ftl b/src/uu/chgrp/locales/fr-FR.ftl new file mode 100644 index 00000000000..d25f0058b73 --- /dev/null +++ b/src/uu/chgrp/locales/fr-FR.ftl @@ -0,0 +1,20 @@ +chgrp-about = Changer le groupe de chaque FICHIER vers GROUPE. +chgrp-usage = chgrp [OPTION]... GROUPE FICHIER... + chgrp [OPTION]... --reference=RFICHIER FICHIER... + +# Messages d'aide +chgrp-help-print-help = Afficher les informations d'aide. +chgrp-help-changes = comme verbeux mais rapporter seulement lors d'un changement +chgrp-help-quiet = supprimer la plupart des messages d'erreur +chgrp-help-verbose = afficher un diagnostic pour chaque fichier traité +chgrp-help-preserve-root = échouer à opérer récursivement sur '/' +chgrp-help-no-preserve-root = ne pas traiter '/' spécialement (par défaut) +chgrp-help-reference = utiliser le groupe de RFICHIER plutôt que spécifier les valeurs de GROUPE +chgrp-help-from = changer le groupe seulement si son groupe actuel correspond à GROUPE +chgrp-help-recursive = opérer sur les fichiers et répertoires récursivement + +# Messages d'erreur +chgrp-error-invalid-group-id = identifiant de groupe invalide : '{ $gid_str }' +chgrp-error-invalid-group = groupe invalide : '{ $group }' +chgrp-error-failed-to-get-attributes = échec de l'obtention des attributs de { $file } +chgrp-error-invalid-user = utilisateur invalide : '{ $from_group }' diff --git a/src/uu/chgrp/src/chgrp.rs b/src/uu/chgrp/src/chgrp.rs index fba2cef1611..07859d07d05 100644 --- a/src/uu/chgrp/src/chgrp.rs +++ b/src/uu/chgrp/src/chgrp.rs @@ -6,29 +6,48 @@ // spell-checker:ignore (ToDO) COMFOLLOW Chowner RFILE RFILE's derefer dgid nonblank nonprint nonprinting use uucore::display::Quotable; -pub use uucore::entries; +use uucore::entries; use uucore::error::{FromIo, UResult, USimpleError}; -use uucore::perms::{chown_base, options, GidUidOwnerFilter, IfFrom}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; +use uucore::perms::{GidUidOwnerFilter, IfFrom, chown_base, options}; +use uucore::translate; -use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; +use clap::{Arg, ArgAction, ArgMatches, Command}; use std::fs; use std::os::unix::fs::MetadataExt; -const ABOUT: &str = help_about!("chgrp.md"); -const USAGE: &str = help_usage!("chgrp.md"); +fn parse_gid_from_str(group: &str) -> Result { + if let Some(gid_str) = group.strip_prefix(':') { + // Handle :gid format + gid_str + .parse::() + .map_err(|_| translate!("chgrp-error-invalid-group-id", "gid_str" => gid_str)) + } else { + // Try as group name first + match entries::grp2gid(group) { + Ok(g) => Ok(g), + // If group name lookup fails, try parsing as raw number + Err(_) => group + .parse::() + .map_err(|_| translate!("chgrp-error-invalid-group", "group" => group)), + } + } +} -fn parse_gid_and_uid(matches: &ArgMatches) -> UResult { - let mut raw_group: String = String::new(); - let dest_gid = if let Some(file) = matches.get_one::(options::REFERENCE) { - fs::metadata(file) +fn get_dest_gid(matches: &ArgMatches) -> UResult<(Option, String)> { + let mut raw_group = String::new(); + let dest_gid = if let Some(file) = matches.get_one::(options::REFERENCE) { + let path = std::path::Path::new(file); + fs::metadata(path) .map(|meta| { let gid = meta.gid(); raw_group = entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()); Some(gid) }) - .map_err_context(|| format!("failed to get attributes of {}", file.quote()))? + .map_err_context( + || translate!("chgrp-error-failed-to-get-attributes", "file" => path.quote()), + )? } else { let group = matches .get_one::(options::ARG_GROUP) @@ -38,22 +57,38 @@ fn parse_gid_and_uid(matches: &ArgMatches) -> UResult { if group.is_empty() { None } else { - match entries::grp2gid(group) { + match parse_gid_from_str(group) { Ok(g) => Some(g), - _ => { - return Err(USimpleError::new( - 1, - format!("invalid group: {}", group.quote()), - )) - } + Err(e) => return Err(USimpleError::new(1, e)), } } }; + Ok((dest_gid, raw_group)) +} + +fn parse_gid_and_uid(matches: &ArgMatches) -> UResult { + let (dest_gid, raw_group) = get_dest_gid(matches)?; + + // Handle --from option + let filter = if let Some(from_group) = matches.get_one::(options::FROM) { + match parse_gid_from_str(from_group) { + Ok(g) => IfFrom::Group(g), + Err(_) => { + return Err(USimpleError::new( + 1, + translate!("chgrp-error-invalid-user", "from_group" => from_group), + )); + } + } + } else { + IfFrom::All + }; + Ok(GidUidOwnerFilter { dest_gid, dest_uid: None, raw_owner: raw_group, - filter: IfFrom::All, + filter, }) } @@ -64,22 +99,23 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) + .about(translate!("chgrp-about")) + .override_usage(format_usage(&translate!("chgrp-usage"))) .infer_long_args(true) .disable_help_flag(true) .arg( Arg::new(options::HELP) .long(options::HELP) - .help("Print help information.") - .action(ArgAction::Help) + .help(translate!("chgrp-help-print-help")) + .action(ArgAction::Help), ) .arg( Arg::new(options::verbosity::CHANGES) .short('c') .long(options::verbosity::CHANGES) - .help("like verbose but report only when a change is made") + .help(translate!("chgrp-help-changes")) .action(ArgAction::SetTrue), ) .arg( @@ -91,40 +127,26 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::verbosity::QUIET) .long(options::verbosity::QUIET) - .help("suppress most error messages") + .help(translate!("chgrp-help-quiet")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::verbosity::VERBOSE) .short('v') .long(options::verbosity::VERBOSE) - .help("output a diagnostic for every file processed") + .help(translate!("chgrp-help-verbose")) .action(ArgAction::SetTrue), ) - .arg( - Arg::new(options::dereference::DEREFERENCE) - .long(options::dereference::DEREFERENCE) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::dereference::NO_DEREFERENCE) - .short('h') - .long(options::dereference::NO_DEREFERENCE) - .help( - "affect symbolic links instead of any referenced file (useful only on systems that can change the ownership of a symlink)", - ) - .action(ArgAction::SetTrue), - ) .arg( Arg::new(options::preserve_root::PRESERVE) .long(options::preserve_root::PRESERVE) - .help("fail to operate recursively on '/'") + .help(translate!("chgrp-help-preserve-root")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::preserve_root::NO_PRESERVE) .long(options::preserve_root::NO_PRESERVE) - .help("do not treat '/' specially (the default)") + .help(translate!("chgrp-help-no-preserve-root")) .action(ArgAction::SetTrue), ) .arg( @@ -132,32 +154,22 @@ pub fn uu_app() -> Command { .long(options::REFERENCE) .value_name("RFILE") .value_hint(clap::ValueHint::FilePath) - .help("use RFILE's group rather than specifying GROUP values"), + .value_parser(clap::value_parser!(std::ffi::OsString)) + .help(translate!("chgrp-help-reference")), + ) + .arg( + Arg::new(options::FROM) + .long(options::FROM) + .value_name("GROUP") + .help(translate!("chgrp-help-from")), ) .arg( Arg::new(options::RECURSIVE) .short('R') .long(options::RECURSIVE) - .help("operate on files and directories recursively") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::traverse::TRAVERSE) - .short(options::traverse::TRAVERSE.chars().next().unwrap()) - .help("if a command line argument is a symbolic link to a directory, traverse it") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::traverse::NO_TRAVERSE) - .short(options::traverse::NO_TRAVERSE.chars().next().unwrap()) - .help("do not traverse any symbolic links (default)") - .overrides_with_all([options::traverse::TRAVERSE, options::traverse::EVERY]) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::traverse::EVERY) - .short(options::traverse::EVERY.chars().next().unwrap()) - .help("traverse every symbolic link to a directory encountered") + .help(translate!("chgrp-help-recursive")) .action(ArgAction::SetTrue), ) + // Add common arguments with chgrp, chown & chmod + .args(uucore::perms::common_args()) } diff --git a/src/uu/chmod/Cargo.toml b/src/uu/chmod/Cargo.toml index 2a6a46474cd..8d48f343a91 100644 --- a/src/uu/chmod/Cargo.toml +++ b/src/uu/chmod/Cargo.toml @@ -1,23 +1,27 @@ [package] name = "uu_chmod" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "chmod ~ (uutils) change mode of FILE" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/chmod" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/chmod.rs" [dependencies] clap = { workspace = true } -libc = { workspace = true } -uucore = { workspace = true, features = ["fs", "mode"] } +thiserror = { workspace = true } +uucore = { workspace = true, features = ["entries", "fs", "mode", "perms"] } +fluent = { workspace = true } [[bin]] name = "chmod" diff --git a/src/uu/chmod/chmod.md b/src/uu/chmod/chmod.md deleted file mode 100644 index d6c2ed2d8e3..00000000000 --- a/src/uu/chmod/chmod.md +++ /dev/null @@ -1,16 +0,0 @@ - - -# chmod - -``` -chmod [OPTION]... MODE[,MODE]... FILE... -chmod [OPTION]... OCTAL-MODE FILE... -chmod [OPTION]... --reference=RFILE FILE... -``` - -Change the mode of each FILE to MODE. -With --reference, change the mode of each FILE to that of RFILE. - -## After Help - -Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+'. diff --git a/src/uu/chmod/locales/en-US.ftl b/src/uu/chmod/locales/en-US.ftl new file mode 100644 index 00000000000..52447f26399 --- /dev/null +++ b/src/uu/chmod/locales/en-US.ftl @@ -0,0 +1,31 @@ +chmod-about = Change the mode of each FILE to MODE. + With --reference, change the mode of each FILE to that of RFILE. +chmod-usage = chmod [OPTION]... MODE[,MODE]... FILE... + chmod [OPTION]... OCTAL-MODE FILE... + chmod [OPTION]... --reference=RFILE FILE... +chmod-after-help = Each MODE is of the form [ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+. +chmod-error-cannot-stat = cannot stat attributes of {$file} +chmod-error-dangling-symlink = cannot operate on dangling symlink {$file} +chmod-error-no-such-file = cannot access {$file}: No such file or directory +chmod-error-preserve-root = it is dangerous to operate recursively on {$file} + chmod: use --no-preserve-root to override this failsafe +chmod-error-permission-denied = {$file}: Permission denied +chmod-error-new-permissions = {$file}: new permissions are {$actual}, not {$expected} +chmod-error-missing-operand = missing operand + +# Help messages +chmod-help-print-help = Print help information. +chmod-help-changes = like verbose but report only when a change is made +chmod-help-quiet = suppress most error messages +chmod-help-verbose = output a diagnostic for every file processed +chmod-help-no-preserve-root = do not treat '/' specially (the default) +chmod-help-preserve-root = fail to operate recursively on '/' +chmod-help-recursive = change files and directories recursively +chmod-help-reference = use RFILE's mode instead of MODE values + +# Verbose messages +chmod-verbose-failed-dangling = failed to change mode of {$file} from 0000 (---------) to 1500 (r-x-----T) +chmod-verbose-neither-changed = neither symbolic link {$file} nor referent has been changed +chmod-verbose-mode-retained = mode of {$file} retained as {$mode_octal} ({$mode_display}) +chmod-verbose-failed-change = failed to change mode of file {$file} from {$old_mode} ({$old_mode_display}) to {$new_mode} ({$new_mode_display}) +chmod-verbose-mode-changed = mode of {$file} changed from {$old_mode} ({$old_mode_display}) to {$new_mode} ({$new_mode_display}) diff --git a/src/uu/chmod/locales/fr-FR.ftl b/src/uu/chmod/locales/fr-FR.ftl new file mode 100644 index 00000000000..0518356281d --- /dev/null +++ b/src/uu/chmod/locales/fr-FR.ftl @@ -0,0 +1,33 @@ +chmod-about = Changer le mode de chaque FICHIER vers MODE. + Avec --reference, changer le mode de chaque FICHIER vers celui de RFICHIER. +chmod-usage = chmod [OPTION]... MODE[,MODE]... FICHIER... + chmod [OPTION]... MODE-OCTAL FICHIER... + chmod [OPTION]... --reference=RFICHIER FICHIER... +chmod-after-help = Chaque MODE est de la forme [ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+. + +# Messages d'aide +chmod-help-print-help = Afficher les informations d'aide. +chmod-help-changes = comme verbeux mais rapporter seulement lors d'un changement +chmod-help-quiet = supprimer la plupart des messages d'erreur +chmod-help-verbose = afficher un diagnostic pour chaque fichier traité +chmod-help-no-preserve-root = ne pas traiter '/' spécialement (par défaut) +chmod-help-preserve-root = échouer à opérer récursivement sur '/' +chmod-help-recursive = changer les fichiers et répertoires récursivement +chmod-help-reference = utiliser le mode de RFICHIER au lieu des valeurs de MODE + +# Messages d'erreur +chmod-error-cannot-stat = impossible d'obtenir les attributs de {$file} +chmod-error-dangling-symlink = impossible d'opérer sur le lien symbolique pendouillant {$file} +chmod-error-no-such-file = impossible d'accéder à {$file} : Aucun fichier ou dossier de ce type +chmod-error-preserve-root = il est dangereux d'opérer récursivement sur {$file} + chmod: utiliser --no-preserve-root pour outrepasser cette protection +chmod-error-permission-denied = {$file} : Permission refusée +chmod-error-new-permissions = {$file} : les nouvelles permissions sont {$actual}, pas {$expected} +chmod-error-missing-operand = opérande manquant + +# Messages verbeux/de statut +chmod-verbose-failed-dangling = échec du changement de mode de {$file} de 0000 (---------) vers 1500 (r-x-----T) +chmod-verbose-neither-changed = ni le lien symbolique {$file} ni la référence n'ont été changés +chmod-verbose-mode-retained = mode de {$file} conservé comme {$mode_octal} ({$mode_display}) +chmod-verbose-failed-change = échec du changement de mode du fichier {$file} de {$old_mode} ({$old_mode_display}) vers {$new_mode} ({$new_mode_display}) +chmod-verbose-mode-changed = mode de {$file} changé de {$old_mode} ({$old_mode_display}) vers {$new_mode} ({$new_mode_display}) diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index d1325743782..d8152d8bdc6 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -5,24 +5,43 @@ // spell-checker:ignore (ToDO) Chmoder cmode fmode fperm fref ugoa RFILE RFILE's -use clap::{crate_version, Arg, ArgAction, Command}; +use clap::{Arg, ArgAction, Command}; use std::ffi::OsString; use std::fs; use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::path::Path; +use thiserror::Error; +use uucore::LocalizedCommand; use uucore::display::Quotable; -use uucore::error::{set_exit_code, ExitCode, UResult, USimpleError, UUsageError}; +use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError, set_exit_code}; use uucore::fs::display_permissions_unix; use uucore::libc::mode_t; -#[cfg(not(windows))] use uucore::mode; -use uucore::{format_usage, help_about, help_section, help_usage, show, show_error}; +use uucore::perms::{TraverseSymlinks, configure_symlink_and_recursion}; +use uucore::{format_usage, show, show_error}; -const ABOUT: &str = help_about!("chmod.md"); -const USAGE: &str = help_usage!("chmod.md"); -const LONG_USAGE: &str = help_section!("after help", "chmod.md"); +use uucore::translate; + +#[derive(Debug, Error)] +enum ChmodError { + #[error("{}", translate!("chmod-error-cannot-stat", "file" => _0.quote()))] + CannotStat(String), + #[error("{}", translate!("chmod-error-dangling-symlink", "file" => _0.quote()))] + DanglingSymlink(String), + #[error("{}", translate!("chmod-error-no-such-file", "file" => _0.quote()))] + NoSuchFile(String), + #[error("{}", translate!("chmod-error-preserve-root", "file" => _0.quote()))] + PreserveRoot(String), + #[error("{}", translate!("chmod-error-permission-denied", "file" => _0.quote()))] + PermissionDenied(String), + #[error("{}", translate!("chmod-error-new-permissions", "file" => _0.clone(), "actual" => _1.clone(), "expected" => _2.clone()))] + NewPermissions(String, String, String), +} + +impl UError for ChmodError {} mod options { + pub const HELP: &str = "help"; pub const CHANGES: &str = "changes"; pub const QUIET: &str = "quiet"; // visible_alias("silent") pub const VERBOSE: &str = "verbose"; @@ -92,21 +111,19 @@ fn extract_negative_modes(mut args: impl uucore::Args) -> (Option, Vec UResult<()> { let (parsed_cmode, args) = extract_negative_modes(args.skip(1)); // skip binary name - let matches = uu_app().after_help(LONG_USAGE).try_get_matches_from(args)?; + let matches = uu_app() + .after_help(translate!("chmod-after-help")) + .get_matches_from_localized(args); let changes = matches.get_flag(options::CHANGES); let quiet = matches.get_flag(options::QUIET); let verbose = matches.get_flag(options::VERBOSE); let preserve_root = matches.get_flag(options::PRESERVE_ROOT); - let recursive = matches.get_flag(options::RECURSIVE); - let fmode = match matches.get_one::(options::REFERENCE) { + let fmode = match matches.get_one::(options::REFERENCE) { Some(fref) => match fs::metadata(fref) { Ok(meta) => Some(meta.mode() & 0o7777), - Err(err) => { - return Err(USimpleError::new( - 1, - format!("cannot stat attributes of {}: {}", fref.quote(), err), - )) + Err(_) => { + return Err(ChmodError::CannotStat(fref.to_string_lossy().to_string()).into()); } }, None => None, @@ -118,25 +135,30 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else { modes.unwrap().to_string() // modes is required }; - // FIXME: enable non-utf8 paths - let mut files: Vec = matches - .get_many::(options::FILE) - .map(|v| v.map(ToString::to_string).collect()) + let mut files: Vec = matches + .get_many::(options::FILE) + .map(|v| v.cloned().collect()) .unwrap_or_default(); let cmode = if fmode.is_some() { // "--reference" and MODE are mutually exclusive // if "--reference" was used MODE needs to be interpreted as another FILE // it wasn't possible to implement this behavior directly with clap - files.push(cmode); + files.push(OsString::from(cmode)); None } else { Some(cmode) }; if files.is_empty() { - return Err(UUsageError::new(1, "missing operand".to_string())); + return Err(UUsageError::new( + 1, + translate!("chmod-error-missing-operand"), + )); } + let (recursive, dereference, traverse_symlinks) = + configure_symlink_and_recursion(&matches, TraverseSymlinks::First)?; + let chmoder = Chmoder { changes, quiet, @@ -145,6 +167,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { recursive, fmode, cmode, + traverse_symlinks, + dereference, }; chmoder.chmod(&files) @@ -152,17 +176,25 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) + .about(translate!("chmod-about")) + .override_usage(format_usage(&translate!("chmod-usage"))) .args_override_self(true) .infer_long_args(true) .no_binary_name(true) + .disable_help_flag(true) + .arg( + Arg::new(options::HELP) + .long(options::HELP) + .help(translate!("chmod-help-print-help")) + .action(ArgAction::Help), + ) .arg( Arg::new(options::CHANGES) .long(options::CHANGES) .short('c') - .help("like verbose but report only when a change is made") + .help(translate!("chmod-help-changes")) .action(ArgAction::SetTrue), ) .arg( @@ -170,52 +202,57 @@ pub fn uu_app() -> Command { .long(options::QUIET) .visible_alias("silent") .short('f') - .help("suppress most error messages") + .help(translate!("chmod-help-quiet")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::VERBOSE) .long(options::VERBOSE) .short('v') - .help("output a diagnostic for every file processed") + .help(translate!("chmod-help-verbose")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::NO_PRESERVE_ROOT) .long(options::NO_PRESERVE_ROOT) - .help("do not treat '/' specially (the default)") + .help(translate!("chmod-help-no-preserve-root")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::PRESERVE_ROOT) .long(options::PRESERVE_ROOT) - .help("fail to operate recursively on '/'") + .help(translate!("chmod-help-preserve-root")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::RECURSIVE) .long(options::RECURSIVE) .short('R') - .help("change files and directories recursively") + .help(translate!("chmod-help-recursive")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::REFERENCE) .long("reference") .value_hint(clap::ValueHint::FilePath) - .help("use RFILE's mode instead of MODE values"), + .value_parser(clap::value_parser!(OsString)) + .help(translate!("chmod-help-reference")), ) .arg( - Arg::new(options::MODE).required_unless_present(options::REFERENCE), // It would be nice if clap could parse with delimiter, e.g. "g-x,u+x", - // however .multiple_occurrences(true) cannot be used here because FILE already needs that. - // Only one positional argument with .multiple_occurrences(true) set is allowed per command + Arg::new(options::MODE).required_unless_present(options::REFERENCE), + // It would be nice if clap could parse with delimiter, e.g. "g-x,u+x", + // however .multiple_occurrences(true) cannot be used here because FILE already needs that. + // Only one positional argument with .multiple_occurrences(true) set is allowed per command ) .arg( Arg::new(options::FILE) .required_unless_present(options::MODE) .action(ArgAction::Append) - .value_hint(clap::ValueHint::AnyPath), + .value_hint(clap::ValueHint::AnyPath) + .value_parser(clap::value_parser!(OsString)), ) + // Add common arguments with chgrp, chown & chmod + .args(uucore::perms::common_args()) } struct Chmoder { @@ -226,54 +263,60 @@ struct Chmoder { recursive: bool, fmode: Option, cmode: Option, + traverse_symlinks: TraverseSymlinks, + dereference: bool, } impl Chmoder { - fn chmod(&self, files: &[String]) -> UResult<()> { + fn chmod(&self, files: &[OsString]) -> UResult<()> { let mut r = Ok(()); for filename in files { - let filename = &filename[..]; let file = Path::new(filename); if !file.exists() { if file.is_symlink() { + if !self.dereference && !self.recursive { + // The file is a symlink and we should not follow it + // Don't try to change the mode of the symlink itself + continue; + } + if self.recursive && self.traverse_symlinks == TraverseSymlinks::None { + continue; + } + if !self.quiet { - show!(USimpleError::new( - 1, - format!("cannot operate on dangling symlink {}", filename.quote()), + show!(ChmodError::DanglingSymlink( + filename.to_string_lossy().to_string() )); + set_exit_code(1); } + if self.verbose { println!( - "failed to change mode of {} from 0000 (---------) to 1500 (r-x-----T)", - filename.quote() + "{}", + translate!("chmod-verbose-failed-dangling", "file" => filename.to_string_lossy().quote()) ); } } else if !self.quiet { - show!(USimpleError::new( - 1, - format!( - "cannot access {}: No such file or directory", - filename.quote() - ) + show!(ChmodError::NoSuchFile( + filename.to_string_lossy().to_string() )); } // GNU exits with exit code 1 even if -q or --quiet are passed // So we set the exit code, because it hasn't been set yet if `self.quiet` is true. set_exit_code(1); continue; + } else if !self.dereference && file.is_symlink() { + // The file is a symlink and we should not follow it + // chmod 755 --no-dereference a/link + // should not change the permissions in this case + continue; } - if self.recursive && self.preserve_root && filename == "/" { - return Err(USimpleError::new( - 1, - format!( - "it is dangerous to operate recursively on {}\nchmod: use --no-preserve-root to override this failsafe", - filename.quote() - ) - )); + if self.recursive && self.preserve_root && file == Path::new("/") { + return Err(ChmodError::PreserveRoot("/".to_string()).into()); } if self.recursive { - r = self.walk_dir(file); + r = self.walk_dir_with_context(file, true); } else { r = self.chmod_file(file).and(r); } @@ -281,53 +324,94 @@ impl Chmoder { r } - fn walk_dir(&self, file_path: &Path) -> UResult<()> { + fn walk_dir_with_context(&self, file_path: &Path, is_command_line_arg: bool) -> UResult<()> { let mut r = self.chmod_file(file_path); - if !file_path.is_symlink() && file_path.is_dir() { + + // Determine whether to traverse symlinks based on context and traversal mode + let should_follow_symlink = match self.traverse_symlinks { + TraverseSymlinks::All => true, + TraverseSymlinks::First => is_command_line_arg, // Only follow symlinks that are command line args + TraverseSymlinks::None => false, + }; + + // If the path is a directory (or we should follow symlinks), recurse into it + if (!file_path.is_symlink() || should_follow_symlink) && file_path.is_dir() { for dir_entry in file_path.read_dir()? { - let path = dir_entry?.path(); - if !path.is_symlink() { - r = self.walk_dir(path.as_path()); + let path = match dir_entry { + Ok(entry) => entry.path(), + Err(err) => { + r = r.and(Err(err.into())); + continue; + } + }; + if path.is_symlink() { + r = self.handle_symlink_during_recursion(&path).and(r); + } else { + r = self.walk_dir_with_context(path.as_path(), false).and(r); } } } r } - #[cfg(windows)] - fn chmod_file(&self, file: &Path) -> UResult<()> { - // chmod is useless on Windows - // it doesn't set any permissions at all - // instead it just sets the readonly attribute on the file - Ok(()) + fn handle_symlink_during_recursion(&self, path: &Path) -> UResult<()> { + // During recursion, determine behavior based on traversal mode + match self.traverse_symlinks { + TraverseSymlinks::All => { + // Follow all symlinks during recursion + // Check if the symlink target is a directory, but handle dangling symlinks gracefully + match fs::metadata(path) { + Ok(meta) if meta.is_dir() => self.walk_dir_with_context(path, false), + Ok(_) => { + // It's a file symlink, chmod it + self.chmod_file(path) + } + Err(_) => { + // Dangling symlink, chmod it without dereferencing + self.chmod_file_internal(path, false) + } + } + } + TraverseSymlinks::First | TraverseSymlinks::None => { + // Don't follow symlinks encountered during recursion + // For these symlinks, don't dereference them even if dereference is normally true + self.chmod_file_internal(path, false) + } + } } - #[cfg(unix)] + fn chmod_file(&self, file: &Path) -> UResult<()> { - use uucore::mode::get_umask; + self.chmod_file_internal(file, self.dereference) + } - let fperm = match fs::metadata(file) { + fn chmod_file_internal(&self, file: &Path, dereference: bool) -> UResult<()> { + use uucore::{mode::get_umask, perms::get_metadata}; + + let metadata = get_metadata(file, dereference); + + let fperm = match metadata { Ok(meta) => meta.mode() & 0o7777, Err(err) => { - if file.is_symlink() { + // Handle dangling symlinks or other errors + return if file.is_symlink() && !dereference { if self.verbose { println!( "neither symbolic link {} nor referent has been changed", file.quote() ); } - return Ok(()); + Ok(()) // Skip dangling symlinks } else if err.kind() == std::io::ErrorKind::PermissionDenied { // These two filenames would normally be conditionally // quoted, but GNU's tests expect them to always be quoted - return Err(USimpleError::new( - 1, - format!("{}: Permission denied", file.quote()), - )); + Err(ChmodError::PermissionDenied(file.to_string_lossy().to_string()).into()) } else { - return Err(USimpleError::new(1, format!("{}: {}", file.quote(), err))); - } + Err(ChmodError::CannotStat(file.to_string_lossy().to_string()).into()) + }; } }; + + // Determine the new permissions to apply match self.fmode { Some(mode) => self.change_file(fperm, mode, file)?, None => { @@ -366,18 +450,28 @@ impl Chmoder { } } - self.change_file(fperm, new_mode, file)?; + // Special handling for symlinks when not dereferencing + if file.is_symlink() && !dereference { + // TODO: On most Unix systems, symlink permissions are ignored by the kernel, + // so changing them has no effect. We skip this operation for compatibility. + // Note that "chmod without dereferencing" effectively does nothing on symlinks. + if self.verbose { + println!( + "neither symbolic link {} nor referent has been changed", + file.quote() + ); + } + } else { + self.change_file(fperm, new_mode, file)?; + } // if a permission would have been removed if umask was 0, but it wasn't because umask was not 0, print an error and fail if (new_mode & !naively_expected_new_mode) != 0 { - return Err(USimpleError::new( - 1, - format!( - "{}: new permissions are {}, not {}", - file.maybe_quote(), - display_permissions_unix(new_mode as mode_t, false), - display_permissions_unix(naively_expected_new_mode as mode_t, false) - ), - )); + return Err(ChmodError::NewPermissions( + file.to_string_lossy().to_string(), + display_permissions_unix(new_mode as mode_t, false), + display_permissions_unix(naively_expected_new_mode as mode_t, false), + ) + .into()); } } } @@ -385,29 +479,25 @@ impl Chmoder { Ok(()) } - #[cfg(unix)] fn change_file(&self, fperm: u32, mode: u32, file: &Path) -> Result<(), i32> { if fperm == mode { if self.verbose && !self.changes { println!( - "mode of {} retained as {:04o} ({})", + "mode of {} retained as {fperm:04o} ({})", file.quote(), - fperm, display_permissions_unix(fperm as mode_t, false), ); } Ok(()) } else if let Err(err) = fs::set_permissions(file, fs::Permissions::from_mode(mode)) { if !self.quiet { - show_error!("{}", err); + show_error!("{err}"); } if self.verbose { println!( - "failed to change mode of file {} from {:04o} ({}) to {:04o} ({})", + "failed to change mode of file {} from {fperm:04o} ({}) to {mode:04o} ({})", file.quote(), - fperm, display_permissions_unix(fperm as mode_t, false), - mode, display_permissions_unix(mode as mode_t, false) ); } @@ -415,11 +505,9 @@ impl Chmoder { } else { if self.verbose || self.changes { println!( - "mode of {} changed from {:04o} ({}) to {:04o} ({})", + "mode of {} changed from {fperm:04o} ({}) to {mode:04o} ({})", file.quote(), - fperm, display_permissions_unix(fperm as mode_t, false), - mode, display_permissions_unix(mode as mode_t, false) ); } diff --git a/src/uu/chown/Cargo.toml b/src/uu/chown/Cargo.toml index ae4c969c3c9..5fa279650ea 100644 --- a/src/uu/chown/Cargo.toml +++ b/src/uu/chown/Cargo.toml @@ -1,15 +1,18 @@ [package] name = "uu_chown" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "chown ~ (uutils) change the ownership of FILE" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/chown" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/chown.rs" @@ -17,6 +20,7 @@ path = "src/chown.rs" [dependencies] clap = { workspace = true } uucore = { workspace = true, features = ["entries", "fs", "perms"] } +fluent = { workspace = true } [[bin]] name = "chown" diff --git a/src/uu/chown/chown.md b/src/uu/chown/chown.md deleted file mode 100644 index 83101c74c73..00000000000 --- a/src/uu/chown/chown.md +++ /dev/null @@ -1,9 +0,0 @@ - -# chown - -``` -chown [OPTION]... [OWNER][:[GROUP]] FILE... -chown [OPTION]... --reference=RFILE FILE... -``` - -Change file owner and group diff --git a/src/uu/chown/locales/en-US.ftl b/src/uu/chown/locales/en-US.ftl new file mode 100644 index 00000000000..0dfe8301e9d --- /dev/null +++ b/src/uu/chown/locales/en-US.ftl @@ -0,0 +1,23 @@ +chown-about = Change file owner and group +chown-usage = chown [OPTION]... [OWNER][:[GROUP]] FILE... + chown [OPTION]... --reference=RFILE FILE... + +# Help messages +chown-help-print-help = Print help information. +chown-help-changes = like verbose but report only when a change is made +chown-help-from = change the owner and/or group of each file only if its + current owner and/or group match those specified here. + Either may be omitted, in which case a match is not required + for the omitted attribute +chown-help-preserve-root = fail to operate recursively on '/' +chown-help-no-preserve-root = do not treat '/' specially (the default) +chown-help-quiet = suppress most error messages +chown-help-recursive = operate on files and directories recursively +chown-help-reference = use RFILE's owner and group rather than specifying OWNER:GROUP values +chown-help-verbose = output a diagnostic for every file processed + +# Error messages +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 } diff --git a/src/uu/chown/locales/fr-FR.ftl b/src/uu/chown/locales/fr-FR.ftl new file mode 100644 index 00000000000..48e39853a3e --- /dev/null +++ b/src/uu/chown/locales/fr-FR.ftl @@ -0,0 +1,23 @@ +chown-about = Changer le propriétaire et le groupe des fichiers +chown-usage = chown [OPTION]... [PROPRIÉTAIRE][:[GROUPE]] FICHIER... + chown [OPTION]... --reference=RFICHIER FICHIER... + +# Messages d'aide +chown-help-print-help = Afficher les informations d'aide. +chown-help-changes = comme verbeux mais rapporter seulement lors d'un changement +chown-help-from = changer le propriétaire et/ou le groupe de chaque fichier seulement si son + propriétaire et/ou groupe actuel correspondent à ceux spécifiés ici. + L'un ou l'autre peut être omis, auquel cas une correspondance n'est pas requise + pour l'attribut omis +chown-help-preserve-root = échouer à opérer récursivement sur '/' +chown-help-no-preserve-root = ne pas traiter '/' spécialement (par défaut) +chown-help-quiet = supprimer la plupart des messages d'erreur +chown-help-recursive = opérer sur les fichiers et répertoires récursivement +chown-help-reference = utiliser le propriétaire et groupe de RFICHIER plutôt que spécifier les valeurs PROPRIÉTAIRE:GROUPE +chown-help-verbose = afficher un diagnostic pour chaque fichier traité + +# Messages d'erreur +chown-error-failed-to-get-attributes = échec de l'obtention des attributs de { $file } +chown-error-invalid-user = utilisateur invalide : { $user } +chown-error-invalid-group = groupe invalide : { $group } +chown-error-invalid-spec = spécification invalide : { $spec } diff --git a/src/uu/chown/src/chown.rs b/src/uu/chown/src/chown.rs index 0e9b8b2423c..ceff36b59fe 100644 --- a/src/uu/chown/src/chown.rs +++ b/src/uu/chown/src/chown.rs @@ -7,20 +7,17 @@ use uucore::display::Quotable; pub use uucore::entries::{self, Group, Locate, Passwd}; -use uucore::perms::{chown_base, options, GidUidOwnerFilter, IfFrom}; -use uucore::{format_usage, help_about, help_usage}; +use uucore::format_usage; +use uucore::perms::{GidUidOwnerFilter, IfFrom, chown_base, options}; +use uucore::translate; use uucore::error::{FromIo, UResult, USimpleError}; -use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; +use clap::{Arg, ArgAction, ArgMatches, Command}; use std::fs; use std::os::unix::fs::MetadataExt; -static ABOUT: &str = help_about!("chown.md"); - -const USAGE: &str = help_usage!("chown.md"); - fn parse_gid_uid_and_filter(matches: &ArgMatches) -> UResult { let filter = if let Some(spec) = matches.get_one::(options::FROM) { match parse_spec(spec, ':')? { @@ -37,8 +34,9 @@ fn parse_gid_uid_and_filter(matches: &ArgMatches) -> UResult let dest_gid: Option; let raw_owner: String; if let Some(file) = matches.get_one::(options::REFERENCE) { - let meta = fs::metadata(file) - .map_err_context(|| format!("failed to get attributes of {}", file.quote()))?; + let meta = fs::metadata(file).map_err_context( + || translate!("chown-error-failed-to-get-attributes", "file" => file.quote()), + )?; let gid = meta.gid(); let uid = meta.uid(); dest_gid = Some(gid); @@ -78,83 +76,60 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) + .about(translate!("chown-about")) + .override_usage(format_usage(&translate!("chown-usage"))) .infer_long_args(true) .disable_help_flag(true) .arg( Arg::new(options::HELP) .long(options::HELP) - .help("Print help information.") + .help(translate!("chown-help-print-help")) .action(ArgAction::Help), ) .arg( Arg::new(options::verbosity::CHANGES) .short('c') .long(options::verbosity::CHANGES) - .help("like verbose but report only when a change is made") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::dereference::DEREFERENCE) - .long(options::dereference::DEREFERENCE) - .help( - "affect the referent of each symbolic link (this is the default), \ - rather than the symbolic link itself", - ) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::dereference::NO_DEREFERENCE) - .short('h') - .long(options::dereference::NO_DEREFERENCE) - .help( - "affect symbolic links instead of any referenced file \ - (useful only on systems that can change the ownership of a symlink)", - ) + .help(translate!("chown-help-changes")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::FROM) .long(options::FROM) - .help( - "change the owner and/or group of each file only if its \ - current owner and/or group match those specified here. \ - Either may be omitted, in which case a match is not required \ - for the omitted attribute", - ) + .help(translate!("chown-help-from")) .value_name("CURRENT_OWNER:CURRENT_GROUP"), ) .arg( Arg::new(options::preserve_root::PRESERVE) .long(options::preserve_root::PRESERVE) - .help("fail to operate recursively on '/'") + .help(translate!("chown-help-preserve-root")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::preserve_root::NO_PRESERVE) .long(options::preserve_root::NO_PRESERVE) - .help("do not treat '/' specially (the default)") + .help(translate!("chown-help-no-preserve-root")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::verbosity::QUIET) .long(options::verbosity::QUIET) - .help("suppress most error messages") + .help(translate!("chown-help-quiet")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::RECURSIVE) .short('R') .long(options::RECURSIVE) - .help("operate on files and directories recursively") + .help(translate!("chown-help-recursive")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::REFERENCE) .long(options::REFERENCE) - .help("use RFILE's owner and group rather than specifying OWNER:GROUP values") + .help(translate!("chown-help-reference")) .value_name("RFILE") .value_hint(clap::ValueHint::FilePath) .num_args(1..), @@ -165,34 +140,15 @@ pub fn uu_app() -> Command { .long(options::verbosity::SILENT) .action(ArgAction::SetTrue), ) - .arg( - Arg::new(options::traverse::TRAVERSE) - .short(options::traverse::TRAVERSE.chars().next().unwrap()) - .help("if a command line argument is a symbolic link to a directory, traverse it") - .overrides_with_all([options::traverse::EVERY, options::traverse::NO_TRAVERSE]) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::traverse::EVERY) - .short(options::traverse::EVERY.chars().next().unwrap()) - .help("traverse every symbolic link to a directory encountered") - .overrides_with_all([options::traverse::TRAVERSE, options::traverse::NO_TRAVERSE]) - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new(options::traverse::NO_TRAVERSE) - .short(options::traverse::NO_TRAVERSE.chars().next().unwrap()) - .help("do not traverse any symbolic links (default)") - .overrides_with_all([options::traverse::TRAVERSE, options::traverse::EVERY]) - .action(ArgAction::SetTrue), - ) .arg( Arg::new(options::verbosity::VERBOSE) .long(options::verbosity::VERBOSE) .short('v') - .help("output a diagnostic for every file processed") + .help(translate!("chown-help-verbose")) .action(ArgAction::SetTrue), ) + // Add common arguments with chgrp, chown & chmod + .args(uucore::perms::common_args()) } /// Parses the user string to extract the UID. @@ -217,7 +173,7 @@ fn parse_uid(user: &str, spec: &str, sep: char) -> UResult> { Ok(uid) => Ok(Some(uid)), Err(_) => Err(USimpleError::new( 1, - format!("invalid user: {}", spec.quote()), + translate!("chown-error-invalid-user", "user" => spec.quote()), )), } } @@ -236,7 +192,7 @@ fn parse_gid(group: &str, spec: &str) -> UResult> { Ok(gid) => Ok(Some(gid)), Err(_) => Err(USimpleError::new( 1, - format!("invalid group: {}", spec.quote()), + translate!("chown-error-invalid-group", "group" => spec.quote()), )), }, } @@ -263,15 +219,12 @@ fn parse_spec(spec: &str, sep: char) -> UResult<(Option, Option)> { let uid = parse_uid(user, spec, sep)?; let gid = parse_gid(group, spec)?; - if user.chars().next().map(char::is_numeric).unwrap_or(false) - && group.is_empty() - && spec != user - { + if user.chars().next().is_some_and(char::is_numeric) && group.is_empty() && spec != user { // if the arg starts with an id numeric value, the group isn't set but the separator is provided, // we should fail with an error return Err(USimpleError::new( 1, - format!("invalid spec: {}", spec.quote()), + translate!("chown-error-invalid-spec", "spec" => spec.quote()), )); } @@ -281,9 +234,15 @@ fn parse_spec(spec: &str, sep: char) -> UResult<(Option, Option)> { #[cfg(test)] mod test { use super::*; + use std::env; + use uucore::locale; #[test] fn test_parse_spec() { + unsafe { + env::set_var("LANG", "C"); + } + let _ = locale::setup_localization("chown"); assert!(matches!(parse_spec(":", ':'), Ok((None, None)))); assert!(matches!(parse_spec(".", ':'), Ok((None, None)))); assert!(matches!(parse_spec(".", '.'), Ok((None, None)))); diff --git a/src/uu/chroot/Cargo.toml b/src/uu/chroot/Cargo.toml index d6090ec8c18..d251c98231d 100644 --- a/src/uu/chroot/Cargo.toml +++ b/src/uu/chroot/Cargo.toml @@ -1,22 +1,27 @@ [package] name = "uu_chroot" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "chroot ~ (uutils) run COMMAND under a new root directory" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/chroot" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/chroot.rs" [dependencies] clap = { workspace = true } +thiserror = { workspace = true } uucore = { workspace = true, features = ["entries", "fs"] } +fluent = { workspace = true } [[bin]] name = "chroot" diff --git a/src/uu/chroot/chroot.md b/src/uu/chroot/chroot.md deleted file mode 100644 index 3967d08f963..00000000000 --- a/src/uu/chroot/chroot.md +++ /dev/null @@ -1,8 +0,0 @@ - -# chroot - -``` -chroot [OPTION]... NEWROOT [COMMAND [ARG]...] -``` - -Run COMMAND with root directory set to NEWROOT. diff --git a/src/uu/chroot/locales/en-US.ftl b/src/uu/chroot/locales/en-US.ftl new file mode 100644 index 00000000000..24c109c406e --- /dev/null +++ b/src/uu/chroot/locales/en-US.ftl @@ -0,0 +1,25 @@ +chroot-about = Run COMMAND with root directory set to NEWROOT. +chroot-usage = chroot [OPTION]... NEWROOT [COMMAND [ARG]...] + +# Help messages +chroot-help-groups = Comma-separated list of groups to switch to +chroot-help-userspec = Colon-separated user and group to switch to. +chroot-help-skip-chdir = Use this option to not change the working directory to / after changing the root directory to newroot, i.e., inside the chroot. + +# Error messages +chroot-error-skip-chdir-only-permitted = option --skip-chdir only permitted if NEWROOT is old '/' +chroot-error-cannot-enter = cannot chroot to { $dir }: { $err } +chroot-error-command-failed = failed to run command { $cmd }: { $err } +chroot-error-command-not-found = failed to run command { $cmd }: { $err } +chroot-error-groups-parsing-failed = --groups parsing failed +chroot-error-invalid-group = invalid group: { $group } +chroot-error-invalid-group-list = invalid group list: { $list } +chroot-error-missing-newroot = Missing operand: NEWROOT + Try '{ $util_name } --help' for more information. +chroot-error-no-group-specified = no group specified for unknown uid: { $uid } +chroot-error-no-such-user = invalid user +chroot-error-no-such-group = invalid group +chroot-error-no-such-directory = cannot change root directory to { $dir }: no such directory +chroot-error-set-gid-failed = cannot set gid to { $gid }: { $err } +chroot-error-set-groups-failed = cannot set groups: { $err } +chroot-error-set-user-failed = cannot set user to { $user }: { $err } diff --git a/src/uu/chroot/locales/fr-FR.ftl b/src/uu/chroot/locales/fr-FR.ftl new file mode 100644 index 00000000000..caabd169b2b --- /dev/null +++ b/src/uu/chroot/locales/fr-FR.ftl @@ -0,0 +1,25 @@ +chroot-about = Exécuter COMMANDE avec le répertoire racine défini à NOUVRACINE. +chroot-usage = chroot [OPTION]... NOUVRACINE [COMMANDE [ARG]...] + +# Messages d'aide +chroot-help-groups = Liste de groupes séparés par des virgules vers lesquels basculer +chroot-help-userspec = Utilisateur et groupe séparés par deux-points vers lesquels basculer. +chroot-help-skip-chdir = Utiliser cette option pour ne pas changer le répertoire de travail vers / après avoir changé le répertoire racine vers nouvracine, c.-à-d., à l'intérieur du chroot. + +# Messages d'erreur +chroot-error-skip-chdir-only-permitted = l'option --skip-chdir n'est autorisée que si NOUVRACINE est l'ancien '/' +chroot-error-cannot-enter = impossible de faire chroot vers { $dir } : { $err } +chroot-error-command-failed = échec de l'exécution de la commande { $cmd } : { $err } +chroot-error-command-not-found = échec de l'exécution de la commande { $cmd } : { $err } +chroot-error-groups-parsing-failed = échec de l'analyse de --groups +chroot-error-invalid-group = groupe invalide : { $group } +chroot-error-invalid-group-list = liste de groupes invalide : { $list } +chroot-error-missing-newroot = Opérande manquant : NOUVRACINE + Essayez '{ $util_name } --help' pour plus d'informations. +chroot-error-no-group-specified = aucun groupe spécifié pour l'uid inconnu : { $uid } +chroot-error-no-such-user = utilisateur invalide +chroot-error-no-such-group = groupe invalide +chroot-error-no-such-directory = impossible de changer le répertoire racine vers { $dir } : aucun répertoire de ce type +chroot-error-set-gid-failed = impossible de définir le gid à { $gid } : { $err } +chroot-error-set-groups-failed = impossible de définir les groupes : { $err } +chroot-error-set-user-failed = impossible de définir l'utilisateur à { $user } : { $err } diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index fb20b0ccc46..52db47e36c8 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -7,30 +7,152 @@ mod error; use crate::error::ChrootError; -use clap::{crate_version, Arg, ArgAction, Command}; +use clap::{Arg, ArgAction, Command}; use std::ffi::CString; use std::io::Error; use std::os::unix::prelude::OsStrExt; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process; -use uucore::error::{set_exit_code, UClapError, UResult, UUsageError}; -use uucore::fs::{canonicalize, MissingHandling, ResolveMode}; +use uucore::entries::{Locate, Passwd, grp2gid, usr2uid}; +use uucore::error::{UClapError, UResult, UUsageError, set_exit_code}; +use uucore::fs::{MissingHandling, ResolveMode, canonicalize}; use uucore::libc::{self, chroot, setgid, setgroups, setuid}; -use uucore::{entries, format_usage, help_about, help_usage}; +use uucore::{format_usage, show}; -static ABOUT: &str = help_about!("chroot.md"); -static USAGE: &str = help_usage!("chroot.md"); +use uucore::translate; mod options { pub const NEWROOT: &str = "newroot"; - pub const USER: &str = "user"; - pub const GROUP: &str = "group"; pub const GROUPS: &str = "groups"; pub const USERSPEC: &str = "userspec"; pub const COMMAND: &str = "command"; pub const SKIP_CHDIR: &str = "skip-chdir"; } +/// A user and group specification, where each is optional. +enum UserSpec { + NeitherGroupNorUser, + UserOnly(String), + GroupOnly(String), + UserAndGroup(String, String), +} + +struct Options { + /// Path to the new root directory. + newroot: PathBuf, + /// Whether to change to the new root directory. + skip_chdir: bool, + /// List of groups under which the command will be run. + groups: Option>, + /// The user and group (each optional) under which the command will be run. + userspec: Option, +} + +/// Parse a user and group from the argument to `--userspec`. +/// +/// The `spec` must be of the form `[USER][:[GROUP]]`, otherwise an +/// error is returned. +fn parse_userspec(spec: &str) -> UserSpec { + match spec.split_once(':') { + // "" + None if spec.is_empty() => UserSpec::NeitherGroupNorUser, + // "usr" + None => UserSpec::UserOnly(spec.to_string()), + // ":" + Some(("", "")) => UserSpec::NeitherGroupNorUser, + // ":grp" + Some(("", grp)) => UserSpec::GroupOnly(grp.to_string()), + // "usr:" + Some((usr, "")) => UserSpec::UserOnly(usr.to_string()), + // "usr:grp" + Some((usr, grp)) => UserSpec::UserAndGroup(usr.to_string(), grp.to_string()), + } +} + +/// Pre-condition: `list_str` is non-empty. +fn parse_group_list(list_str: &str) -> Result, ChrootError> { + let split: Vec<&str> = list_str.split(',').collect(); + if split.len() == 1 { + let name = split[0].trim(); + if name.is_empty() { + // --groups=" " + // chroot: invalid group ' ' + Err(ChrootError::InvalidGroup(name.to_string())) + } else { + // --groups="blah" + Ok(vec![name.to_string()]) + } + } else if split.iter().all(|s| s.is_empty()) { + // --groups="," + // chroot: invalid group list ',' + Err(ChrootError::InvalidGroupList(list_str.to_string())) + } else { + let mut result = vec![]; + let mut err = false; + for name in split { + let trimmed_name = name.trim(); + if trimmed_name.is_empty() { + if name.is_empty() { + // --groups="," + continue; + } + + // --groups=", " + // chroot: invalid group ' ' + show!(ChrootError::InvalidGroup(name.to_string())); + err = true; + } else { + // TODO Figure out a better condition here. + if trimmed_name.starts_with(char::is_numeric) + && trimmed_name.ends_with(|c: char| !c.is_numeric()) + { + // --groups="0trail" + // chroot: invalid group '0trail' + show!(ChrootError::InvalidGroup(name.to_string())); + err = true; + } else { + result.push(trimmed_name.to_string()); + } + } + } + if err { + Err(ChrootError::GroupsParsingFailed) + } else { + Ok(result) + } + } +} + +impl Options { + /// Parse parameters from the command-line arguments. + fn from(matches: &clap::ArgMatches) -> UResult { + let newroot = match matches.get_one::(options::NEWROOT) { + Some(v) => Path::new(v).to_path_buf(), + None => return Err(ChrootError::MissingNewRoot.into()), + }; + let groups = match matches.get_one::(options::GROUPS) { + None => None, + Some(s) => { + if s.is_empty() { + Some(vec![]) + } else { + Some(parse_group_list(s)?) + } + } + }; + let skip_chdir = matches.get_flag(options::SKIP_CHDIR); + let userspec = matches + .get_one::(options::USERSPEC) + .map(|s| parse_userspec(s)); + Ok(Self { + newroot, + skip_chdir, + groups, + userspec, + }) + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args).with_exit_code(125)?; @@ -39,27 +161,27 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let default_option: &'static str = "-i"; let user_shell = std::env::var("SHELL"); - let newroot: &Path = match matches.get_one::(options::NEWROOT) { - Some(v) => Path::new(v), - None => return Err(ChrootError::MissingNewRoot.into()), - }; + let options = Options::from(&matches)?; - let skip_chdir = matches.get_flag(options::SKIP_CHDIR); // We are resolving the path in case it is a symlink or /. or /../ - if skip_chdir - && canonicalize(newroot, MissingHandling::Normal, ResolveMode::Logical) - .unwrap() - .to_str() + if options.skip_chdir + && canonicalize( + &options.newroot, + MissingHandling::Normal, + ResolveMode::Logical, + ) + .unwrap() + .to_str() != Some("/") { return Err(UUsageError::new( 125, - "option --skip-chdir only permitted if NEWROOT is old '/'", + translate!("chroot-error-skip-chdir-only-permitted"), )); } - if !newroot.is_dir() { - return Err(ChrootError::NoSuchDirectory(format!("{}", newroot.display())).into()); + if !options.newroot.is_dir() { + return Err(ChrootError::NoSuchDirectory(format!("{}", options.newroot.display())).into()); } let commands = match matches.get_many::(options::COMMAND) { @@ -85,7 +207,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let chroot_args = &command[1..]; // NOTE: Tests can only trigger code beyond this point if they're invoked with root permissions - set_context(newroot, &matches)?; + set_context(&options)?; let pstatus = match process::Command::new(chroot_command) .args(chroot_args) @@ -98,7 +220,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else { ChrootError::CommandFailed(command[0].to_string(), e) } - .into()) + .into()); } }; @@ -113,9 +235,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) + .about(translate!("chroot-about")) + .override_usage(format_usage(&translate!("chroot-usage"))) .infer_long_args(true) .trailing_var_arg(true) .arg( @@ -125,45 +248,23 @@ pub fn uu_app() -> Command { .required(true) .index(1), ) - .arg( - Arg::new(options::USER) - .short('u') - .long(options::USER) - .help("User (ID or name) to switch before running the program") - .value_name("USER"), - ) - .arg( - Arg::new(options::GROUP) - .short('g') - .long(options::GROUP) - .help("Group (ID or name) to switch to") - .value_name("GROUP"), - ) .arg( Arg::new(options::GROUPS) - .short('G') .long(options::GROUPS) - .help("Comma-separated list of groups to switch to") + .overrides_with(options::GROUPS) + .help(translate!("chroot-help-groups")) .value_name("GROUP1,GROUP2..."), ) .arg( Arg::new(options::USERSPEC) .long(options::USERSPEC) - .help( - "Colon-separated user and group to switch to. \ - Same as -u USER -g GROUP. \ - Userspec has higher preference than -u and/or -g", - ) + .help(translate!("chroot-help-userspec")) .value_name("USER:GROUP"), ) .arg( Arg::new(options::SKIP_CHDIR) .long(options::SKIP_CHDIR) - .help( - "Use this option to not change the working directory \ - to / after changing the root directory to newroot, \ - i.e., inside the chroot.", - ) + .help(translate!("chroot-help-skip-chdir")) .action(ArgAction::SetTrue), ) .arg( @@ -175,119 +276,178 @@ pub fn uu_app() -> Command { ) } -fn set_context(root: &Path, options: &clap::ArgMatches) -> UResult<()> { - let userspec_str = options.get_one::(options::USERSPEC); - let user_str = options - .get_one::(options::USER) - .map(|s| s.as_str()) - .unwrap_or_default(); - let group_str = options - .get_one::(options::GROUP) - .map(|s| s.as_str()) - .unwrap_or_default(); - let groups_str = options - .get_one::(options::GROUPS) - .map(|s| s.as_str()) - .unwrap_or_default(); - let skip_chdir = options.contains_id(options::SKIP_CHDIR); - let userspec = match userspec_str { - Some(u) => { - let s: Vec<&str> = u.split(':').collect(); - if s.len() != 2 || s.iter().any(|&spec| spec.is_empty()) { - return Err(ChrootError::InvalidUserspec(u.to_string()).into()); - }; - s - } - None => Vec::new(), - }; - - let (user, group) = if userspec.is_empty() { - (user_str, group_str) - } else { - (userspec[0], userspec[1]) - }; +/// Get the UID for the given username, falling back to numeric parsing. +/// +/// According to the documentation of GNU `chroot`, "POSIX requires that +/// these commands first attempt to resolve the specified string as a +/// name, and only once that fails, then try to interpret it as an ID." +fn name_to_uid(name: &str) -> Result { + match usr2uid(name) { + Ok(uid) => Ok(uid), + Err(_) => name + .parse::() + .map_err(|_| ChrootError::NoSuchUser), + } +} - enter_chroot(root, skip_chdir)?; +/// Get the GID for the given group name, falling back to numeric parsing. +/// +/// According to the documentation of GNU `chroot`, "POSIX requires that +/// these commands first attempt to resolve the specified string as a +/// name, and only once that fails, then try to interpret it as an ID." +fn name_to_gid(name: &str) -> Result { + match grp2gid(name) { + Ok(gid) => Ok(gid), + Err(_) => name + .parse::() + .map_err(|_| ChrootError::NoSuchGroup), + } +} - set_groups_from_str(groups_str)?; - set_main_group(group)?; - set_user(user)?; - Ok(()) +/// Get the list of group IDs for the given user. +/// +/// According to the GNU documentation, "the supplementary groups are +/// set according to the system defined list for that user". This +/// function gets that list. +fn supplemental_gids(uid: libc::uid_t) -> Vec { + match Passwd::locate(uid) { + Err(_) => vec![], + Ok(passwd) => passwd.belongs_to(), + } } -fn enter_chroot(root: &Path, skip_chdir: bool) -> UResult<()> { - let err = unsafe { - chroot( - CString::new(root.as_os_str().as_bytes().to_vec()) - .unwrap() - .as_bytes_with_nul() - .as_ptr() as *const libc::c_char, - ) - }; +/// Set the supplemental group IDs for this process. +fn set_supplemental_gids(gids: &[libc::gid_t]) -> std::io::Result<()> { + #[cfg(any(target_vendor = "apple", target_os = "freebsd", target_os = "openbsd"))] + let n = gids.len() as libc::c_int; + #[cfg(any(target_os = "linux", target_os = "android"))] + let n = gids.len() as libc::size_t; + let err = unsafe { setgroups(n, gids.as_ptr()) }; + if err == 0 { + Ok(()) + } else { + Err(Error::last_os_error()) + } +} +/// Set the group ID of this process. +fn set_gid(gid: libc::gid_t) -> std::io::Result<()> { + let err = unsafe { setgid(gid) }; if err == 0 { - if !skip_chdir { - std::env::set_current_dir(root).unwrap(); - } Ok(()) } else { - Err(ChrootError::CannotEnter(format!("{}", root.display()), Error::last_os_error()).into()) + Err(Error::last_os_error()) } } -fn set_main_group(group: &str) -> UResult<()> { - if !group.is_empty() { - let group_id = match entries::grp2gid(group) { - Ok(g) => g, - _ => return Err(ChrootError::NoSuchGroup(group.to_string()).into()), - }; - let err = unsafe { setgid(group_id) }; - if err != 0 { - return Err( - ChrootError::SetGidFailed(group_id.to_string(), Error::last_os_error()).into(), - ); - } +/// Set the user ID of this process. +fn set_uid(uid: libc::uid_t) -> std::io::Result<()> { + let err = unsafe { setuid(uid) }; + if err == 0 { + Ok(()) + } else { + Err(Error::last_os_error()) } - Ok(()) } -#[cfg(any(target_vendor = "apple", target_os = "freebsd", target_os = "openbsd"))] -fn set_groups(groups: &[libc::gid_t]) -> libc::c_int { - unsafe { setgroups(groups.len() as libc::c_int, groups.as_ptr()) } +/// What to do when the `--groups` argument is missing. +enum Strategy { + /// Do nothing. + Nothing, + /// Use the list of supplemental groups for the given user. + /// + /// If the `bool` parameter is `false` and the list of groups for + /// the given user is empty, then this will result in an error. + FromUID(libc::uid_t, bool), +} + +/// Set supplemental groups when the `--groups` argument is not specified. +fn handle_missing_groups(strategy: Strategy) -> Result<(), ChrootError> { + match strategy { + Strategy::Nothing => Ok(()), + Strategy::FromUID(uid, false) => { + let gids = supplemental_gids(uid); + if gids.is_empty() { + Err(ChrootError::NoGroupSpecified(uid)) + } else { + set_supplemental_gids(&gids).map_err(ChrootError::SetGroupsFailed) + } + } + Strategy::FromUID(uid, true) => { + let gids = supplemental_gids(uid); + set_supplemental_gids(&gids).map_err(ChrootError::SetGroupsFailed) + } + } } -#[cfg(any(target_os = "linux", target_os = "android"))] -fn set_groups(groups: &[libc::gid_t]) -> libc::c_int { - unsafe { setgroups(groups.len() as libc::size_t, groups.as_ptr()) } +/// Set supplemental groups for this process. +fn set_supplemental_gids_with_strategy( + strategy: Strategy, + groups: Option<&Vec>, +) -> Result<(), ChrootError> { + match groups { + None => handle_missing_groups(strategy), + Some(groups) => { + let mut gids = vec![]; + for group in groups { + gids.push(name_to_gid(group)?); + } + set_supplemental_gids(&gids).map_err(ChrootError::SetGroupsFailed) + } + } } -fn set_groups_from_str(groups: &str) -> UResult<()> { - if !groups.is_empty() { - let mut groups_vec = vec![]; - for group in groups.split(',') { - let gid = match entries::grp2gid(group) { - Ok(g) => g, - Err(_) => return Err(ChrootError::NoSuchGroup(group.to_string()).into()), - }; - groups_vec.push(gid); +/// Change the root, set the user ID, and set the group IDs for this process. +fn set_context(options: &Options) -> UResult<()> { + enter_chroot(&options.newroot, options.skip_chdir)?; + match &options.userspec { + None | Some(UserSpec::NeitherGroupNorUser) => { + let strategy = Strategy::Nothing; + set_supplemental_gids_with_strategy(strategy, options.groups.as_ref())?; } - let err = set_groups(&groups_vec); - if err != 0 { - return Err(ChrootError::SetGroupsFailed(Error::last_os_error()).into()); + Some(UserSpec::UserOnly(user)) => { + let uid = name_to_uid(user)?; + let gid = uid as libc::gid_t; + let strategy = Strategy::FromUID(uid, false); + set_supplemental_gids_with_strategy(strategy, options.groups.as_ref())?; + set_gid(gid).map_err(|e| ChrootError::SetGidFailed(user.to_string(), e))?; + set_uid(uid).map_err(|e| ChrootError::SetUserFailed(user.to_string(), e))?; + } + Some(UserSpec::GroupOnly(group)) => { + let gid = name_to_gid(group)?; + let strategy = Strategy::Nothing; + set_supplemental_gids_with_strategy(strategy, options.groups.as_ref())?; + set_gid(gid).map_err(|e| ChrootError::SetGidFailed(group.to_string(), e))?; + } + Some(UserSpec::UserAndGroup(user, group)) => { + let uid = name_to_uid(user)?; + let gid = name_to_gid(group)?; + let strategy = Strategy::FromUID(uid, true); + set_supplemental_gids_with_strategy(strategy, options.groups.as_ref())?; + set_gid(gid).map_err(|e| ChrootError::SetGidFailed(group.to_string(), e))?; + set_uid(uid).map_err(|e| ChrootError::SetUserFailed(user.to_string(), e))?; } } Ok(()) } -fn set_user(user: &str) -> UResult<()> { - if !user.is_empty() { - let user_id = entries::usr2uid(user).unwrap(); - let err = unsafe { setuid(user_id as libc::uid_t) }; - if err != 0 { - return Err( - ChrootError::SetUserFailed(user.to_string(), Error::last_os_error()).into(), - ); +fn enter_chroot(root: &Path, skip_chdir: bool) -> UResult<()> { + let err = unsafe { + chroot( + CString::new(root.as_os_str().as_bytes().to_vec()) + .map_err(|e| ChrootError::CannotEnter("root".to_string(), e.into()))? + .as_bytes_with_nul() + .as_ptr() + .cast::(), + ) + }; + + if err == 0 { + if !skip_chdir { + std::env::set_current_dir("/")?; } + Ok(()) + } else { + Err(ChrootError::CannotEnter(format!("{}", root.display()), Error::last_os_error()).into()) } - Ok(()) } diff --git a/src/uu/chroot/src/error.rs b/src/uu/chroot/src/error.rs index 526f1a75a43..52f03ba3a96 100644 --- a/src/uu/chroot/src/error.rs +++ b/src/uu/chroot/src/error.rs @@ -4,51 +4,73 @@ // file that was distributed with this source code. // spell-checker:ignore NEWROOT Userspec userspec //! Errors returned by chroot. -use std::fmt::Display; use std::io::Error; +use thiserror::Error; use uucore::display::Quotable; use uucore::error::UError; +use uucore::libc; +use uucore::translate; /// Errors that can happen while executing chroot. -#[derive(Debug)] +#[derive(Debug, Error)] pub enum ChrootError { /// Failed to enter the specified directory. - CannotEnter(String, Error), + #[error("{}", translate!("chroot-error-cannot-enter", "dir" => _0.quote(), "err" => _1))] + CannotEnter(String, #[source] Error), /// Failed to execute the specified command. - CommandFailed(String, Error), + #[error("{}", translate!("chroot-error-command-failed", "cmd" => _0.quote(), "err" => _1))] + CommandFailed(String, #[source] Error), /// Failed to find the specified command. - CommandNotFound(String, Error), + #[error("{}", translate!("chroot-error-command-not-found", "cmd" => _0.quote(), "err" => _1))] + CommandNotFound(String, #[source] Error), - /// The given user and group specification was invalid. - InvalidUserspec(String), + #[error("{}", translate!("chroot-error-groups-parsing-failed"))] + GroupsParsingFailed, + + #[error("{}", translate!("chroot-error-invalid-group", "group" => _0.quote()))] + InvalidGroup(String), + + #[error("{}", translate!("chroot-error-invalid-group-list", "list" => _0.quote()))] + InvalidGroupList(String), /// The new root directory was not given. + #[error("{}", translate!("chroot-error-missing-newroot", "util_name" => uucore::execution_phrase()))] MissingNewRoot, + #[error("{}", translate!("chroot-error-no-group-specified", "uid" => _0))] + NoGroupSpecified(libc::uid_t), + + /// Failed to find the specified user. + #[error("{}", translate!("chroot-error-no-such-user"))] + NoSuchUser, + /// Failed to find the specified group. - NoSuchGroup(String), + #[error("{}", translate!("chroot-error-no-such-group"))] + NoSuchGroup, /// The given directory does not exist. + #[error("{}", translate!("chroot-error-no-such-directory", "dir" => _0.quote()))] NoSuchDirectory(String), /// The call to `setgid()` failed. - SetGidFailed(String, Error), + #[error("{}", translate!("chroot-error-set-gid-failed", "gid" => _0, "err" => _1))] + SetGidFailed(String, #[source] Error), /// The call to `setgroups()` failed. + #[error("{}", translate!("chroot-error-set-groups-failed", "err" => _0))] SetGroupsFailed(Error), /// The call to `setuid()` failed. - SetUserFailed(String, Error), + #[error("{}", translate!("chroot-error-set-user-failed", "user" => _0.maybe_quote(), "err" => _1))] + SetUserFailed(String, #[source] Error), } -impl std::error::Error for ChrootError {} - impl UError for ChrootError { - // 125 if chroot itself fails - // 126 if command is found but cannot be invoked - // 127 if command cannot be found + /// 125 if chroot itself fails + /// 126 if command is found but cannot be invoked + /// 127 if command cannot be found fn code(&self) -> i32 { match self { Self::CommandFailed(_, _) => 126, @@ -57,31 +79,3 @@ impl UError for ChrootError { } } } - -impl Display for ChrootError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::CannotEnter(s, e) => write!(f, "cannot chroot to {}: {}", s.quote(), e,), - Self::CommandFailed(s, e) | Self::CommandNotFound(s, e) => { - write!(f, "failed to run command {}: {}", s.to_string().quote(), e,) - } - Self::InvalidUserspec(s) => write!(f, "invalid userspec: {}", s.quote(),), - Self::MissingNewRoot => write!( - f, - "Missing operand: NEWROOT\nTry '{} --help' for more information.", - uucore::execution_phrase(), - ), - Self::NoSuchGroup(s) => write!(f, "no such group: {}", s.maybe_quote(),), - Self::NoSuchDirectory(s) => write!( - f, - "cannot change root directory to {}: no such directory", - s.quote(), - ), - Self::SetGidFailed(s, e) => write!(f, "cannot set gid to {s}: {e}"), - Self::SetGroupsFailed(e) => write!(f, "cannot set groups: {e}"), - Self::SetUserFailed(s, e) => { - write!(f, "cannot set user to {}: {}", s.maybe_quote(), e) - } - } - } -} diff --git a/src/uu/cksum/Cargo.toml b/src/uu/cksum/Cargo.toml index 406aa75df77..0eb4d28541d 100644 --- a/src/uu/cksum/Cargo.toml +++ b/src/uu/cksum/Cargo.toml @@ -1,23 +1,27 @@ [package] name = "uu_cksum" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "cksum ~ (uutils) display CRC and size of input" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/cksum" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/cksum.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["encoding", "sum"] } +uucore = { workspace = true, features = ["checksum", "encoding", "sum"] } hex = { workspace = true } +fluent = { workspace = true } [[bin]] name = "cksum" diff --git a/src/uu/cksum/cksum.md b/src/uu/cksum/cksum.md deleted file mode 100644 index 4b0d25f32c3..00000000000 --- a/src/uu/cksum/cksum.md +++ /dev/null @@ -1,23 +0,0 @@ -# cksum - -``` -cksum [OPTIONS] [FILE]... -``` - -Print CRC and size for each file - -## After Help - -DIGEST determines the digest algorithm and default output format: - -- `sysv`: (equivalent to sum -s) -- `bsd`: (equivalent to sum -r) -- `crc`: (equivalent to cksum) -- `md5`: (equivalent to md5sum) -- `sha1`: (equivalent to sha1sum) -- `sha224`: (equivalent to sha224sum) -- `sha256`: (equivalent to sha256sum) -- `sha384`: (equivalent to sha384sum) -- `sha512`: (equivalent to sha512sum) -- `blake2b`: (equivalent to b2sum) -- `sm3`: (only available through cksum) diff --git a/src/uu/cksum/locales/en-US.ftl b/src/uu/cksum/locales/en-US.ftl new file mode 100644 index 00000000000..338ae874ce4 --- /dev/null +++ b/src/uu/cksum/locales/en-US.ftl @@ -0,0 +1,35 @@ +cksum-about = Print CRC and size for each file +cksum-usage = cksum [OPTIONS] [FILE]... +cksum-after-help = DIGEST determines the digest algorithm and default output format: + + - sysv: (equivalent to sum -s) + - bsd: (equivalent to sum -r) + - crc: (equivalent to cksum) + - crc32b: (only available through cksum) + - md5: (equivalent to md5sum) + - sha1: (equivalent to sha1sum) + - sha224: (equivalent to sha224sum) + - sha256: (equivalent to sha256sum) + - sha384: (equivalent to sha384sum) + - sha512: (equivalent to sha512sum) + - blake2b: (equivalent to b2sum) + - sm3: (only available through cksum) + +# Help messages +cksum-help-algorithm = select the digest type to use. See DIGEST below +cksum-help-untagged = create a reversed style checksum, without digest type +cksum-help-tag = create a BSD style checksum, undo --untagged (default) +cksum-help-length = digest length in bits; must not exceed the max for the blake2 algorithm and must be a multiple of 8 +cksum-help-raw = emit a raw binary digest, not hexadecimal +cksum-help-strict = exit non-zero for improperly formatted checksum lines +cksum-help-check = read hashsums from the FILEs and check them +cksum-help-base64 = emit a base64 digest, not hexadecimal +cksum-help-warn = warn about improperly formatted checksum lines +cksum-help-status = don't output anything, status code shows success +cksum-help-quiet = don't print OK for each successfully verified file +cksum-help-ignore-missing = don't fail or report status for missing files +cksum-help-zero = end each output line with NUL, not newline, and disable file name escaping + +# Error messages +cksum-error-is-directory = { $file }: Is a directory +cksum-error-failed-to-read-input = failed to read input diff --git a/src/uu/cksum/locales/fr-FR.ftl b/src/uu/cksum/locales/fr-FR.ftl new file mode 100644 index 00000000000..52b4b9ed742 --- /dev/null +++ b/src/uu/cksum/locales/fr-FR.ftl @@ -0,0 +1,35 @@ +cksum-about = Afficher le CRC et la taille de chaque fichier +cksum-usage = cksum [OPTION]... [FICHIER]... +cksum-after-help = DIGEST détermine l'algorithme de condensé et le format de sortie par défaut : + + - sysv : (équivalent à sum -s) + - bsd : (équivalent à sum -r) + - crc : (équivalent à cksum) + - crc32b : (disponible uniquement via cksum) + - md5 : (équivalent à md5sum) + - sha1 : (équivalent à sha1sum) + - sha224 : (équivalent à sha224sum) + - sha256 : (équivalent à sha256sum) + - sha384 : (équivalent à sha384sum) + - sha512 : (équivalent à sha512sum) + - blake2b : (équivalent à b2sum) + - sm3 : (disponible uniquement via cksum) + +# Messages d'aide +cksum-help-algorithm = sélectionner le type de condensé à utiliser. Voir DIGEST ci-dessous +cksum-help-untagged = créer une somme de contrôle de style inversé, sans type de condensé +cksum-help-tag = créer une somme de contrôle de style BSD, annuler --untagged (par défaut) +cksum-help-length = longueur du condensé en bits ; ne doit pas dépasser le maximum pour l'algorithme blake2 et doit être un multiple de 8 +cksum-help-raw = émettre un condensé binaire brut, pas hexadécimal +cksum-help-strict = sortir avec un code non-zéro pour les lignes de somme de contrôle mal formatées +cksum-help-check = lire les sommes de hachage des FICHIERs et les vérifier +cksum-help-base64 = émettre un condensé base64, pas hexadécimal +cksum-help-warn = avertir des lignes de somme de contrôle mal formatées +cksum-help-status = ne rien afficher, le code de statut indique le succès +cksum-help-quiet = ne pas afficher OK pour chaque fichier vérifié avec succès +cksum-help-ignore-missing = ne pas échouer ou signaler le statut pour les fichiers manquants +cksum-help-zero = terminer chaque ligne de sortie avec NUL, pas un saut de ligne, et désactiver l'échappement des noms de fichiers + +# Messages d'erreur +cksum-error-is-directory = { $file } : Est un répertoire +cksum-error-failed-to-read-input = échec de la lecture de l'entrée diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index c5c362c5936..01e3da55f0c 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -4,47 +4,32 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) fname, algo -use clap::{crate_version, value_parser, Arg, ArgAction, Command}; -use hex::decode; -use hex::encode; -use std::error::Error; -use std::ffi::OsStr; -use std::fmt::Display; + +use clap::builder::ValueParser; +use clap::{Arg, ArgAction, Command, value_parser}; +use std::ffi::{OsStr, OsString}; use std::fs::File; -use std::io::{self, stdin, stdout, BufReader, Read, Write}; +use std::io::{self, BufReader, Read, Write, stdin, stdout}; use std::iter; use std::path::Path; +use uucore::checksum::{ + ALGORITHM_OPTIONS_BLAKE2B, ALGORITHM_OPTIONS_BSD, ALGORITHM_OPTIONS_CRC, + ALGORITHM_OPTIONS_CRC32B, ALGORITHM_OPTIONS_SYSV, ChecksumError, ChecksumOptions, + ChecksumVerbose, SUPPORTED_ALGORITHMS, calculate_blake2b_length, detect_algo, digest_reader, + perform_checksum_validation, +}; +use uucore::translate; + +use uucore::LocalizedCommand; use uucore::{ encoding, - error::{FromIo, UError, UResult, USimpleError}, - format_usage, help_about, help_section, help_usage, show, - sum::{ - div_ceil, Blake2b, Digest, DigestWriter, Md5, Sha1, Sha224, Sha256, Sha384, Sha512, Sm3, - BSD, CRC, SYSV, - }, + error::{FromIo, UResult, USimpleError}, + format_usage, + line_ending::LineEnding, + os_str_as_bytes, show, + sum::Digest, }; -const USAGE: &str = help_usage!("cksum.md"); -const ABOUT: &str = help_about!("cksum.md"); -const AFTER_HELP: &str = help_section!("after help", "cksum.md"); - -const ALGORITHM_OPTIONS_SYSV: &str = "sysv"; -const ALGORITHM_OPTIONS_BSD: &str = "bsd"; -const ALGORITHM_OPTIONS_CRC: &str = "crc"; -const ALGORITHM_OPTIONS_MD5: &str = "md5"; -const ALGORITHM_OPTIONS_SHA1: &str = "sha1"; -const ALGORITHM_OPTIONS_SHA224: &str = "sha224"; -const ALGORITHM_OPTIONS_SHA256: &str = "sha256"; -const ALGORITHM_OPTIONS_SHA384: &str = "sha384"; -const ALGORITHM_OPTIONS_SHA512: &str = "sha512"; -const ALGORITHM_OPTIONS_BLAKE2B: &str = "blake2b"; -const ALGORITHM_OPTIONS_SM3: &str = "sm3"; - -#[derive(Debug)] -enum CkSumError { - RawMultipleFiles, -} - #[derive(Debug, PartialEq)] enum OutputFormat { Hexadecimal, @@ -52,101 +37,15 @@ enum OutputFormat { Base64, } -impl UError for CkSumError { - fn code(&self) -> i32 { - match self { - Self::RawMultipleFiles => 1, - } - } -} - -impl Error for CkSumError {} - -impl Display for CkSumError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::RawMultipleFiles => { - write!(f, "the --raw option is not supported with multiple files") - } - } - } -} - -fn detect_algo( - program: &str, - length: Option, -) -> (&'static str, Box, usize) { - match program { - ALGORITHM_OPTIONS_SYSV => ( - ALGORITHM_OPTIONS_SYSV, - Box::new(SYSV::new()) as Box, - 512, - ), - ALGORITHM_OPTIONS_BSD => ( - ALGORITHM_OPTIONS_BSD, - Box::new(BSD::new()) as Box, - 1024, - ), - ALGORITHM_OPTIONS_CRC => ( - ALGORITHM_OPTIONS_CRC, - Box::new(CRC::new()) as Box, - 256, - ), - ALGORITHM_OPTIONS_MD5 => ( - ALGORITHM_OPTIONS_MD5, - Box::new(Md5::new()) as Box, - 128, - ), - ALGORITHM_OPTIONS_SHA1 => ( - ALGORITHM_OPTIONS_SHA1, - Box::new(Sha1::new()) as Box, - 160, - ), - ALGORITHM_OPTIONS_SHA224 => ( - ALGORITHM_OPTIONS_SHA224, - Box::new(Sha224::new()) as Box, - 224, - ), - ALGORITHM_OPTIONS_SHA256 => ( - ALGORITHM_OPTIONS_SHA256, - Box::new(Sha256::new()) as Box, - 256, - ), - ALGORITHM_OPTIONS_SHA384 => ( - ALGORITHM_OPTIONS_SHA384, - Box::new(Sha384::new()) as Box, - 384, - ), - ALGORITHM_OPTIONS_SHA512 => ( - ALGORITHM_OPTIONS_SHA512, - Box::new(Sha512::new()) as Box, - 512, - ), - ALGORITHM_OPTIONS_BLAKE2B => ( - ALGORITHM_OPTIONS_BLAKE2B, - Box::new(if let Some(length) = length { - Blake2b::with_output_bytes(length) - } else { - Blake2b::new() - }) as Box, - 512, - ), - ALGORITHM_OPTIONS_SM3 => ( - ALGORITHM_OPTIONS_SM3, - Box::new(Sm3::new()) as Box, - 512, - ), - _ => unreachable!("unknown algorithm: clap should have prevented this case"), - } -} - struct Options { algo_name: &'static str, digest: Box, output_bits: usize, - untagged: bool, + tag: bool, // will cover the --untagged option length: Option, output_format: OutputFormat, + asterisk: bool, // if we display an asterisk or not (--binary/--text) + line_ending: LineEnding, } /// Calculate checksum @@ -154,7 +53,7 @@ struct Options { /// # Arguments /// /// * `options` - CLI options for the assigning checksum algorithm -/// * `files` - A iterator of OsStr which is a bunch of files that are using for calculating checksum +/// * `files` - A iterator of [`OsStr`] which is a bunch of files that are using for calculating checksum #[allow(clippy::cognitive_complexity)] fn cksum<'a, I>(mut options: Options, files: I) -> UResult<()> where @@ -162,7 +61,7 @@ where { let files: Vec<_> = files.collect(); if options.output_format == OutputFormat::Raw && files.len() > 1 { - return Err(Box::new(CkSumError::RawMultipleFiles)); + return Err(Box::new(ChecksumError::RawMultipleFiles)); } for filename in files { @@ -170,6 +69,8 @@ where let stdin_buf; let file_buf; let not_file = filename == OsStr::new("-"); + + // Handle the file input let mut file = BufReader::new(if not_file { stdin_buf = stdin(); Box::new(stdin_buf) as Box @@ -185,15 +86,19 @@ where }; Box::new(file_buf) as Box }); - let (sum_hex, sz) = digest_read(&mut options.digest, &mut file, options.output_bits) - .map_err_context(|| "failed to read input".to_string())?; + if filename.is_dir() { show!(USimpleError::new( 1, - format!("{}: Is a directory", filename.display()) + translate!("cksum-error-is-directory", "file" => filename.display()) )); continue; } + + let (sum_hex, sz) = + digest_reader(&mut options.digest, &mut file, false, options.output_bits) + .map_err_context(|| translate!("cksum-error-failed-to-read-input"))?; + let sum = match options.output_format { OutputFormat::Raw => { let bytes = match options.algo_name { @@ -201,7 +106,7 @@ where ALGORITHM_OPTIONS_SYSV | ALGORITHM_OPTIONS_BSD => { sum_hex.parse::().unwrap().to_be_bytes().to_vec() } - _ => decode(sum_hex).unwrap(), + _ => hex::decode(sum_hex).unwrap(), }; // Cannot handle multiple files anyway, output immediately. stdout().write_all(&bytes)?; @@ -209,95 +114,79 @@ where } OutputFormat::Hexadecimal => sum_hex, OutputFormat::Base64 => match options.algo_name { - ALGORITHM_OPTIONS_CRC | ALGORITHM_OPTIONS_SYSV | ALGORITHM_OPTIONS_BSD => sum_hex, - _ => encoding::encode(encoding::Format::Base64, &decode(sum_hex).unwrap()).unwrap(), + ALGORITHM_OPTIONS_CRC + | ALGORITHM_OPTIONS_CRC32B + | ALGORITHM_OPTIONS_SYSV + | ALGORITHM_OPTIONS_BSD => sum_hex, + _ => encoding::for_cksum::BASE64.encode(&hex::decode(sum_hex).unwrap()), }, }; + // The BSD checksum output is 5 digit integer let bsd_width = 5; - match (options.algo_name, not_file) { - (ALGORITHM_OPTIONS_SYSV, true) => println!( - "{} {}", - sum.parse::().unwrap(), - div_ceil(sz, options.output_bits) - ), - (ALGORITHM_OPTIONS_SYSV, false) => println!( - "{} {} {}", - sum.parse::().unwrap(), - div_ceil(sz, options.output_bits), - filename.display() + let (before_filename, should_print_filename, after_filename) = match options.algo_name { + ALGORITHM_OPTIONS_SYSV => ( + format!( + "{} {}{}", + sum.parse::().unwrap(), + sz.div_ceil(options.output_bits), + if not_file { "" } else { " " } + ), + !not_file, + String::new(), ), - (ALGORITHM_OPTIONS_BSD, true) => println!( - "{:0bsd_width$} {:bsd_width$}", - sum.parse::().unwrap(), - div_ceil(sz, options.output_bits) + ALGORITHM_OPTIONS_BSD => ( + format!( + "{:0bsd_width$} {:bsd_width$}{}", + sum.parse::().unwrap(), + sz.div_ceil(options.output_bits), + if not_file { "" } else { " " } + ), + !not_file, + String::new(), ), - (ALGORITHM_OPTIONS_BSD, false) => println!( - "{:0bsd_width$} {:bsd_width$} {}", - sum.parse::().unwrap(), - div_ceil(sz, options.output_bits), - filename.display() + ALGORITHM_OPTIONS_CRC | ALGORITHM_OPTIONS_CRC32B => ( + format!("{sum} {sz}{}", if not_file { "" } else { " " }), + !not_file, + String::new(), ), - (ALGORITHM_OPTIONS_CRC, true) => println!("{sum} {sz}"), - (ALGORITHM_OPTIONS_CRC, false) => println!("{sum} {sz} {}", filename.display()), - (ALGORITHM_OPTIONS_BLAKE2B, _) if !options.untagged => { - if let Some(length) = options.length { - // Multiply by 8 here, as we want to print the length in bits. - println!("BLAKE2b-{} ({}) = {sum}", length * 8, filename.display()); - } else { - println!("BLAKE2b ({}) = {sum}", filename.display()); - } + ALGORITHM_OPTIONS_BLAKE2B if options.tag => { + ( + if let Some(length) = options.length { + // Multiply by 8 here, as we want to print the length in bits. + format!("BLAKE2b-{} (", length * 8) + } else { + "BLAKE2b (".to_owned() + }, + true, + format!(") = {sum}"), + ) } _ => { - if options.untagged { - println!("{sum} {}", filename.display()); + if options.tag { + ( + format!("{} (", options.algo_name.to_ascii_uppercase()), + true, + format!(") = {sum}"), + ) } else { - println!( - "{} ({}) = {sum}", - options.algo_name.to_ascii_uppercase(), - filename.display() - ); + let prefix = if options.asterisk { "*" } else { " " }; + (format!("{sum} {prefix}"), true, String::new()) } } + }; + + print!("{before_filename}"); + if should_print_filename { + // The filename might not be valid UTF-8, and filename.display() would mangle the names. + // Therefore, emit the bytes directly to stdout, without any attempt at encoding them. + let _dropped_result = stdout().write_all(os_str_as_bytes(filename.as_os_str())?); } + print!("{after_filename}{}", options.line_ending); } - Ok(()) } -fn digest_read( - digest: &mut Box, - reader: &mut BufReader, - output_bits: usize, -) -> io::Result<(String, usize)> { - digest.reset(); - - // Read bytes from `reader` and write those bytes to `digest`. - // - // If `binary` is `false` and the operating system is Windows, then - // `DigestWriter` replaces "\r\n" with "\n" before it writes the - // bytes into `digest`. Otherwise, it just inserts the bytes as-is. - // - // In order to support replacing "\r\n", we must call `finalize()` - // in order to support the possibility that the last character read - // from the reader was "\r". (This character gets buffered by - // `DigestWriter` and only written if the following character is - // "\n". But when "\r" is the last character read, we need to force - // it to be written.) - let mut digest_writer = DigestWriter::new(digest, true); - let output_size = std::io::copy(reader, &mut digest_writer)? as usize; - digest_writer.finalize(); - - if digest.output_bits() > 0 { - Ok((digest.result_str(), output_size)) - } else { - // Assume it's SHAKE. result_str() doesn't work with shake (as of 8/30/2016) - let mut bytes = vec![0; (output_bits + 7) / 8]; - digest.hash_finalize(&mut bytes); - Ok((encode(bytes), output_size)) - } -} - mod options { pub const ALGORITHM: &str = "algorithm"; pub const FILE: &str = "file"; @@ -306,59 +195,124 @@ mod options { pub const LENGTH: &str = "length"; pub const RAW: &str = "raw"; pub const BASE64: &str = "base64"; + pub const CHECK: &str = "check"; + pub const STRICT: &str = "strict"; + pub const TEXT: &str = "text"; + pub const BINARY: &str = "binary"; + pub const STATUS: &str = "status"; + pub const WARN: &str = "warn"; + pub const IGNORE_MISSING: &str = "ignore-missing"; + pub const QUIET: &str = "quiet"; + pub const ZERO: &str = "zero"; +} + +/*** + * cksum has a bunch of legacy behavior. + * We handle this in this function to make sure they are self contained + * and "easier" to understand + */ +fn handle_tag_text_binary_flags>( + args: impl Iterator, +) -> UResult<(bool, bool)> { + let mut tag = true; + let mut binary = false; + + // --binary, --tag and --untagged are tight together: none of them + // conflicts with each other but --tag will reset "binary" and set "tag". + + for arg in args { + let arg = arg.as_ref(); + if arg == "-b" || arg == "--binary" { + binary = true; + } else if arg == "--tag" { + tag = true; + binary = false; + } else if arg == "--untagged" { + tag = false; + } + } + + Ok((tag, !tag && binary)) } #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); + + let check = matches.get_flag(options::CHECK); let algo_name: &str = match matches.get_one::(options::ALGORITHM) { Some(v) => v, - None => ALGORITHM_OPTIONS_CRC, + None => { + if check { + // if we are doing a --check, we should not default to crc + "" + } else { + ALGORITHM_OPTIONS_CRC + } + } }; let input_length = matches.get_one::(options::LENGTH); - let length = if let Some(length) = input_length { - match length.to_owned() { - 0 => None, - n if n % 8 != 0 => { - // GNU's implementation seem to use these quotation marks - // in their error messages, so we do the same. - uucore::show_error!("invalid length: \u{2018}{length}\u{2019}"); - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "length is not a multiple of 8", - ) - .into()); - } - n if n > 512 => { - uucore::show_error!("invalid length: \u{2018}{length}\u{2019}"); - - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "maximum digest length for \u{2018}BLAKE2b\u{2019} is 512 bits", - ) - .into()); - } - n => { - if algo_name != ALGORITHM_OPTIONS_BLAKE2B { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "--length is only supported with --algorithm=blake2b", - ) - .into()); - } - // Divide by 8, as our blake2b implementation expects bytes - // instead of bits. - Some(n / 8) + let length = match input_length { + Some(length) => { + if algo_name == ALGORITHM_OPTIONS_BLAKE2B { + calculate_blake2b_length(*length)? + } else { + return Err(ChecksumError::LengthOnlyForBlake2b.into()); } } - } else { - None + None => None, }; - let (name, algo, bits) = detect_algo(algo_name, length); + if ["bsd", "crc", "sysv", "crc32b"].contains(&algo_name) && check { + return Err(ChecksumError::AlgorithmNotSupportedWithCheck.into()); + } + + if check { + let text_flag = matches.get_flag(options::TEXT); + let binary_flag = matches.get_flag(options::BINARY); + let strict = matches.get_flag(options::STRICT); + let status = matches.get_flag(options::STATUS); + let warn = matches.get_flag(options::WARN); + let ignore_missing = matches.get_flag(options::IGNORE_MISSING); + let quiet = matches.get_flag(options::QUIET); + let tag = matches.get_flag(options::TAG); + + if tag || binary_flag || text_flag { + return Err(ChecksumError::BinaryTextConflict.into()); + } + + // Determine the appropriate algorithm option to pass + let algo_option = if algo_name.is_empty() { + None + } else { + Some(algo_name) + }; + + // Execute the checksum validation based on the presence of files or the use of stdin + + let files = matches.get_many::(options::FILE).map_or_else( + || iter::once(OsStr::new("-")).collect::>(), + |files| files.map(OsStr::new).collect::>(), + ); + + let verbose = ChecksumVerbose::new(status, quiet, warn); + let opts = ChecksumOptions { + binary: binary_flag, + ignore_missing, + strict, + verbose, + }; + + return perform_checksum_validation(files.iter().copied(), algo_option, length, opts); + } + + let (tag, asterisk) = handle_tag_text_binary_flags(std::env::args_os())?; + + let algo = detect_algo(algo_name, length)?; + let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); let output_format = if matches.get_flag(options::RAW) { OutputFormat::Raw @@ -369,90 +323,147 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let opts = Options { - algo_name: name, - digest: algo, - output_bits: bits, + algo_name: algo.name, + digest: (algo.create_fn)(), + output_bits: algo.bits, length, - untagged: matches.get_flag(options::UNTAGGED), + tag, output_format, + asterisk, + line_ending, }; - match matches.get_many::(options::FILE) { + match matches.get_many::(options::FILE) { Some(files) => cksum(opts, files.map(OsStr::new))?, None => cksum(opts, iter::once(OsStr::new("-")))?, - }; + } Ok(()) } pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) + .about(translate!("cksum-about")) + .override_usage(format_usage(&translate!("cksum-usage"))) .infer_long_args(true) .args_override_self(true) .arg( Arg::new(options::FILE) .hide(true) - .action(clap::ArgAction::Append) + .action(ArgAction::Append) + .value_parser(ValueParser::os_string()) .value_hint(clap::ValueHint::FilePath), ) .arg( Arg::new(options::ALGORITHM) .long(options::ALGORITHM) .short('a') - .help("select the digest type to use. See DIGEST below") + .help(translate!("cksum-help-algorithm")) .value_name("ALGORITHM") - .value_parser([ - ALGORITHM_OPTIONS_SYSV, - ALGORITHM_OPTIONS_BSD, - ALGORITHM_OPTIONS_CRC, - ALGORITHM_OPTIONS_MD5, - ALGORITHM_OPTIONS_SHA1, - ALGORITHM_OPTIONS_SHA224, - ALGORITHM_OPTIONS_SHA256, - ALGORITHM_OPTIONS_SHA384, - ALGORITHM_OPTIONS_SHA512, - ALGORITHM_OPTIONS_BLAKE2B, - ALGORITHM_OPTIONS_SM3, - ]), + .value_parser(SUPPORTED_ALGORITHMS), ) .arg( Arg::new(options::UNTAGGED) .long(options::UNTAGGED) - .help("create a reversed style checksum, without digest type") + .help(translate!("cksum-help-untagged")) .action(ArgAction::SetTrue) .overrides_with(options::TAG), ) .arg( Arg::new(options::TAG) .long(options::TAG) - .help("create a BSD style checksum, undo --untagged (default)") - .action(ArgAction::SetTrue), + .help(translate!("cksum-help-tag")) + .action(ArgAction::SetTrue) + .overrides_with(options::UNTAGGED), ) .arg( Arg::new(options::LENGTH) .long(options::LENGTH) .value_parser(value_parser!(usize)) .short('l') - .help("digest length in bits; must not exceed the max for the blake2 algorithm and must be a multiple of 8") + .help(translate!("cksum-help-length")) .action(ArgAction::Set), ) .arg( Arg::new(options::RAW) - .long(options::RAW) - .help("emit a raw binary digest, not hexadecimal") - .action(ArgAction::SetTrue), + .long(options::RAW) + .help(translate!("cksum-help-raw")) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::STRICT) + .long(options::STRICT) + .help(translate!("cksum-help-strict")) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::CHECK) + .short('c') + .long(options::CHECK) + .help(translate!("cksum-help-check")) + .action(ArgAction::SetTrue), ) .arg( Arg::new(options::BASE64) - .long(options::BASE64) - .help("emit a base64 digest, not hexadecimal") - .action(ArgAction::SetTrue) - // Even though this could easily just override an earlier '--raw', - // GNU cksum does not permit these flags to be combined: - .conflicts_with(options::RAW), + .long(options::BASE64) + .help(translate!("cksum-help-base64")) + .action(ArgAction::SetTrue) + // Even though this could easily just override an earlier '--raw', + // GNU cksum does not permit these flags to be combined: + .conflicts_with(options::RAW), + ) + .arg( + Arg::new(options::TEXT) + .long(options::TEXT) + .short('t') + .hide(true) + .overrides_with(options::BINARY) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::BINARY) + .long(options::BINARY) + .short('b') + .hide(true) + .overrides_with(options::TEXT) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::WARN) + .short('w') + .long("warn") + .help(translate!("cksum-help-warn")) + .action(ArgAction::SetTrue) + .overrides_with_all([options::STATUS, options::QUIET]), + ) + .arg( + Arg::new(options::STATUS) + .long("status") + .help(translate!("cksum-help-status")) + .action(ArgAction::SetTrue) + .overrides_with_all([options::WARN, options::QUIET]), + ) + .arg( + Arg::new(options::QUIET) + .long(options::QUIET) + .help(translate!("cksum-help-quiet")) + .action(ArgAction::SetTrue) + .overrides_with_all([options::WARN, options::STATUS]), + ) + .arg( + Arg::new(options::IGNORE_MISSING) + .long(options::IGNORE_MISSING) + .help(translate!("cksum-help-ignore-missing")) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::ZERO) + .long(options::ZERO) + .short('z') + .help(translate!("cksum-help-zero")) + .action(ArgAction::SetTrue), ) - .after_help(AFTER_HELP) + .after_help(translate!("cksum-after-help")) } diff --git a/src/uu/comm/Cargo.toml b/src/uu/comm/Cargo.toml index 41e2e3b7459..8f8f6fba70a 100644 --- a/src/uu/comm/Cargo.toml +++ b/src/uu/comm/Cargo.toml @@ -1,22 +1,26 @@ [package] name = "uu_comm" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "comm ~ (uutils) compare sorted inputs" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/comm" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/comm.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true } +uucore = { workspace = true, features = ["fs"] } +fluent = { workspace = true } [[bin]] name = "comm" diff --git a/src/uu/comm/comm.md b/src/uu/comm/comm.md deleted file mode 100644 index 91f4467d443..00000000000 --- a/src/uu/comm/comm.md +++ /dev/null @@ -1,13 +0,0 @@ -# comm - -``` -comm [OPTION]... FILE1 FILE2 -``` - -Compare two sorted files line by line. - -When FILE1 or FILE2 (not both) is -, read standard input. - -With no options, produce three-column output. Column one contains -lines unique to FILE1, column two contains lines unique to FILE2, -and column three contains lines common to both files. diff --git a/src/uu/comm/locales/en-US.ftl b/src/uu/comm/locales/en-US.ftl new file mode 100644 index 00000000000..1118c09bb52 --- /dev/null +++ b/src/uu/comm/locales/en-US.ftl @@ -0,0 +1,27 @@ +comm-about = Compare two sorted files line by line. + + When FILE1 or FILE2 (not both) is -, read standard input. + + With no options, produce three-column output. Column one contains + lines unique to FILE1, column two contains lines unique to FILE2, + and column three contains lines common to both files. +comm-usage = comm [OPTION]... FILE1 FILE2 + +# Help messages +comm-help-column-1 = suppress column 1 (lines unique to FILE1) +comm-help-column-2 = suppress column 2 (lines unique to FILE2) +comm-help-column-3 = suppress column 3 (lines that appear in both files) +comm-help-delimiter = separate columns with STR +comm-help-zero-terminated = line delimiter is NUL, not newline +comm-help-total = output a summary +comm-help-check-order = check that the input is correctly sorted, even if all input lines are pairable +comm-help-no-check-order = do not check that the input is correctly sorted + +# Error messages +comm-error-file-not-sorted = comm: file { $file_num } is not in sorted order +comm-error-input-not-sorted = comm: input is not in sorted order +comm-error-is-directory = Is a directory +comm-error-multiple-conflicting-delimiters = multiple conflicting output delimiters specified + +# Other messages +comm-total = total diff --git a/src/uu/comm/locales/fr-FR.ftl b/src/uu/comm/locales/fr-FR.ftl new file mode 100644 index 00000000000..490a6be3501 --- /dev/null +++ b/src/uu/comm/locales/fr-FR.ftl @@ -0,0 +1,27 @@ +comm-about = Comparer deux fichiers triés ligne par ligne. + + Lorsque FICHIER1 ou FICHIER2 (pas les deux) est -, lire l'entrée standard. + + Sans options, produit une sortie à trois colonnes. La colonne un contient + les lignes uniques à FICHIER1, la colonne deux contient les lignes uniques à FICHIER2, + et la colonne trois contient les lignes communes aux deux fichiers. +comm-usage = comm [OPTION]... FICHIER1 FICHIER2 + +# Messages d'aide +comm-help-column-1 = supprimer la colonne 1 (lignes uniques à FICHIER1) +comm-help-column-2 = supprimer la colonne 2 (lignes uniques à FICHIER2) +comm-help-column-3 = supprimer la colonne 3 (lignes qui apparaissent dans les deux fichiers) +comm-help-delimiter = séparer les colonnes avec STR +comm-help-zero-terminated = le délimiteur de ligne est NUL, pas nouvelle ligne +comm-help-total = afficher un résumé +comm-help-check-order = vérifier que l'entrée est correctement triée, même si toutes les lignes d'entrée sont appariables +comm-help-no-check-order = ne pas vérifier que l'entrée est correctement triée + +# Messages d'erreur +comm-error-file-not-sorted = comm : le fichier { $file_num } n'est pas dans l'ordre trié +comm-error-input-not-sorted = comm : l'entrée n'est pas dans l'ordre trié +comm-error-is-directory = Est un répertoire +comm-error-multiple-conflicting-delimiters = plusieurs délimiteurs de sortie en conflit spécifiés + +# Autres messages +comm-total = total diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index dd49ef53b02..e383d4d6fa4 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -3,20 +3,21 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) delim mkdelim +// spell-checker:ignore (ToDO) delim mkdelim pairable use std::cmp::Ordering; -use std::fs::File; -use std::io::{self, stdin, BufRead, BufReader, Stdin}; +use std::ffi::OsString; +use std::fs::{File, metadata}; +use std::io::{self, BufRead, BufReader, Read, Stdin, stdin}; use std::path::Path; -use uucore::error::{FromIo, UResult}; +use uucore::LocalizedCommand; +use uucore::error::{FromIo, UResult, USimpleError}; +use uucore::format_usage; +use uucore::fs::paths_refer_to_same_file; use uucore::line_ending::LineEnding; -use uucore::{format_usage, help_about, help_usage}; +use uucore::translate; -use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; - -const ABOUT: &str = help_about!("comm.md"); -const USAGE: &str = help_usage!("comm.md"); +use clap::{Arg, ArgAction, ArgMatches, Command}; mod options { pub const COLUMN_1: &str = "1"; @@ -28,6 +29,30 @@ mod options { pub const FILE_2: &str = "FILE2"; pub const TOTAL: &str = "total"; pub const ZERO_TERMINATED: &str = "zero-terminated"; + pub const CHECK_ORDER: &str = "check-order"; + pub const NO_CHECK_ORDER: &str = "nocheck-order"; +} + +#[derive(Debug, Clone, Copy)] +enum FileNumber { + One, + Two, +} + +impl FileNumber { + fn as_str(&self) -> &'static str { + match self { + FileNumber::One => "1", + FileNumber::Two => "2", + } + } +} + +struct OrderChecker { + last_line: Vec, + file_num: FileNumber, + check_order: bool, + has_error: bool, } enum Input { @@ -42,7 +67,7 @@ struct LineReader { impl LineReader { fn new(input: Input, line_ending: LineEnding) -> Self { - Self { input, line_ending } + Self { line_ending, input } } fn read_line(&mut self, buf: &mut Vec) -> io::Result { @@ -61,12 +86,74 @@ impl LineReader { } } -fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) { - let delim = match opts.get_one::(options::DELIMITER).unwrap().as_str() { - "" => "\0", - delim => delim, - }; +impl OrderChecker { + fn new(file_num: FileNumber, check_order: bool) -> Self { + Self { + last_line: Vec::new(), + file_num, + check_order, + has_error: false, + } + } + + fn verify_order(&mut self, current_line: &[u8]) -> bool { + if self.last_line.is_empty() { + self.last_line = current_line.to_vec(); + return true; + } + + let is_ordered = *current_line >= *self.last_line; + if !is_ordered && !self.has_error { + eprintln!( + "{}", + translate!("comm-error-file-not-sorted", "file_num" => self.file_num.as_str()) + ); + self.has_error = true; + } + + self.last_line = current_line.to_vec(); + is_ordered || !self.check_order + } +} + +// Check if two files are identical by comparing their contents +pub fn are_files_identical(path1: &Path, path2: &Path) -> io::Result { + // First compare file sizes + let metadata1 = metadata(path1)?; + let metadata2 = metadata(path2)?; + + if metadata1.len() != metadata2.len() { + return Ok(false); + } + + let file1 = File::open(path1)?; + let file2 = File::open(path2)?; + + let mut reader1 = BufReader::new(file1); + let mut reader2 = BufReader::new(file2); + + let mut buffer1 = [0; 8192]; + let mut buffer2 = [0; 8192]; + + loop { + let bytes1 = reader1.read(&mut buffer1)?; + let bytes2 = reader2.read(&mut buffer2)?; + + if bytes1 != bytes2 { + return Ok(false); + } + if bytes1 == 0 { + return Ok(true); + } + + if buffer1[..bytes1] != buffer2[..bytes2] { + return Ok(false); + } + } +} + +fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) -> UResult<()> { let width_col_1 = usize::from(!opts.get_flag(options::COLUMN_1)); let width_col_2 = usize::from(!opts.get_flag(options::COLUMN_2)); @@ -82,6 +169,26 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) { let mut total_col_2 = 0; let mut total_col_3 = 0; + let check_order = opts.get_flag(options::CHECK_ORDER); + let no_check_order = opts.get_flag(options::NO_CHECK_ORDER); + + // Determine if we should perform order checking + let should_check_order = !no_check_order + && (check_order + || if let (Some(file1), Some(file2)) = ( + opts.get_one::(options::FILE_1), + opts.get_one::(options::FILE_2), + ) { + !(paths_refer_to_same_file(file1.as_os_str(), file2.as_os_str(), true) + || are_files_identical(Path::new(file1), Path::new(file2)).unwrap_or(false)) + } else { + true + }); + + let mut checker1 = OrderChecker::new(FileNumber::One, check_order); + let mut checker2 = OrderChecker::new(FileNumber::Two, check_order); + let mut input_error = false; + while na.is_ok() || nb.is_ok() { let ord = match (na.is_ok(), nb.is_ok()) { (false, true) => Ordering::Greater, @@ -97,6 +204,9 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) { match ord { Ordering::Less => { + if should_check_order && !checker1.verify_order(ra) { + break; + } if !opts.get_flag(options::COLUMN_1) { print!("{}", String::from_utf8_lossy(ra)); } @@ -105,6 +215,9 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) { total_col_1 += 1; } Ordering::Greater => { + if should_check_order && !checker2.verify_order(rb) { + break; + } if !opts.get_flag(options::COLUMN_2) { print!("{delim_col_2}{}", String::from_utf8_lossy(rb)); } @@ -113,6 +226,10 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) { total_col_2 += 1; } Ordering::Equal => { + if should_check_order && (!checker1.verify_order(ra) || !checker2.verify_order(rb)) + { + break; + } if !opts.get_flag(options::COLUMN_3) { print!("{delim_col_3}{}", String::from_utf8_lossy(ra)); } @@ -123,19 +240,40 @@ fn comm(a: &mut LineReader, b: &mut LineReader, opts: &ArgMatches) { total_col_3 += 1; } } + + // Track if we've seen any order errors + if (checker1.has_error || checker2.has_error) && !input_error && !check_order { + input_error = true; + } } if opts.get_flag(options::TOTAL) { let line_ending = LineEnding::from_zero_flag(opts.get_flag(options::ZERO_TERMINATED)); - print!("{total_col_1}{delim}{total_col_2}{delim}{total_col_3}{delim}total{line_ending}"); + print!( + "{total_col_1}{delim}{total_col_2}{delim}{total_col_3}{delim}{}{line_ending}", + translate!("comm-total") + ); + } + + if should_check_order && (checker1.has_error || checker2.has_error) { + // Print the input error message once at the end + if input_error { + eprintln!("{}", translate!("comm-error-input-not-sorted")); + } + Err(USimpleError::new(1, "")) + } else { + Ok(()) } } -fn open_file(name: &str, line_ending: LineEnding) -> io::Result { +fn open_file(name: &OsString, line_ending: LineEnding) -> io::Result { if name == "-" { Ok(LineReader::new(Input::Stdin(stdin()), line_ending)) } else { - let f = File::open(Path::new(name))?; + if metadata(name)?.is_dir() { + return Err(io::Error::other(translate!("comm-error-is-directory"))); + } + let f = File::open(name)?; Ok(LineReader::new( Input::FileIn(BufReader::new(f)), line_ending, @@ -145,47 +283,74 @@ fn open_file(name: &str, line_ending: LineEnding) -> io::Result { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO_TERMINATED)); - let filename1 = matches.get_one::(options::FILE_1).unwrap(); - let filename2 = matches.get_one::(options::FILE_2).unwrap(); - let mut f1 = open_file(filename1, line_ending).map_err_context(|| filename1.to_string())?; - let mut f2 = open_file(filename2, line_ending).map_err_context(|| filename2.to_string())?; + let filename1 = matches.get_one::(options::FILE_1).unwrap(); + let filename2 = matches.get_one::(options::FILE_2).unwrap(); + let mut f1 = open_file(filename1, line_ending) + .map_err_context(|| filename1.to_string_lossy().to_string())?; + let mut f2 = open_file(filename2, line_ending) + .map_err_context(|| filename2.to_string_lossy().to_string())?; - comm(&mut f1, &mut f2, &matches); - Ok(()) + // Due to default_value(), there must be at least one value here, thus unwrap() must not panic. + let all_delimiters = matches + .get_many::(options::DELIMITER) + .unwrap() + .map(String::from) + .collect::>(); + for delim in &all_delimiters[1..] { + // Note that this check is very different from ".conflicts_with_self(true).action(ArgAction::Set)", + // as this accepts duplicate *identical* arguments. + if delim != &all_delimiters[0] { + // Note: This intentionally deviate from the GNU error message by inserting the word "conflicting". + return Err(USimpleError::new( + 1, + translate!("comm-error-multiple-conflicting-delimiters"), + )); + } + } + let delim = match &*all_delimiters[0] { + "" => "\0", + delim => delim, + }; + + comm(&mut f1, &mut f2, delim, &matches) } pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) + .about(translate!("comm-about")) + .override_usage(format_usage(&translate!("comm-usage"))) .infer_long_args(true) + .args_override_self(true) .arg( Arg::new(options::COLUMN_1) .short('1') - .help("suppress column 1 (lines unique to FILE1)") + .help(translate!("comm-help-column-1")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::COLUMN_2) .short('2') - .help("suppress column 2 (lines unique to FILE2)") + .help(translate!("comm-help-column-2")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::COLUMN_3) .short('3') - .help("suppress column 3 (lines that appear in both files)") + .help(translate!("comm-help-column-3")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::DELIMITER) .long(options::DELIMITER) - .help("separate columns with STR") + .help(translate!("comm-help-delimiter")) .value_name("STR") .default_value(options::DELIMITER_DEFAULT) + .allow_hyphen_values(true) + .action(ArgAction::Append) .hide_default_value(true), ) .arg( @@ -193,23 +358,38 @@ pub fn uu_app() -> Command { .long(options::ZERO_TERMINATED) .short('z') .overrides_with(options::ZERO_TERMINATED) - .help("line delimiter is NUL, not newline") + .help(translate!("comm-help-zero-terminated")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::FILE_1) .required(true) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::FILE_2) .required(true) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::TOTAL) .long(options::TOTAL) - .help("output a summary") + .help(translate!("comm-help-total")) .action(ArgAction::SetTrue), ) + .arg( + Arg::new(options::CHECK_ORDER) + .long(options::CHECK_ORDER) + .help(translate!("comm-help-check-order")) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::NO_CHECK_ORDER) + .long(options::NO_CHECK_ORDER) + .help(translate!("comm-help-no-check-order")) + .action(ArgAction::SetTrue) + .conflicts_with(options::CHECK_ORDER), + ) } diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index 1e379b72f9f..9e5373d232b 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -1,19 +1,18 @@ [package] name = "uu_cp" -version = "0.0.25" -authors = [ - "Jordy Dickinson ", - "Joshua S. Miller ", - "uutils developers", -] -license = "MIT" description = "cp ~ (uutils) copy SOURCE to DESTINATION" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/cp" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/cp.rs" @@ -22,18 +21,23 @@ path = "src/cp.rs" clap = { workspace = true } filetime = { workspace = true } libc = { workspace = true } -quick-error = { workspace = true } +linux-raw-sys = { workspace = true, features = ["ioctl"] } selinux = { workspace = true, optional = true } uucore = { workspace = true, features = [ "backup-control", + "buf-copy", "entries", "fs", + "fsxattr", + "parser", "perms", "mode", "update-control", ] } walkdir = { workspace = true } indicatif = { workspace = true } +thiserror = { workspace = true } +fluent = { workspace = true } [target.'cfg(unix)'.dependencies] xattr = { workspace = true } @@ -44,5 +48,5 @@ name = "cp" path = "src/main.rs" [features] -feat_selinux = ["selinux"] +feat_selinux = ["selinux", "uucore/selinux"] feat_acl = ["exacl"] diff --git a/src/uu/cp/cp.md b/src/uu/cp/cp.md deleted file mode 100644 index 7485340f2ac..00000000000 --- a/src/uu/cp/cp.md +++ /dev/null @@ -1,25 +0,0 @@ -# cp - -``` -cp [OPTION]... [-T] SOURCE DEST -cp [OPTION]... SOURCE... DIRECTORY -cp [OPTION]... -t DIRECTORY SOURCE... -``` - -Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY. - -## After Help - -Do not copy a non-directory that has an existing destination with the same or newer modification timestamp; -instead, silently skip the file without failing. If timestamps are being preserved, the comparison is to the -source timestamp truncated to the resolutions of the destination file system and of the system calls used to -update timestamps; this avoids duplicate work if several `cp -pu` commands are executed with the same source -and destination. This option is ignored if the `-n` or `--no-clobber` option is also specified. Also, if -`--preserve=links` is also specified (like with `cp -au` for example), that will take precedence; consequently, -depending on the order that files are processed from the source, newer files in the destination may be replaced, -to mirror hard links in the source. which gives more control over which existing files in the destination are -replaced, and its value can be one of the following: - -* `all` This is the default operation when an `--update` option is not specified, and results in all existing files in the destination being replaced. -* `none` This is similar to the `--no-clobber` option, in that no files in the destination are replaced, but also skipping a file does not induce a failure. -* `older` This is the default operation when `--update` is specified, and results in files being replaced if they’re older than the corresponding source file. diff --git a/src/uu/cp/locales/en-US.ftl b/src/uu/cp/locales/en-US.ftl new file mode 100644 index 00000000000..bc39d613289 --- /dev/null +++ b/src/uu/cp/locales/en-US.ftl @@ -0,0 +1,116 @@ +cp-about = Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY. +cp-usage = cp [OPTION]... [-T] SOURCE DEST + cp [OPTION]... SOURCE... DIRECTORY + cp [OPTION]... -t DIRECTORY SOURCE... +cp-after-help = Do not copy a non-directory that has an existing destination with the same or newer modification timestamp; + instead, silently skip the file without failing. If timestamps are being preserved, the comparison is to the + source timestamp truncated to the resolutions of the destination file system and of the system calls used to + update timestamps; this avoids duplicate work if several cp -pu commands are executed with the same source + and destination. This option is ignored if the -n or --no-clobber option is also specified. Also, if + --preserve=links is also specified (like with cp -au for example), that will take precedence; consequently, + depending on the order that files are processed from the source, newer files in the destination may be replaced, + to mirror hard links in the source. which gives more control over which existing files in the destination are + replaced, and its value can be one of the following: + + - all This is the default operation when an --update option is not specified, and results in all existing files in the destination being replaced. + - none This is similar to the --no-clobber option, in that no files in the destination are replaced, but also skipping a file does not induce a failure. + - older This is the default operation when --update is specified, and results in files being replaced if they're older than the corresponding source file. + +# Help messages +cp-help-target-directory = copy all SOURCE arguments into target-directory +cp-help-no-target-directory = Treat DEST as a regular file and not a directory +cp-help-interactive = ask before overwriting files +cp-help-link = hard-link files instead of copying +cp-help-no-clobber = don't overwrite a file that already exists +cp-help-recursive = copy directories recursively +cp-help-strip-trailing-slashes = remove any trailing slashes from each SOURCE argument +cp-help-debug = explain how a file is copied. Implies -v +cp-help-verbose = explicitly state what is being done +cp-help-symbolic-link = make symbolic links instead of copying +cp-help-force = if an existing destination file cannot be opened, remove it and try again (this option is ignored when the -n option is also used). Currently not implemented for Windows. +cp-help-remove-destination = remove each existing destination file before attempting to open it (contrast with --force). On Windows, currently only works for writeable files. +cp-help-reflink = control clone/CoW copies. See below +cp-help-attributes-only = Don't copy the file data, just the attributes +cp-help-preserve = Preserve the specified attributes (default: mode, ownership (unix only), timestamps), if possible additional attributes: context, links, xattr, all +cp-help-preserve-default = same as --preserve=mode,ownership(unix only),timestamps +cp-help-no-preserve = don't preserve the specified attributes +cp-help-parents = use full source file name under DIRECTORY +cp-help-no-dereference = never follow symbolic links in SOURCE +cp-help-dereference = always follow symbolic links in SOURCE +cp-help-cli-symbolic-links = follow command-line symbolic links in SOURCE +cp-help-archive = Same as -dR --preserve=all +cp-help-no-dereference-preserve-links = same as --no-dereference --preserve=links +cp-help-one-file-system = stay on this file system +cp-help-sparse = control creation of sparse files. See below +cp-help-selinux = set SELinux security context of destination file to default type +cp-help-context = like -Z, or if CTX is specified then set the SELinux or SMACK security context to CTX +cp-help-progress = Display a progress bar. Note: this feature is not supported by GNU coreutils. +cp-help-copy-contents = NotImplemented: copy contents of special files when recursive + +# Error messages +cp-error-missing-file-operand = missing file operand +cp-error-missing-destination-operand = missing destination file operand after { $source } +cp-error-extra-operand = extra operand { $operand } +cp-error-same-file = { $source } and { $dest } are the same file +cp-error-backing-up-destroy-source = backing up { $dest } might destroy source; { $source } not copied +cp-error-cannot-open-for-reading = cannot open { $source } for reading +cp-error-not-writing-dangling-symlink = not writing through dangling symlink { $dest } +cp-error-failed-to-clone = failed to clone { $source } from { $dest }: { $error } +cp-error-cannot-change-attribute = cannot change attribute { $dest }: Source file is a non regular file +cp-error-cannot-stat = cannot stat { $source }: No such file or directory +cp-error-cannot-create-symlink = cannot create symlink { $dest } to { $source } +cp-error-cannot-create-hard-link = cannot create hard link { $dest } to { $source } +cp-error-omitting-directory = -r not specified; omitting directory { $dir } +cp-error-cannot-copy-directory-into-itself = cannot copy a directory, { $source }, into itself, { $dest } +cp-error-will-not-copy-through-symlink = will not copy { $source } through just-created symlink { $dest } +cp-error-will-not-overwrite-just-created = will not overwrite just-created { $dest } with { $source } +cp-error-target-not-directory = target: { $target } is not a directory +cp-error-cannot-overwrite-directory-with-non-directory = cannot overwrite directory { $dir } with non-directory +cp-error-cannot-overwrite-non-directory-with-directory = cannot overwrite non-directory with directory +cp-error-with-parents-dest-must-be-dir = with --parents, the destination must be a directory +cp-error-not-replacing = not replacing { $file } +cp-error-failed-get-current-dir = failed to get current directory { $error } +cp-error-failed-set-permissions = cannot set permissions { $path } +cp-error-backup-mutually-exclusive = options --backup and --no-clobber are mutually exclusive +cp-error-invalid-argument = invalid argument { $arg } for '{ $option }' +cp-error-option-not-implemented = Option '{ $option }' not yet implemented. +cp-error-not-all-files-copied = Not all files were copied +cp-error-reflink-always-sparse-auto = `--reflink=always` can be used only with --sparse=auto +cp-error-file-exists = { $path }: File exists +cp-error-invalid-backup-argument = --backup is mutually exclusive with -n or --update=none-fail +cp-error-reflink-not-supported = --reflink is only supported on linux and macOS +cp-error-sparse-not-supported = --sparse is only supported on linux +cp-error-not-a-directory = { $path } is not a directory +cp-error-selinux-not-enabled = SELinux was not enabled during the compile time! +cp-error-selinux-set-context = failed to set the security context of { $path }: { $error } +cp-error-selinux-get-context = failed to get security context of { $path } +cp-error-selinux-error = SELinux error: { $error } +cp-error-cannot-create-fifo = cannot create fifo { $path }: File exists +cp-error-invalid-attribute = invalid attribute { $value } +cp-error-failed-to-create-whole-tree = failed to create whole tree +cp-error-failed-to-create-directory = Failed to create directory: { $error } +cp-error-backup-format = cp: { $error } + Try '{ $exec } --help' for more information. + +# Debug enum strings +cp-debug-enum-no = no +cp-debug-enum-yes = yes +cp-debug-enum-avoided = avoided +cp-debug-enum-unsupported = unsupported +cp-debug-enum-unknown = unknown +cp-debug-enum-zeros = zeros +cp-debug-enum-seek-hole = SEEK_HOLE +cp-debug-enum-seek-hole-zeros = SEEK_HOLE + zeros + +# Warning messages +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-created-directory = { $source } -> { $dest } +cp-debug-copy-offload = copy offload: { $offload }, reflink: { $reflink }, sparse detection: { $sparse } + +# Prompts +cp-prompt-overwrite = overwrite { $path }? +cp-prompt-overwrite-with-mode = replace { $path }, overriding mode diff --git a/src/uu/cp/locales/fr-FR.ftl b/src/uu/cp/locales/fr-FR.ftl new file mode 100644 index 00000000000..2fea7cf4d7a --- /dev/null +++ b/src/uu/cp/locales/fr-FR.ftl @@ -0,0 +1,116 @@ +cp-about = Copier SOURCE vers DEST, ou plusieurs SOURCE(s) vers RÉPERTOIRE. +cp-usage = cp [OPTION]... [-T] SOURCE DEST + cp [OPTION]... SOURCE... RÉPERTOIRE + cp [OPTION]... -t RÉPERTOIRE SOURCE... +cp-after-help = Ne pas copier un non-répertoire qui a une destination existante avec le même horodatage de modification ou plus récent ; + à la place, ignorer silencieusement le fichier sans échec. Si les horodatages sont préservés, la comparaison est faite avec + l'horodatage source tronqué aux résolutions du système de fichiers de destination et des appels système utilisés pour + mettre à jour les horodatages ; cela évite le travail en double si plusieurs commandes cp -pu sont exécutées avec la même source + et destination. Cette option est ignorée si l'option -n ou --no-clobber est également spécifiée. De plus, si + --preserve=links est également spécifié (comme avec cp -au par exemple), cela aura la priorité ; par conséquent, + selon l'ordre dans lequel les fichiers sont traités depuis la source, les fichiers plus récents dans la destination peuvent être remplacés, + pour refléter les liens durs dans la source. ce qui donne plus de contrôle sur les fichiers existants dans la destination qui sont + remplacés, et sa valeur peut être l'une des suivantes : + + - all C'est l'opération par défaut lorsqu'une option --update n'est pas spécifiée, et entraîne le remplacement de tous les fichiers existants dans la destination. + - none Cela est similaire à l'option --no-clobber, en ce sens qu'aucun fichier dans la destination n'est remplacé, mais ignorer un fichier n'induit pas d'échec. + - older C'est l'opération par défaut lorsque --update est spécifié, et entraîne le remplacement des fichiers s'ils sont plus anciens que le fichier source correspondant. + +# Messages d'aide +cp-help-target-directory = copier tous les arguments SOURCE dans le répertoire cible +cp-help-no-target-directory = Traiter DEST comme un fichier régulier et non comme un répertoire +cp-help-interactive = demander avant d'écraser les fichiers +cp-help-link = créer des liens durs au lieu de copier +cp-help-no-clobber = ne pas écraser un fichier qui existe déjà +cp-help-recursive = copier les répertoires récursivement +cp-help-strip-trailing-slashes = supprimer les barres obliques finales de chaque argument SOURCE +cp-help-debug = expliquer comment un fichier est copié. Implique -v +cp-help-verbose = indiquer explicitement ce qui est fait +cp-help-symbolic-link = créer des liens symboliques au lieu de copier +cp-help-force = si un fichier de destination existant ne peut pas être ouvert, le supprimer et réessayer (cette option est ignorée lorsque l'option -n est également utilisée). Actuellement non implémenté pour Windows. +cp-help-remove-destination = supprimer chaque fichier de destination existant avant de tenter de l'ouvrir (contraste avec --force). Sur Windows, ne fonctionne actuellement que pour les fichiers inscriptibles. +cp-help-reflink = contrôler les copies clone/CoW. Voir ci-dessous +cp-help-attributes-only = Ne pas copier les données du fichier, juste les attributs +cp-help-preserve = Préserver les attributs spécifiés (par défaut : mode, propriété (unix uniquement), horodatages), si possible attributs supplémentaires : contexte, liens, xattr, all +cp-help-preserve-default = identique à --preserve=mode,ownership(unix uniquement),timestamps +cp-help-no-preserve = ne pas préserver les attributs spécifiés +cp-help-parents = utiliser le nom complet du fichier source sous RÉPERTOIRE +cp-help-no-dereference = ne jamais suivre les liens symboliques dans SOURCE +cp-help-dereference = toujours suivre les liens symboliques dans SOURCE +cp-help-cli-symbolic-links = suivre les liens symboliques de la ligne de commande dans SOURCE +cp-help-archive = Identique à -dR --preserve=all +cp-help-no-dereference-preserve-links = identique à --no-dereference --preserve=links +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-copy-contents = Non implémenté : copier le contenu des fichiers spéciaux lors de la récursion + +# Messages d'erreur +cp-error-missing-file-operand = opérande fichier manquant +cp-error-missing-destination-operand = opérande fichier de destination manquant après { $source } +cp-error-extra-operand = opérande supplémentaire { $operand } +cp-error-same-file = { $source } et { $dest } sont le même fichier +cp-error-backing-up-destroy-source = sauvegarder { $dest } pourrait détruire la source ; { $source } non copié +cp-error-cannot-open-for-reading = impossible d'ouvrir { $source } en lecture +cp-error-not-writing-dangling-symlink = ne pas écrire à travers le lien symbolique pendant { $dest } +cp-error-failed-to-clone = échec du clonage de { $source } depuis { $dest } : { $error } +cp-error-cannot-change-attribute = impossible de changer l'attribut { $dest } : Le fichier source n'est pas un fichier régulier +cp-error-cannot-stat = impossible de faire stat sur { $source } : Aucun fichier ou répertoire de ce type +cp-error-cannot-create-symlink = impossible de créer le lien symbolique { $dest } vers { $source } +cp-error-cannot-create-hard-link = impossible de créer le lien dur { $dest } vers { $source } +cp-error-omitting-directory = -r non spécifié ; répertoire { $dir } omis +cp-error-cannot-copy-directory-into-itself = impossible de copier un répertoire, { $source }, dans lui-même, { $dest } +cp-error-will-not-copy-through-symlink = ne copiera pas { $source } à travers le lien symbolique tout juste créé { $dest } +cp-error-will-not-overwrite-just-created = n'écrasera pas le fichier tout juste créé { $dest } avec { $source } +cp-error-target-not-directory = cible : { $target } n'est pas un répertoire +cp-error-cannot-overwrite-directory-with-non-directory = impossible d'écraser le répertoire { $dir } avec un non-répertoire +cp-error-cannot-overwrite-non-directory-with-directory = impossible d'écraser un non-répertoire avec un répertoire +cp-error-with-parents-dest-must-be-dir = avec --parents, la destination doit être un répertoire +cp-error-not-replacing = ne remplace pas { $file } +cp-error-failed-get-current-dir = échec de l'obtention du répertoire actuel { $error } +cp-error-failed-set-permissions = impossible de définir les permissions { $path } +cp-error-backup-mutually-exclusive = les options --backup et --no-clobber sont mutuellement exclusives +cp-error-invalid-argument = argument invalide { $arg } pour '{ $option }' +cp-error-option-not-implemented = Option '{ $option }' pas encore implémentée. +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-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 } +cp-error-selinux-get-context = échec de l'obtention du contexte de sécurité de { $path } +cp-error-selinux-error = Erreur SELinux : { $error } +cp-error-cannot-create-fifo = impossible de créer le fifo { $path } : Le fichier existe +cp-error-invalid-attribute = attribut invalide { $value } +cp-error-failed-to-create-whole-tree = échec de la création de l'arborescence complète +cp-error-failed-to-create-directory = Échec de la création du répertoire : { $error } +cp-error-backup-format = cp : { $error } + Tentez '{ $exec } --help' pour plus d'informations. + +# Debug enum strings +cp-debug-enum-no = non +cp-debug-enum-yes = oui +cp-debug-enum-avoided = évité +cp-debug-enum-unsupported = non supporté +cp-debug-enum-unknown = inconnu +cp-debug-enum-zeros = zéros +cp-debug-enum-seek-hole = SEEK_HOLE +cp-debug-enum-seek-hole-zeros = SEEK_HOLE + zéros + +# Messages d'avertissement +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-created-directory = { $source } -> { $dest } +cp-debug-copy-offload = copy offload : { $offload }, reflink : { $reflink }, sparse detection : { $sparse } + +# Invites +cp-prompt-overwrite = écraser { $path } ? +cp-prompt-overwrite-with-mode = remplacer { $path }, en écrasant le mode diff --git a/src/uu/cp/src/copydir.rs b/src/uu/cp/src/copydir.rs index 7a9d797e81c..d7066610602 100644 --- a/src/uu/cp/src/copydir.rs +++ b/src/uu/cp/src/copydir.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore TODO canonicalizes direntry pathbuf symlinked +// spell-checker:ignore TODO canonicalizes direntry pathbuf symlinked IRWXO IRWXG //! Recursively copy the contents of a directory. //! //! See the [`copy_directory`] function for more information. @@ -18,21 +18,23 @@ use indicatif::ProgressBar; use uucore::display::Quotable; use uucore::error::UIoError; use uucore::fs::{ - canonicalize, path_ends_with_terminator, FileInformation, MissingHandling, ResolveMode, + FileInformation, MissingHandling, ResolveMode, canonicalize, path_ends_with_terminator, }; +use uucore::translate; + use uucore::show; use uucore::show_error; use uucore::uio_error; use walkdir::{DirEntry, WalkDir}; use crate::{ - aligned_ancestors, context_for, copy_attributes, copy_file, copy_link, CopyResult, Error, - Options, + CopyResult, CpError, Options, aligned_ancestors, context_for, copy_attributes, copy_file, + copy_link, }; /// Ensure a Windows path starts with a `\\?`. #[cfg(target_os = "windows")] -fn adjust_canonicalization(p: &Path) -> Cow { +fn adjust_canonicalization(p: &Path) -> Cow<'_, Path> { // In some cases, \\? can be missing on some Windows paths. Add it at the // beginning unless the path is prefixed with a device namespace. const VERBATIM_PREFIX: &str = r"\\?"; @@ -42,8 +44,9 @@ fn adjust_canonicalization(p: &Path) -> Cow { .components() .next() .and_then(|comp| comp.as_os_str().to_str()) - .map(|p_str| p_str.starts_with(VERBATIM_PREFIX) || p_str.starts_with(DEVICE_NS_PREFIX)) - .unwrap_or_default(); + .is_some_and(|p_str| { + p_str.starts_with(VERBATIM_PREFIX) || p_str.starts_with(DEVICE_NS_PREFIX) + }); if has_prefix { p.into() @@ -79,6 +82,12 @@ fn get_local_to_root_parent( } } +/// Given an iterator, return all its items except the last. +fn skip_last(mut iter: impl Iterator) -> impl Iterator { + let last = iter.next(); + iter.scan(last, |state, item| state.replace(item)) +} + /// Paths that are invariant throughout the traversal when copying a directory. struct Context<'a> { /// The current working directory at the time of starting the traversal. @@ -95,7 +104,7 @@ struct Context<'a> { } impl<'a> Context<'a> { - fn new(root: &'a Path, target: &'a Path) -> std::io::Result { + fn new(root: &'a Path, target: &'a Path) -> io::Result { let current_dir = env::current_dir()?; let root_path = current_dir.join(root); let root_parent = if target.exists() && !root.to_str().unwrap().ends_with("/.") { @@ -162,20 +171,24 @@ struct Entry { } impl Entry { - fn new( + fn new>( context: &Context, - direntry: &DirEntry, + source: A, no_target_dir: bool, ) -> Result { - let source_relative = direntry.path().to_path_buf(); + let source = source.as_ref(); + let source_relative = source.to_path_buf(); let source_absolute = context.current_dir.join(&source_relative); let mut descendant = get_local_to_root_parent(&source_absolute, context.root_parent.as_deref())?; if no_target_dir { - let source_is_dir = direntry.path().is_dir(); + let source_is_dir = source.is_dir(); if path_ends_with_terminator(context.target) && source_is_dir { - if let Err(e) = std::fs::create_dir_all(context.target) { - eprintln!("Failed to create directory: {}", e); + if let Err(e) = fs::create_dir_all(context.target) { + eprintln!( + "{}", + translate!("cp-error-failed-to-create-directory", "error" => e) + ); } } else { descendant = descendant.strip_prefix(context.root)?.to_path_buf(); @@ -193,34 +206,15 @@ impl Entry { } } -/// Decide whether the given path ends with `/.`. -/// -/// # Examples -/// -/// ```rust,ignore -/// assert!(ends_with_slash_dot("/.")); -/// assert!(ends_with_slash_dot("./.")); -/// assert!(ends_with_slash_dot("a/.")); -/// -/// assert!(!ends_with_slash_dot(".")); -/// assert!(!ends_with_slash_dot("./")); -/// assert!(!ends_with_slash_dot("a/..")); -/// ``` -fn ends_with_slash_dot

(path: P) -> bool -where - P: AsRef, -{ - // `path.ends_with(".")` does not seem to work - path.as_ref().display().to_string().ends_with("/.") -} - +#[allow(clippy::too_many_arguments)] /// Copy a single entry during a directory traversal. fn copy_direntry( - progress_bar: &Option, + progress_bar: Option<&ProgressBar>, entry: Entry, options: &Options, symlinked_files: &mut HashSet, preserve_hard_links: bool, + copied_destinations: &HashSet, copied_files: &mut HashMap, ) -> CopyResult<()> { let Entry { @@ -233,82 +227,59 @@ fn copy_direntry( // If the source is a symbolic link and the options tell us not to // dereference the link, then copy the link object itself. if source_absolute.is_symlink() && !options.dereference { - return copy_link(&source_absolute, &local_to_target, symlinked_files); + return copy_link(&source_absolute, &local_to_target, symlinked_files, options); } // If the source is a directory and the destination does not // exist, ... - if source_absolute.is_dir() - && !ends_with_slash_dot(&source_absolute) - && !local_to_target.exists() - { - if target_is_file { - return Err("cannot overwrite non-directory with directory".into()); + if source_absolute.is_dir() && !local_to_target.exists() { + return if target_is_file { + Err(translate!("cp-error-cannot-overwrite-non-directory-with-directory").into()) } else { - // TODO Since the calling code is traversing from the root - // of the directory structure, I don't think - // `create_dir_all()` will have any benefit over - // `create_dir()`, since all the ancestor directories - // should have already been created. - fs::create_dir_all(&local_to_target)?; + build_dir(&local_to_target, false, options, Some(&source_absolute))?; if options.verbose { println!("{}", context_for(&source_relative, &local_to_target)); } - return Ok(()); - } + Ok(()) + }; } // If the source is not a directory, then we need to copy the file. if !source_absolute.is_dir() { - if preserve_hard_links { - match copy_file( - progress_bar, - &source_absolute, - local_to_target.as_path(), - options, - symlinked_files, - copied_files, - false, - ) { - Ok(_) => Ok(()), - Err(err) => { - if source_absolute.is_symlink() { - // silent the error with a symlink - // In case we do --archive, we might copy the symlink - // before the file itself - Ok(()) - } else { - Err(err) - } + if let Err(err) = copy_file( + progress_bar, + &source_absolute, + local_to_target.as_path(), + options, + symlinked_files, + copied_destinations, + copied_files, + false, + ) { + if preserve_hard_links { + if !source_absolute.is_symlink() { + return Err(err); } - }?; - } else { - // At this point, `path` is just a plain old file. - // Terminate this function immediately if there is any - // kind of error *except* a "permission denied" error. - // - // TODO What other kinds of errors, if any, should - // cause us to continue walking the directory? - match copy_file( - progress_bar, - &source_absolute, - local_to_target.as_path(), - options, - symlinked_files, - copied_files, - false, - ) { - Ok(_) => {} - Err(Error::IoErrContext(e, _)) - if e.kind() == std::io::ErrorKind::PermissionDenied => - { - show!(uio_error!( - e, - "cannot open {} for reading", - source_relative.quote(), - )); + // silent the error with a symlink + // In case we do --archive, we might copy the symlink + // before the file itself + } else { + // At this point, `path` is just a plain old file. + // Terminate this function immediately if there is any + // kind of error *except* a "permission denied" error. + // + // TODO What other kinds of errors, if any, should + // cause us to continue walking the directory? + match err { + CpError::IoErrContext(e, _) if e.kind() == io::ErrorKind::PermissionDenied => { + show!(uio_error!( + e, + "{}", + translate!("cp-error-cannot-open-for-reading", "source" => source_relative.quote()), + )); + } + e => return Err(e), } - Err(e) => return Err(e), } } } @@ -323,19 +294,17 @@ fn copy_direntry( /// /// Any errors encountered copying files in the tree will be logged but /// will not cause a short-circuit. +#[allow(clippy::too_many_arguments)] pub(crate) fn copy_directory( - progress_bar: &Option, + progress_bar: Option<&ProgressBar>, root: &Path, target: &Path, options: &Options, symlinked_files: &mut HashSet, + copied_destinations: &HashSet, copied_files: &mut HashMap, source_in_command_line: bool, ) -> CopyResult<()> { - if !options.recursive { - return Err(format!("-r not specified; omitting directory {}", root.quote()).into()); - } - // if no-dereference is enabled and this is a symlink, copy it as a file if !options.dereference(source_in_command_line) && root.is_symlink() { return copy_file( @@ -344,18 +313,19 @@ pub(crate) fn copy_directory( target, options, symlinked_files, + copied_destinations, copied_files, source_in_command_line, ); } + if !options.recursive { + return Err(translate!("cp-error-omitting-directory", "dir" => root.quote()).into()); + } + // check if root is a prefix of target if path_has_prefix(target, root)? { - return Err(format!( - "cannot copy a directory, {}, into itself, {}", - root.quote(), - target.join(root.file_name().unwrap()).quote() - ) + return Err(translate!("cp-error-cannot-copy-directory-into-itself", "source" => root.quote(), "dest" => target.join(root.file_name().unwrap()).quote()) .into()); } @@ -370,8 +340,7 @@ pub(crate) fn copy_directory( let tmp = if options.parents { if let Some(parent) = root.parent() { let new_target = target.join(parent); - std::fs::create_dir_all(&new_target)?; - + build_dir(&new_target, true, options, None)?; if options.verbose { // For example, if copying file `a/b/c` and its parents // to directory `d/`, then print @@ -400,9 +369,17 @@ pub(crate) fn copy_directory( // the target directory. let context = match Context::new(root, target) { Ok(c) => c, - Err(e) => return Err(format!("failed to get current directory {e}").into()), + Err(e) => { + return Err(translate!("cp-error-failed-get-current-dir", "error" => e).into()); + } }; + // The directory we were in during the previous iteration + let mut last_iter: Option = None; + + // Keep track of all directories we've created that need permission fixes + let mut dirs_needing_permissions: Vec<(PathBuf, PathBuf)> = Vec::new(); + // Traverse the contents of the directory, copying each one. for direntry_result in WalkDir::new(root) .same_file_system(options.one_file_system) @@ -410,30 +387,97 @@ pub(crate) fn copy_directory( { match direntry_result { Ok(direntry) => { - let entry = Entry::new(&context, &direntry, options.no_target_dir)?; + let entry = Entry::new(&context, direntry.path(), options.no_target_dir)?; + copy_direntry( progress_bar, entry, options, symlinked_files, preserve_hard_links, + copied_destinations, copied_files, )?; + + // We omit certain permissions when creating directories + // to prevent other users from accessing them before they're done. + // We thus need to fix the permissions of each directory we copy + // once it's contents are ready. + // This "fixup" is implemented here in a memory-efficient manner. + // + // We detect iterations where we "walk up" the directory tree, + // and fix permissions on all the directories we exited. + // (Note that there can be more than one! We might step out of + // `./a/b/c` into `./a/`, in which case we'll need to fix the + // permissions of both `./a/b/c` and `./a/b`, in that order.) + if direntry.file_type().is_dir() { + // Add this directory to our list for permission fixing later + let entry_for_tracking = + Entry::new(&context, direntry.path(), options.no_target_dir)?; + dirs_needing_permissions.push(( + entry_for_tracking.source_absolute, + entry_for_tracking.local_to_target, + )); + + // If true, last_iter is not a parent of this iter. + // The means we just exited a directory. + let went_up = if let Some(last_iter) = &last_iter { + last_iter.path().strip_prefix(direntry.path()).is_ok() + } else { + false + }; + + if went_up { + // Compute the "difference" between `last_iter` and `direntry`. + // For example, if... + // - last_iter = `a/b/c/d` + // - direntry = `a/b` + // then diff = `c/d` + // + // All the unwraps() here are unreachable. + let last_iter = last_iter.as_ref().unwrap(); + let diff = last_iter.path().strip_prefix(direntry.path()).unwrap(); + + // Fix permissions for every entry in `diff`, inside-out. + // We skip the last directory (which will be `.`) because + // its permissions will be fixed when we walk _out_ of it. + // (at this point, we might not be done copying `.`!) + for p in skip_last(diff.ancestors()) { + let src = direntry.path().join(p); + let entry = Entry::new(&context, &src, options.no_target_dir)?; + + copy_attributes( + &entry.source_absolute, + &entry.local_to_target, + &options.attributes, + )?; + } + } + + last_iter = Some(direntry); + } } + // Print an error message, but continue traversing the directory. - Err(e) => show_error!("{}", e), + Err(e) => show_error!("{e}"), } } - // Copy the attributes from the root directory to the target directory. + // Fix permissions for all directories we created + // This ensures that even sibling directories get their permissions fixed + for (source_path, dest_path) in dirs_needing_permissions { + copy_attributes(&source_path, &dest_path, &options.attributes)?; + } + + // Also fix permissions for parent directories, + // if we were asked to create them. if options.parents { let dest = target.join(root.file_name().unwrap()); - copy_attributes(root, dest.as_path(), &options.attributes)?; for (x, y) in aligned_ancestors(root, dest.as_path()) { - copy_attributes(x, y, &options.attributes)?; + if let Ok(src) = canonicalize(x, MissingHandling::Normal, ResolveMode::Physical) { + copy_attributes(&src, y, &options.attributes)?; + } } - } else { - copy_attributes(root, target, &options.attributes)?; } Ok(()) @@ -463,25 +507,61 @@ pub fn path_has_prefix(p1: &Path, p2: &Path) -> io::Result { Ok(pathbuf1.starts_with(pathbuf2)) } -#[cfg(test)] -mod tests { - use super::ends_with_slash_dot; - - #[test] - #[allow(clippy::cognitive_complexity)] - fn test_ends_with_slash_dot() { - assert!(ends_with_slash_dot("/.")); - assert!(ends_with_slash_dot("./.")); - assert!(ends_with_slash_dot("../.")); - assert!(ends_with_slash_dot("a/.")); - assert!(ends_with_slash_dot("/a/.")); - - assert!(!ends_with_slash_dot("")); - assert!(!ends_with_slash_dot(".")); - assert!(!ends_with_slash_dot("./")); - assert!(!ends_with_slash_dot("..")); - assert!(!ends_with_slash_dot("/..")); - assert!(!ends_with_slash_dot("a/..")); - assert!(!ends_with_slash_dot("/a/..")); +/// Builds a directory at the specified path with the given options. +/// +/// # Notes +/// - If `copy_attributes_from` is `Some`, the new directory's attributes will be +/// copied from the provided file. Otherwise, the new directory will have the default +/// attributes for the current user. +/// - This method excludes certain permissions if ownership or special mode bits could +/// potentially change. (See `test_dir_perm_race_with_preserve_mode_and_ownership`) +/// - The `recursive` flag determines whether parent directories should be created +/// if they do not already exist. +// we need to allow unused_variable since `options` might be unused in non unix systems +#[allow(unused_variables)] +fn build_dir( + path: &PathBuf, + recursive: bool, + options: &Options, + copy_attributes_from: Option<&Path>, +) -> CopyResult<()> { + let mut builder = fs::DirBuilder::new(); + builder.recursive(recursive); + + // To prevent unauthorized access before the folder is ready, + // exclude certain permissions if ownership or special mode bits + // could potentially change. + #[cfg(unix)] + { + use crate::Preserve; + use std::os::unix::fs::PermissionsExt; + + // we need to allow trivial casts here because some systems like linux have u32 constants in + // in libc while others don't. + #[allow(clippy::unnecessary_cast)] + let mut excluded_perms = if matches!(options.attributes.ownership, Preserve::Yes { .. }) { + libc::S_IRWXG | libc::S_IRWXO // exclude rwx for group and other + } else if matches!(options.attributes.mode, Preserve::Yes { .. }) { + libc::S_IWGRP | libc::S_IWOTH //exclude w for group and other + } else { + 0 + } as u32; + + let umask = if copy_attributes_from.is_some() + && matches!(options.attributes.mode, Preserve::Yes { .. }) + { + !fs::symlink_metadata(copy_attributes_from.unwrap())? + .permissions() + .mode() + } else { + uucore::mode::get_umask() + }; + + excluded_perms |= umask; + let mode = !excluded_perms & 0o777; //use only the last three octet bits + std::os::unix::fs::DirBuilderExt::mode(&mut builder, mode); } + + builder.create(path)?; + Ok(()) } diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 778ddf843b6..0c3a6ca0c5e 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -4,41 +4,44 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) copydir ficlone fiemap ftruncate linkgs lstat nlink nlinks pathbuf pwrite reflink strs xattrs symlinked deduplicated advcpmv nushell IRWXG IRWXO IRWXU IRWXUGO IRWXU IRWXG IRWXO IRWXUGO -use quick_error::quick_error; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; -use std::env; -#[cfg(not(windows))] -use std::ffi::CString; -use std::fs::{self, File, Metadata, OpenOptions, Permissions}; -use std::io; -#[cfg(unix)] -use std::os::unix::ffi::OsStrExt; +use std::ffi::OsString; +use std::fmt::Display; +use std::fs::{self, Metadata, OpenOptions, Permissions}; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, PermissionsExt}; +#[cfg(unix)] +use std::os::unix::net::UnixListener; use std::path::{Path, PathBuf, StripPrefixError}; +use std::{fmt, io}; +use uucore::LocalizedCommand; +#[cfg(all(unix, not(target_os = "android")))] +use uucore::fsxattr::copy_xattrs; +use uucore::translate; -use clap::{builder::ValueParser, crate_version, Arg, ArgAction, ArgMatches, Command}; +use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser, value_parser}; use filetime::FileTime; use indicatif::{ProgressBar, ProgressStyle}; -#[cfg(unix)] -use libc::mkfifo; -use quick_error::ResultExt; +use thiserror::Error; use platform::copy_on_write; use uucore::display::Quotable; -use uucore::error::{set_exit_code, UClapError, UError, UResult, UUsageError}; +use uucore::error::{UError, UResult, UUsageError, set_exit_code}; +#[cfg(unix)] +use uucore::fs::make_fifo; use uucore::fs::{ - are_hardlinks_to_same_file, canonicalize, is_symlink_loop, path_ends_with_terminator, - paths_refer_to_same_file, FileInformation, MissingHandling, ResolveMode, + FileInformation, MissingHandling, ResolveMode, are_hardlinks_to_same_file, canonicalize, + get_filename, is_symlink_loop, normalize_path, path_ends_with_terminator, + paths_refer_to_same_file, }; use uucore::{backup_control, update_control}; // These are exposed for projects (e.g. nushell) that want to create an `Options` value, which // requires these enum. pub use uucore::{backup_control::BackupMode, update_control::UpdateMode}; use uucore::{ - format_usage, help_about, help_section, help_usage, prompt_yes, show_error, show_warning, - util_name, + format_usage, parser::shortcut_value_parser::ShortcutValueParser, prompt_yes, show_error, + show_warning, }; use crate::copydir::copy_directory; @@ -46,74 +49,105 @@ use crate::copydir::copy_directory; mod copydir; mod platform; -quick_error! { - #[derive(Debug)] - pub enum Error { - /// Simple io::Error wrapper - IoErr(err: io::Error) { from() source(err) display("{}", err)} - - /// Wrapper for io::Error with path context - IoErrContext(err: io::Error, path: String) { - display("{}: {}", path, err) - context(path: &'a str, err: io::Error) -> (err, path.to_owned()) - context(context: String, err: io::Error) -> (err, context) - source(err) - } - - /// General copy error - Error(err: String) { - display("{}", err) - from(err: String) -> (err) - from(err: &'static str) -> (err.to_string()) - } - - /// Represents the state when a non-fatal error has occurred - /// and not all files were copied. - NotAllFilesCopied {} - - /// Simple walkdir::Error wrapper - WalkDirErr(err: walkdir::Error) { from() display("{}", err) source(err) } - - /// Simple std::path::StripPrefixError wrapper - StripPrefixError(err: StripPrefixError) { from() } - - /// Result of a skipped file - /// Currently happens when "no" is selected in interactive mode - Skipped { } - - /// Result of a skipped file - InvalidArgument(description: String) { display("{}", description) } +#[derive(Debug, Error)] +pub enum CpError { + /// Simple [`io::Error`] wrapper + #[error("{0}")] + IoErr(#[from] io::Error), + + /// Wrapper for [`io::Error`] with path context + #[error("{1}: {0}")] + IoErrContext(io::Error, String), + + /// General copy error + #[error("{0}")] + Error(String), + + /// Represents the state when a non-fatal error has occurred + /// and not all files were copied. + #[error("{}", translate!("cp-error-not-all-files-copied"))] + NotAllFilesCopied, + + /// Simple [`walkdir::Error`] wrapper + #[error("{0}")] + WalkDirErr(#[from] walkdir::Error), + + /// Simple [`StripPrefixError`] wrapper + #[error(transparent)] + StripPrefixError(#[from] StripPrefixError), + + /// Result of a skipped file + /// Currently happens when "no" is selected in interactive mode or when + /// `no-clobber` flag is set and destination is already present. + /// `exit with error` is used to determine which exit code should be returned. + #[error("Skipped copying file (exit with error = {0})")] + Skipped(bool), + + /// Invalid argument error + #[error("{0}")] + InvalidArgument(String), + + /// All standard options are included as an implementation + /// path, but those that are not implemented yet should return + /// a `NotImplemented` error. + #[error("{}", translate!("cp-error-option-not-implemented", "option" => 0))] + NotImplemented(String), + + /// Invalid arguments to backup + #[error(transparent)] + Backup(#[from] BackupError), + + #[error("{}", translate!("cp-error-not-a-directory", "path" => .0.quote()))] + NotADirectory(PathBuf), +} - /// All standard options are included as an an implementation - /// path, but those that are not implemented yet should return - /// a NotImplemented error. - NotImplemented(opt: String) { display("Option '{}' not yet implemented.", opt) } +// Manual impl for &str +impl From<&'static str> for CpError { + fn from(s: &'static str) -> Self { + Self::Error(s.to_string()) + } +} - /// Invalid arguments to backup - Backup(description: String) { display("{}\nTry '{} --help' for more information.", description, uucore::execution_phrase()) } +impl From for CpError { + fn from(s: String) -> Self { + Self::Error(s) + } +} - NotADirectory(path: PathBuf) { display("'{}' is not a directory", path.display()) } +#[derive(Debug)] +pub struct BackupError(String); + +impl Display for BackupError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + translate!("cp-error-backup-format", "error" => self.0.clone(), "exec" => uucore::execution_phrase()) + ) } } -impl UError for Error { +impl std::error::Error for BackupError {} + +impl UError for CpError { fn code(&self) -> i32 { EXIT_ERR } } -pub type CopyResult = Result; +pub type CopyResult = Result; /// Specifies how to overwrite files. -#[derive(Clone, Copy, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] pub enum ClobberMode { Force, RemoveDestination, + #[default] Standard, } /// Specifies whether files should be overwritten. -#[derive(Clone, Copy, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum OverwriteMode { /// [Default] Always overwrite existing files Clobber(ClobberMode), @@ -123,18 +157,39 @@ pub enum OverwriteMode { NoClobber, } +impl Default for OverwriteMode { + fn default() -> Self { + Self::Clobber(ClobberMode::default()) + } +} + /// Possible arguments for `--reflink`. -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum ReflinkMode { Always, Auto, Never, } +impl Default for ReflinkMode { + #[allow(clippy::derivable_impls)] + fn default() -> Self { + #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] + { + ReflinkMode::Auto + } + #[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] + { + ReflinkMode::Never + } + } +} + /// Possible arguments for `--sparse`. -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] pub enum SparseMode { Always, + #[default] Auto, Never, } @@ -147,10 +202,11 @@ pub enum TargetType { } /// Copy action to perform -#[derive(PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Default)] pub enum CopyMode { Link, SymLink, + #[default] Copy, Update, AttrOnly, @@ -169,7 +225,7 @@ pub enum CopyMode { /// For full compatibility with GNU, these options should also combine. We /// currently only do a best effort imitation of that behavior, because it is /// difficult to achieve in clap, especially with `--no-preserve`. -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct Attributes { #[cfg(unix)] pub ownership: Preserve, @@ -180,6 +236,12 @@ pub struct Attributes { pub xattr: Preserve, } +impl Default for Attributes { + fn default() -> Self { + Self::NONE + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Preserve { // explicit means whether the --no-preserve flag is used or not to distinguish out the default value. @@ -219,6 +281,7 @@ impl Ord for Preserve { /// /// The fields are documented with the arguments that determine their value. #[allow(dead_code)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Options { /// `--attributes-only` pub attributes_only: bool, @@ -280,6 +343,47 @@ pub struct Options { pub verbose: bool, /// `-g`, `--progress` pub progress_bar: bool, + /// -Z + pub set_selinux_context: bool, + // --context + pub context: Option, +} + +impl Default for Options { + fn default() -> Self { + Self { + attributes_only: false, + backup: BackupMode::default(), + copy_contents: false, + cli_dereference: false, + copy_mode: CopyMode::default(), + dereference: false, + no_target_dir: false, + one_file_system: false, + overwrite: OverwriteMode::default(), + parents: false, + sparse_mode: SparseMode::default(), + strip_trailing_slashes: false, + reflink_mode: ReflinkMode::default(), + attributes: Attributes::default(), + recursive: false, + backup_suffix: backup_control::DEFAULT_BACKUP_SUFFIX.to_owned(), + target_dir: None, + update: UpdateMode::default(), + debug: false, + verbose: false, + progress_bar: false, + set_selinux_context: false, + context: None, + } + } +} + +/// Enum representing if a file has been skipped. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum PerformedAction { + Copied, + Skipped, } /// Enum representing various debug states of the offload and reflink actions. @@ -313,28 +417,30 @@ struct CopyDebug { sparse_detection: SparseDebug, } -impl OffloadReflinkDebug { - fn to_string(&self) -> &'static str { - match self { - Self::No => "no", - Self::Yes => "yes", - Self::Avoided => "avoided", - Self::Unsupported => "unsupported", - Self::Unknown => "unknown", - } +impl Display for OffloadReflinkDebug { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let msg = match self { + Self::No => translate!("cp-debug-enum-no"), + Self::Yes => translate!("cp-debug-enum-yes"), + Self::Avoided => translate!("cp-debug-enum-avoided"), + Self::Unsupported => translate!("cp-debug-enum-unsupported"), + Self::Unknown => translate!("cp-debug-enum-unknown"), + }; + write!(f, "{msg}") } } -impl SparseDebug { - fn to_string(&self) -> &'static str { - match self { - Self::No => "no", - Self::Zeros => "zeros", - Self::SeekHole => "SEEK_HOLE", - Self::SeekHoleZeros => "SEEK_HOLE + zeros", - Self::Unsupported => "unsupported", - Self::Unknown => "unknown", - } +impl Display for SparseDebug { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let msg = match self { + Self::No => translate!("cp-debug-enum-no"), + Self::Zeros => translate!("cp-debug-enum-zeros"), + Self::SeekHole => translate!("cp-debug-enum-seek-hole"), + Self::SeekHoleZeros => translate!("cp-debug-enum-seek-hole-zeros"), + Self::Unsupported => translate!("cp-debug-enum-unsupported"), + Self::Unknown => translate!("cp-debug-enum-unknown"), + }; + write!(f, "{msg}") } } @@ -343,17 +449,11 @@ impl SparseDebug { /// It prints the debug information of the offload, reflink, and sparse detection actions. fn show_debug(copy_debug: &CopyDebug) { println!( - "copy offload: {}, reflink: {}, sparse detection: {}", - copy_debug.offload.to_string(), - copy_debug.reflink.to_string(), - copy_debug.sparse_detection.to_string(), + "{}", + translate!("cp-debug-copy-offload", "offload" => copy_debug.offload, "reflink" => copy_debug.reflink, "sparse" => copy_debug.sparse_detection) ); } -const ABOUT: &str = help_about!("cp.md"); -const USAGE: &str = help_usage!("cp.md"); -const AFTER_HELP: &str = help_section!("after help", "cp.md"); - static EXIT_ERR: i32 = 1; // Argument constants @@ -382,6 +482,7 @@ mod options { pub const RECURSIVE: &str = "recursive"; pub const REFLINK: &str = "reflink"; pub const REMOVE_DESTINATION: &str = "remove-destination"; + pub const SELINUX: &str = "Z"; pub const SPARSE: &str = "sparse"; pub const STRIP_TRAILING_SLASHES: &str = "strip-trailing-slashes"; pub const SYMBOLIC_LINK: &str = "symbolic-link"; @@ -396,22 +497,20 @@ static PRESERVABLE_ATTRIBUTES: &[&str] = &[ "ownership", "timestamps", "context", - "link", "links", "xattr", "all", ]; #[cfg(not(unix))] -static PRESERVABLE_ATTRIBUTES: &[&str] = &[ - "mode", - "timestamps", - "context", - "link", - "links", - "xattr", - "all", -]; +static PRESERVABLE_ATTRIBUTES: &[&str] = + &["mode", "timestamps", "context", "links", "xattr", "all"]; + +const PRESERVE_DEFAULT_VALUES: &str = if cfg!(unix) { + "mode,ownership,timestamp" +} else { + "mode,timestamp" +}; pub fn uu_app() -> Command { const MODE_ARGS: &[&str] = &[ @@ -422,11 +521,13 @@ pub fn uu_app() -> Command { options::COPY_CONTENTS, ]; Command::new(uucore::util_name()) - .version(crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .version(uucore::crate_version!()) + .about(translate!("cp-about")) + .help_template(uucore::localized_help_template(uucore::util_name())) + .override_usage(format_usage(&translate!("cp-usage"))) .after_help(format!( - "{AFTER_HELP}\n\n{}", + "{}\n\n{}", + translate!("cp-after-help"), backup_control::BACKUP_CONTROL_LONG_HELP )) .infer_long_args(true) @@ -439,14 +540,14 @@ pub fn uu_app() -> Command { .value_name(options::TARGET_DIRECTORY) .value_hint(clap::ValueHint::DirPath) .value_parser(ValueParser::path_buf()) - .help("copy all SOURCE arguments into target-directory"), + .help(translate!("cp-help-target-directory")), ) .arg( Arg::new(options::NO_TARGET_DIRECTORY) .short('T') .long(options::NO_TARGET_DIRECTORY) .conflicts_with(options::TARGET_DIRECTORY) - .help("Treat DEST as a regular file and not a directory") + .help(translate!("cp-help-no-target-directory")) .action(ArgAction::SetTrue), ) .arg( @@ -454,7 +555,7 @@ pub fn uu_app() -> Command { .short('i') .long(options::INTERACTIVE) .overrides_with(options::NO_CLOBBER) - .help("ask before overwriting files") + .help(translate!("cp-help-interactive")) .action(ArgAction::SetTrue), ) .arg( @@ -462,7 +563,7 @@ pub fn uu_app() -> Command { .short('l') .long(options::LINK) .overrides_with_all(MODE_ARGS) - .help("hard-link files instead of copying") + .help(translate!("cp-help-link")) .action(ArgAction::SetTrue), ) .arg( @@ -470,35 +571,35 @@ pub fn uu_app() -> Command { .short('n') .long(options::NO_CLOBBER) .overrides_with(options::INTERACTIVE) - .help("don't overwrite a file that already exists") + .help(translate!("cp-help-no-clobber")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::RECURSIVE) - .short('r') - .visible_short_alias('R') + .short('R') + .visible_short_alias('r') .long(options::RECURSIVE) // --archive sets this option - .help("copy directories recursively") + .help(translate!("cp-help-recursive")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::STRIP_TRAILING_SLASHES) .long(options::STRIP_TRAILING_SLASHES) - .help("remove any trailing slashes from each SOURCE argument") + .help(translate!("cp-help-strip-trailing-slashes")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::DEBUG) .long(options::DEBUG) - .help("explain how a file is copied. Implies -v") + .help(translate!("cp-help-debug")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::VERBOSE) .short('v') .long(options::VERBOSE) - .help("explicitly state what is being done") + .help(translate!("cp-help-verbose")) .action(ArgAction::SetTrue), ) .arg( @@ -506,29 +607,21 @@ pub fn uu_app() -> Command { .short('s') .long(options::SYMBOLIC_LINK) .overrides_with_all(MODE_ARGS) - .help("make symbolic links instead of copying") + .help(translate!("cp-help-symbolic-link")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::FORCE) .short('f') .long(options::FORCE) - .help( - "if an existing destination file cannot be opened, remove it and \ - try again (this option is ignored when the -n option is also used). \ - Currently not implemented for Windows.", - ) + .help(translate!("cp-help-force")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::REMOVE_DESTINATION) .long(options::REMOVE_DESTINATION) .overrides_with(options::FORCE) - .help( - "remove each existing destination file before attempting to open it \ - (contrast with --force). On Windows, currently only works for \ - writeable files.", - ) + .help(translate!("cp-help-remove-destination")) .action(ArgAction::SetTrue), ) .arg(backup_control::arguments::backup()) @@ -543,15 +636,15 @@ pub fn uu_app() -> Command { .overrides_with_all(MODE_ARGS) .require_equals(true) .default_missing_value("always") - .value_parser(["auto", "always", "never"]) + .value_parser(ShortcutValueParser::new(["auto", "always", "never"])) .num_args(0..=1) - .help("control clone/CoW copies. See below"), + .help(translate!("cp-help-reflink")), ) .arg( Arg::new(options::ATTRIBUTES_ONLY) .long(options::ATTRIBUTES_ONLY) .overrides_with_all(MODE_ARGS) - .help("Don't copy the file data, just the attributes") + .help(translate!("cp-help-attributes-only")) .action(ArgAction::SetTrue), ) .arg( @@ -559,48 +652,38 @@ pub fn uu_app() -> Command { .long(options::PRESERVE) .action(ArgAction::Append) .use_value_delimiter(true) - .value_parser(clap::builder::PossibleValuesParser::new( - PRESERVABLE_ATTRIBUTES, - )) + .value_parser(ShortcutValueParser::new(PRESERVABLE_ATTRIBUTES)) .num_args(0..) .require_equals(true) .value_name("ATTR_LIST") - .overrides_with_all([ - options::ARCHIVE, - options::PRESERVE_DEFAULT_ATTRIBUTES, - options::NO_PRESERVE, - ]) + .default_missing_value(PRESERVE_DEFAULT_VALUES) // -d sets this option // --archive sets this option - .help( - "Preserve the specified attributes (default: mode, ownership (unix only), \ - timestamps), if possible additional attributes: context, links, xattr, all", - ), + .help(translate!("cp-help-preserve")), ) .arg( Arg::new(options::PRESERVE_DEFAULT_ATTRIBUTES) .short('p') .long(options::PRESERVE_DEFAULT_ATTRIBUTES) - .overrides_with_all([options::PRESERVE, options::NO_PRESERVE, options::ARCHIVE]) - .help("same as --preserve=mode,ownership(unix only),timestamps") + .help(translate!("cp-help-preserve-default")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::NO_PRESERVE) .long(options::NO_PRESERVE) + .action(ArgAction::Append) + .use_value_delimiter(true) + .value_parser(ShortcutValueParser::new(PRESERVABLE_ATTRIBUTES)) + .num_args(0..) + .require_equals(true) .value_name("ATTR_LIST") - .overrides_with_all([ - options::PRESERVE_DEFAULT_ATTRIBUTES, - options::PRESERVE, - options::ARCHIVE, - ]) - .help("don't preserve the specified attributes"), + .help(translate!("cp-help-no-preserve")), ) .arg( Arg::new(options::PARENTS) .long(options::PARENTS) .alias(options::PARENT) - .help("use full source file name under DIRECTORY") + .help(translate!("cp-help-parents")) .action(ArgAction::SetTrue), ) .arg( @@ -609,7 +692,7 @@ pub fn uu_app() -> Command { .long(options::NO_DEREFERENCE) .overrides_with(options::DEREFERENCE) // -d sets this option - .help("never follow symbolic links in SOURCE") + .help(translate!("cp-help-no-dereference")) .action(ArgAction::SetTrue), ) .arg( @@ -617,127 +700,115 @@ pub fn uu_app() -> Command { .short('L') .long(options::DEREFERENCE) .overrides_with(options::NO_DEREFERENCE) - .help("always follow symbolic links in SOURCE") + .help(translate!("cp-help-dereference")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::CLI_SYMBOLIC_LINKS) .short('H') - .help("follow command-line symbolic links in SOURCE") + .help(translate!("cp-help-cli-symbolic-links")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::ARCHIVE) .short('a') .long(options::ARCHIVE) - .overrides_with_all([ - options::PRESERVE_DEFAULT_ATTRIBUTES, - options::PRESERVE, - options::NO_PRESERVE, - ]) - .help("Same as -dR --preserve=all") + .help(translate!("cp-help-archive")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::NO_DEREFERENCE_PRESERVE_LINKS) .short('d') - .help("same as --no-dereference --preserve=links") + .help(translate!("cp-help-no-dereference-preserve-links")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::ONE_FILE_SYSTEM) .short('x') .long(options::ONE_FILE_SYSTEM) - .help("stay on this file system") + .help(translate!("cp-help-one-file-system")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::SPARSE) .long(options::SPARSE) .value_name("WHEN") - .value_parser(["never", "auto", "always"]) - .help("control creation of sparse files. See below"), + .value_parser(ShortcutValueParser::new(["never", "auto", "always"])) + .help(translate!("cp-help-sparse")), ) - // TODO: implement the following args .arg( - Arg::new(options::COPY_CONTENTS) - .long(options::COPY_CONTENTS) - .overrides_with(options::ATTRIBUTES_ONLY) - .help("NotImplemented: copy contents of special files when recursive") + Arg::new(options::SELINUX) + .short('Z') + .help(translate!("cp-help-selinux")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::CONTEXT) .long(options::CONTEXT) .value_name("CTX") - .help( - "NotImplemented: set SELinux security context of destination file to \ - default type", - ), + .value_parser(value_parser!(String)) + .help(translate!("cp-help-context")) + .num_args(0..=1) + .require_equals(true) + .default_missing_value(""), ) - // END TODO .arg( // The 'g' short flag is modeled after advcpmv // See this repo: https://github.com/jarun/advcpmv Arg::new(options::PROGRESS_BAR) .long(options::PROGRESS_BAR) .short('g') - .action(clap::ArgAction::SetTrue) - .help( - "Display a progress bar. \n\ - Note: this feature is not supported by GNU coreutils.", - ), + .action(ArgAction::SetTrue) + .help(translate!("cp-help-progress")), + ) + // TODO: implement the following args + .arg( + Arg::new(options::COPY_CONTENTS) + .long(options::COPY_CONTENTS) + .overrides_with(options::ATTRIBUTES_ONLY) + .help(translate!("cp-help-copy-contents")) + .action(ArgAction::SetTrue), ) + // END TODO .arg( Arg::new(options::PATHS) .action(ArgAction::Append) + .num_args(1..) + .required(true) .value_hint(clap::ValueHint::AnyPath) - .value_parser(ValueParser::path_buf()), + .value_parser(ValueParser::os_string()), ) } #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args); - - // The error is parsed here because we do not want version or help being printed to stderr. - if let Err(e) = matches { - let mut app = uu_app(); + let matches = uu_app().get_matches_from_localized(args); - match e.kind() { - clap::error::ErrorKind::DisplayHelp => { - app.print_help()?; - } - clap::error::ErrorKind::DisplayVersion => print!("{}", app.render_version()), - _ => return Err(Box::new(e.with_exit_code(1))), - }; - } else if let Ok(mut matches) = matches { - let options = Options::from_matches(&matches)?; + let options = Options::from_matches(&matches)?; - if options.overwrite == OverwriteMode::NoClobber && options.backup != BackupMode::NoBackup { - return Err(UUsageError::new( - EXIT_ERR, - "options --backup and --no-clobber are mutually exclusive", - )); - } + if options.overwrite == OverwriteMode::NoClobber && options.backup != BackupMode::None { + return Err(UUsageError::new( + EXIT_ERR, + translate!("cp-error-backup-mutually-exclusive"), + )); + } - let paths: Vec = matches - .remove_many::(options::PATHS) - .map(|v| v.collect()) - .unwrap_or_default(); + let paths: Vec = matches + .get_many::(options::PATHS) + .map(|v| v.map(PathBuf::from).collect()) + .unwrap_or_default(); - let (sources, target) = parse_path_args(paths, &options)?; + let (sources, target) = parse_path_args(paths, &options)?; - if let Err(error) = copy(&sources, &target, &options) { - match error { - // Error::NotAllFilesCopied is non-fatal, but the error - // code should still be EXIT_ERR as does GNU cp - Error::NotAllFilesCopied => {} - // Else we caught a fatal bubbled-up error, log it to stderr - _ => show_error!("{}", error), - }; - set_exit_code(EXIT_ERR); + if let Err(error) = copy(&sources, &target, &options) { + match error { + // Error::NotAllFilesCopied is non-fatal, but the error + // code should still be EXIT_ERR as does GNU cp + CpError::NotAllFilesCopied => {} + // Else we caught a fatal bubbled-up error, log it to stderr + _ => show_error!("{error}"), } + set_exit_code(EXIT_ERR); } Ok(()) @@ -848,7 +919,28 @@ impl Attributes { } } - pub fn parse_iter(values: impl Iterator) -> Result + /// Set the field to `Preserve::No { explicit: true }` if the corresponding field + /// in other is set to `Preserve::Yes { .. }`. + pub fn diff(self, other: &Self) -> Self { + fn update_preserve_field(current: Preserve, other: Preserve) -> Preserve { + if matches!(other, Preserve::Yes { .. }) { + Preserve::No { explicit: true } + } else { + current + } + } + Self { + #[cfg(unix)] + ownership: update_preserve_field(self.ownership, other.ownership), + mode: update_preserve_field(self.mode, other.mode), + timestamps: update_preserve_field(self.timestamps, other.timestamps), + context: update_preserve_field(self.context, other.context), + links: update_preserve_field(self.links, other.links), + xattr: update_preserve_field(self.xattr, other.xattr), + } + } + + pub fn parse_iter(values: impl Iterator) -> CopyResult where T: AsRef, { @@ -861,7 +953,7 @@ impl Attributes { /// Tries to match string containing a parameter to preserve with the corresponding entry in the /// Attributes struct. - fn parse_single_string(value: &str) -> Result { + fn parse_single_string(value: &str) -> CopyResult { let value = value.to_lowercase(); if value == "all" { @@ -878,10 +970,9 @@ impl Attributes { "link" | "links" => &mut new.links, "xattr" => &mut new.xattr, _ => { - return Err(Error::InvalidArgument(format!( - "invalid attribute {}", - value.quote() - ))); + return Err(CpError::InvalidArgument( + translate!("cp-error-invalid-attribute", "value" => value.quote()), + )); } }; @@ -897,7 +988,6 @@ impl Options { let not_implemented_opts = vec![ #[cfg(not(any(windows, unix)))] options::ONE_FILE_SYSTEM, - options::CONTEXT, #[cfg(windows)] options::FORCE, ]; @@ -907,18 +997,28 @@ impl Options { && matches.value_source(not_implemented_opt) == Some(clap::parser::ValueSource::CommandLine) { - return Err(Error::NotImplemented(not_implemented_opt.to_string())); + return Err(CpError::NotImplemented(not_implemented_opt.to_string())); } } let recursive = matches.get_flag(options::RECURSIVE) || matches.get_flag(options::ARCHIVE); let backup_mode = match backup_control::determine_backup_mode(matches) { - Err(e) => return Err(Error::Backup(format!("{e}"))), + Err(e) => return Err(CpError::Backup(BackupError(format!("{e}")))), Ok(mode) => mode, }; let update_mode = update_control::determine_update_mode(matches); + if backup_mode != BackupMode::None + && matches + .get_one::(update_control::arguments::OPT_UPDATE) + .is_some_and(|v| v == "none" || v == "none-fail") + { + return Err(CpError::InvalidArgument( + translate!("cp-error-invalid-backup-argument").to_string(), + )); + } + let backup_suffix = backup_control::determine_backup_suffix(matches); let overwrite = OverwriteMode::from_matches(matches); @@ -931,52 +1031,109 @@ impl Options { if let Some(dir) = &target_dir { if !dir.is_dir() { - return Err(Error::NotADirectory(dir.clone())); + return Err(CpError::NotADirectory(dir.clone())); } - }; - - // Parse attributes to preserve - let mut attributes = - if let Some(attribute_strs) = matches.get_many::(options::PRESERVE) { - if attribute_strs.len() == 0 { - Attributes::DEFAULT - } else { - Attributes::parse_iter(attribute_strs)? + } + // cp follows POSIX conventions for overriding options such as "-a", + // "-d", "--preserve", and "--no-preserve". We can use clap's + // override-all behavior to achieve this, but there's a challenge: when + // clap overrides an argument, it removes all traces of it from the + // match. This poses a problem because flags like "-a" expand to "-dR + // --preserve=all", and we only want to override the "--preserve=all" + // part. Additionally, we need to handle multiple occurrences of the + // same flags. To address this, we create an overriding order from the + // matches here. + let mut overriding_order: Vec<(usize, &str, Vec<&String>)> = vec![]; + // We iterate through each overriding option, adding each occurrence of + // the option along with its value and index as a tuple, and push it to + // `overriding_order`. + for option in [ + options::PRESERVE, + options::NO_PRESERVE, + options::ARCHIVE, + options::PRESERVE_DEFAULT_ATTRIBUTES, + options::NO_DEREFERENCE_PRESERVE_LINKS, + ] { + if let (Ok(Some(val)), Some(index)) = ( + matches.try_get_one::(option), + // even though it says in the doc that `index_of` would give us + // the first index of the argument, when it comes to flag it + // gives us the last index where the flag appeared (probably + // because it overrides itself). Since it is a flag and it would + // have same value across the occurrences we just need the last + // index. + matches.index_of(option), + ) { + if *val { + overriding_order.push((index, option, vec![])); } - } else if matches.get_flag(options::ARCHIVE) { - // --archive is used. Same as --preserve=all - Attributes::ALL - } else if matches.get_flag(options::NO_DEREFERENCE_PRESERVE_LINKS) { - Attributes::LINKS - } else if matches.get_flag(options::PRESERVE_DEFAULT_ATTRIBUTES) { - Attributes::DEFAULT - } else { - Attributes::NONE - }; + } else if let (Some(occurrences), Some(mut indices)) = ( + matches.get_occurrences::(option), + matches.indices_of(option), + ) { + occurrences.for_each(|val| { + if let Some(index) = indices.next() { + let val = val.collect::>(); + // As mentioned in the documentation of the indices_of + // function, it provides the indices of the individual + // values. Therefore, to get the index of the first + // value of the next occurrence in the next iteration, + // we need to advance the indices iterator by the length + // of the current occurrence's values. + for _ in 1..val.len() { + indices.next(); + } + overriding_order.push((index, option, val)); + } + }); + } + } + overriding_order.sort_by(|a, b| a.0.cmp(&b.0)); + + let mut attributes = Attributes::NONE; - // handling no-preserve options and adjusting the attributes - if let Some(attribute_strs) = matches.get_many::(options::NO_PRESERVE) { - if attribute_strs.len() > 0 { - let no_preserve_attributes = Attributes::parse_iter(attribute_strs)?; - if matches!(no_preserve_attributes.links, Preserve::Yes { .. }) { - attributes.links = Preserve::No { explicit: true }; - } else if matches!(no_preserve_attributes.mode, Preserve::Yes { .. }) { - attributes.mode = Preserve::No { explicit: true }; + // Iterate through the `overriding_order` and adjust the attributes accordingly. + for (_, option, val) in overriding_order { + match option { + options::ARCHIVE => { + attributes = Attributes::ALL; } + options::PRESERVE_DEFAULT_ATTRIBUTES => { + attributes = attributes.union(&Attributes::DEFAULT); + } + options::NO_DEREFERENCE_PRESERVE_LINKS => { + attributes = attributes.union(&Attributes::LINKS); + } + options::PRESERVE => { + attributes = attributes.union(&Attributes::parse_iter(val.into_iter())?); + } + options::NO_PRESERVE => { + if !val.is_empty() { + attributes = attributes.diff(&Attributes::parse_iter(val.into_iter())?); + } + } + _ => (), } } - #[cfg(not(feature = "feat_selinux"))] + #[cfg(not(feature = "selinux"))] if let Preserve::Yes { required } = attributes.context { - let selinux_disabled_error = - Error::Error("SELinux was not enabled during the compile time!".to_string()); + let selinux_disabled_error = CpError::Error(translate!("cp-error-selinux-not-enabled")); if required { return Err(selinux_disabled_error); - } else { - show_error_if_needed(&selinux_disabled_error); } + show_error_if_needed(&selinux_disabled_error); } + // Extract the SELinux related flags and options + let set_selinux_context = matches.get_flag(options::SELINUX); + + let context = if matches.contains_id(options::CONTEXT) { + matches.get_one::(options::CONTEXT).cloned() + } else { + None + }; + let options = Self { attributes_only: matches.get_flag(options::ATTRIBUTES_ONLY), copy_contents: matches.get_flag(options::COPY_CONTENTS), @@ -986,7 +1143,9 @@ impl Options { dereference: !(matches.get_flag(options::NO_DEREFERENCE) || matches.get_flag(options::NO_DEREFERENCE_PRESERVE_LINKS) || matches.get_flag(options::ARCHIVE) - || recursive) + // cp normally follows the link only when not copying recursively or when + // --link (-l) is used + || (recursive && CopyMode::from_matches(matches)!= CopyMode::Link )) || matches.get_flag(options::DEREFERENCE), one_file_system: matches.get_flag(options::ONE_FILE_SYSTEM), parents: matches.get_flag(options::PARENTS), @@ -1001,25 +1160,13 @@ impl Options { "auto" => ReflinkMode::Auto, "never" => ReflinkMode::Never, value => { - return Err(Error::InvalidArgument(format!( - "invalid argument {} for \'reflink\'", - value.quote() - ))); + return Err(CpError::InvalidArgument( + translate!("cp-error-invalid-argument", "arg" => value.quote(), "option" => "reflink"), + )); } } } else { - #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] - { - ReflinkMode::Auto - } - #[cfg(not(any( - target_os = "linux", - target_os = "android", - target_os = "macos" - )))] - { - ReflinkMode::Never - } + ReflinkMode::default() } }, sparse_mode: { @@ -1029,9 +1176,9 @@ impl Options { "auto" => SparseMode::Auto, "never" => SparseMode::Never, _ => { - return Err(Error::InvalidArgument(format!( - "invalid argument {val} for \'sparse\'" - ))); + return Err(CpError::InvalidArgument( + translate!("cp-error-invalid-argument", "arg" => val, "option" => "sparse"), + )); } } } else { @@ -1046,6 +1193,8 @@ impl Options { recursive, target_dir, progress_bar: matches.get_flag(options::PROGRESS_BAR), + set_selinux_context: set_selinux_context || context.is_some(), + context, }; Ok(options) @@ -1065,10 +1214,13 @@ impl Options { #[cfg(unix)] fn preserve_mode(&self) -> (bool, bool) { match self.attributes.mode { - Preserve::No { explicit } => match explicit { - true => (false, true), - false => (false, false), - }, + Preserve::No { explicit } => { + if explicit { + (false, true) + } else { + (false, false) + } + } Preserve::Yes { .. } => (true, false), } } @@ -1080,7 +1232,7 @@ impl Options { } impl TargetType { - /// Return TargetType required for `target`. + /// Return [`TargetType`] required for `target`. /// /// Treat target as a dir if we have multiple sources or the target /// exists and already is a directory @@ -1100,16 +1252,20 @@ fn parse_path_args( ) -> CopyResult<(Vec, PathBuf)> { if paths.is_empty() { // No files specified - return Err("missing file operand".into()); + return Err(translate!("cp-error-missing-file-operand").into()); } else if paths.len() == 1 && options.target_dir.is_none() { // Only one file specified - return Err(format!("missing destination file operand after {:?}", paths[0]).into()); + return Err(translate!("cp-error-missing-destination-operand", + "source" => paths[0].quote()) + .into()); } // Return an error if the user requested to copy more than one // file source to a file target if options.no_target_dir && options.target_dir.is_none() && paths.len() > 2 { - return Err(format!("extra operand {:?}", paths[2]).into()); + return Err(translate!("cp-error-extra-operand", + "operand" => paths[2].quote()) + .into()); } let target = match options.target_dir { @@ -1126,6 +1282,9 @@ fn parse_path_args( }; if options.strip_trailing_slashes { + // clippy::assigning_clones added with Rust 1.78 + // Rust version = 1.76 on OpenBSD stable/7.5 + #[cfg_attr(not(target_os = "openbsd"), allow(clippy::assigning_clones))] for source in &mut paths { *source = source.components().as_path().to_owned(); } @@ -1135,19 +1294,19 @@ fn parse_path_args( } /// When handling errors, we don't always want to show them to the user. This function handles that. -fn show_error_if_needed(error: &Error) { +fn show_error_if_needed(error: &CpError) { match error { // When using --no-clobber, we don't want to show // an error message - Error::NotAllFilesCopied => { + CpError::NotAllFilesCopied => { // Need to return an error code } - Error::Skipped => { + CpError::Skipped(_) => { // touch a b && echo "n"|cp -i a b && echo $? // should return an error from GNU 9.2 } _ => { - show_error!("{}", error); + show_error!("{error}"); } } } @@ -1194,43 +1353,54 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult }; for source in sources { - if seen_sources.contains(source) { - // FIXME: compare sources by the actual file they point to, not their path. (e.g. dir/file == dir/../dir/file in most cases) - show_warning!("source file {} specified more than once", source.quote()); + let normalized_source = normalize_path(source); + if options.backup == BackupMode::None && seen_sources.contains(&normalized_source) { + let file_type = if source.symlink_metadata()?.file_type().is_dir() { + "directory" + } else { + "file" + }; + let msg = translate!("cp-warning-source-specified-more-than-once", "file_type" => file_type, "source" => source.quote()); + show_warning!("{msg}"); } else { let dest = construct_dest_path(source, target, target_type, options) .unwrap_or_else(|_| target.to_path_buf()); - if fs::metadata(&dest).is_ok() && !fs::symlink_metadata(&dest)?.file_type().is_symlink() + if fs::metadata(&dest).is_ok() + && !fs::symlink_metadata(&dest)?.file_type().is_symlink() + // if both `source` and `dest` are symlinks, it should be considered as an overwrite. + || fs::metadata(source).is_ok() + && fs::symlink_metadata(source)?.file_type().is_symlink() + || matches!(options.copy_mode, CopyMode::SymLink) { // There is already a file and it isn't a symlink (managed in a different place) - if copied_destinations.contains(&dest) - && options.backup != BackupMode::NumberedBackup - { + if copied_destinations.contains(&dest) && options.backup != BackupMode::Numbered { // If the target file was already created in this cp call, do not overwrite - return Err(Error::Error(format!( - "will not overwrite just-created '{}' with '{}'", - dest.display(), - source.display() - ))); + return Err(CpError::Error( + translate!("cp-error-will-not-overwrite-just-created", "dest" => dest.quote(), "source" => source.quote()), + )); } } if let Err(error) = copy_source( - &progress_bar, + progress_bar.as_ref(), source, target, target_type, options, &mut symlinked_files, + &copied_destinations, &mut copied_files, ) { show_error_if_needed(&error); - non_fatal_errors = true; + if !matches!(error, CpError::Skipped(false)) { + non_fatal_errors = true; + } + } else { + copied_destinations.insert(dest.clone()); } - copied_destinations.insert(dest.clone()); } - seen_sources.insert(source); + seen_sources.insert(normalized_source); } if let Some(pb) = progress_bar { @@ -1238,7 +1408,7 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult } if non_fatal_errors { - Err(Error::NotAllFilesCopied) + Err(CpError::NotAllFilesCopied) } else { Ok(()) } @@ -1251,21 +1421,25 @@ fn construct_dest_path( options: &Options, ) -> CopyResult { if options.no_target_dir && target.is_dir() { - return Err(format!( - "cannot overwrite directory {} with non-directory", - target.quote() - ) - .into()); + return Err( + translate!("cp-error-cannot-overwrite-directory-with-non-directory", + "dir" => target.quote()) + .into(), + ); } if options.parents && !target.is_dir() { - return Err("with --parents, the destination must be a directory".into()); + return Err(translate!("cp-error-with-parents-dest-must-be-dir").into()); } Ok(match target_type { TargetType::Directory => { let root = if options.parents { - Path::new("") + if source_path.has_root() && cfg!(unix) { + Path::new("/") + } else { + Path::new("") + } } else { source_path.parent().unwrap_or(source_path) }; @@ -1274,18 +1448,19 @@ fn construct_dest_path( TargetType::File => target.to_path_buf(), }) } - +#[allow(clippy::too_many_arguments)] fn copy_source( - progress_bar: &Option, + progress_bar: Option<&ProgressBar>, source: &Path, target: &Path, target_type: TargetType, options: &Options, symlinked_files: &mut HashSet, + copied_destinations: &HashSet, copied_files: &mut HashMap, ) -> CopyResult<()> { let source_path = Path::new(&source); - if source_path.is_dir() { + if source_path.is_dir() && (options.dereference || !source_path.is_symlink()) { // Copy as directory copy_directory( progress_bar, @@ -1293,6 +1468,7 @@ fn copy_source( target, options, symlinked_files, + copied_destinations, copied_files, true, ) @@ -1305,30 +1481,99 @@ fn copy_source( dest.as_path(), options, symlinked_files, + copied_destinations, copied_files, true, ); if options.parents { for (x, y) in aligned_ancestors(source, dest.as_path()) { - copy_attributes(x, y, &options.attributes)?; + if let Ok(src) = canonicalize(x, MissingHandling::Normal, ResolveMode::Physical) { + copy_attributes(&src, y, &options.attributes)?; + } } } res } } +/// If `path` does not have `S_IWUSR` set, returns a tuple of the file's +/// mode in octal (index 0) and human-readable (index 1) formats. +/// +/// If the destination of a copy operation is a file that is not writeable to +/// the owner (bit `S_IWUSR`), extra information needs to be added to the +/// interactive mode prompt: the mode (permissions) of the file in octal and +/// human-readable format. +// TODO +// The destination metadata can be read multiple times in the course of a single execution of `cp`. +// This fix adds yet another metadata read. +// Should this metadata be read once and then reused throughout the execution? +// https://github.com/uutils/coreutils/issues/6658 +fn file_mode_for_interactive_overwrite( + #[cfg_attr(not(unix), allow(unused_variables))] path: &Path, +) -> Option<(String, String)> { + // Retain outer braces to ensure only one branch is included + { + #[cfg(unix)] + { + use libc::{S_IWUSR, mode_t}; + use std::os::unix::prelude::MetadataExt; + + match path.metadata() { + Ok(me) => { + // Cast is necessary on some platforms + let mode: mode_t = me.mode() as mode_t; + + // It looks like this extra information is added to the prompt iff the file's user write bit is 0 + // write permission, owner + if uucore::has!(mode, S_IWUSR) { + None + } else { + // Discard leading digits + let mode_without_leading_digits = mode & 0o7777; + + Some(( + format!("{mode_without_leading_digits:04o}"), + uucore::fs::display_permissions_unix(mode, false), + )) + } + } + // TODO: How should failure to read the metadata be handled? Ignoring for now. + Err(_) => None, + } + } + + #[cfg(not(unix))] + { + None + } + } +} + impl OverwriteMode { - fn verify(&self, path: &Path) -> CopyResult<()> { + fn verify(&self, path: &Path, debug: bool) -> CopyResult<()> { match *self { Self::NoClobber => { - eprintln!("{}: not replacing {}", util_name(), path.quote()); - Err(Error::NotAllFilesCopied) + if debug { + println!("{}", translate!("cp-debug-skipped", "path" => path.quote())); + } + Err(CpError::Skipped(false)) } Self::Interactive(_) => { - if prompt_yes!("overwrite {}?", path.quote()) { + let prompt_yes_result = if let Some((octal, human_readable)) = + file_mode_for_interactive_overwrite(path) + { + let prompt_msg = + translate!("cp-prompt-overwrite-with-mode", "path" => path.quote()); + prompt_yes!("{prompt_msg} {octal} ({human_readable})?") + } else { + let prompt_msg = translate!("cp-prompt-overwrite", "path" => path.quote()); + prompt_yes!("{prompt_msg}") + }; + + if prompt_yes_result { Ok(()) } else { - Err(Error::Skipped) + Err(CpError::Skipped(true)) } } Self::Clobber(_) => Ok(()), @@ -1350,7 +1595,43 @@ fn handle_preserve CopyResult<()>>(p: &Preserve, f: F) -> CopyResult< show_error_if_needed(&error); } } - }; + } + Ok(()) +} + +/// Copies extended attributes (xattrs) from `source` to `dest`, ensuring that `dest` is temporarily +/// user-writable if needed and restoring its original permissions afterward. This avoids "Operation +/// not permitted" errors on read-only files. Returns an error if permission or metadata operations fail, +/// or if xattr copying fails. +#[cfg(all(unix, not(target_os = "android")))] +fn copy_extended_attrs(source: &Path, dest: &Path) -> CopyResult<()> { + let metadata = fs::symlink_metadata(dest)?; + + // Check if the destination file is currently read-only for the user. + let mut perms = metadata.permissions(); + let was_readonly = perms.readonly(); + + // Temporarily grant user write if it was read-only. + if was_readonly { + #[allow(clippy::permissions_set_readonly_false)] + perms.set_readonly(false); + fs::set_permissions(dest, perms)?; + } + + // Perform the xattr copy and capture any potential error, + // so we can restore permissions before returning. + let copy_xattrs_result = copy_xattrs(source, dest); + + // Restore read-only if we changed it. + if was_readonly { + let mut revert_perms = fs::symlink_metadata(dest)?.permissions(); + revert_perms.set_readonly(true); + fs::set_permissions(dest, revert_perms)?; + } + + // If copying xattrs failed, propagate that error now. + copy_xattrs_result?; + Ok(()) } @@ -1361,32 +1642,43 @@ pub(crate) fn copy_attributes( attributes: &Attributes, ) -> CopyResult<()> { let context = &*format!("{} -> {}", source.quote(), dest.quote()); - let source_metadata = fs::symlink_metadata(source).context(context)?; + let source_metadata = + fs::symlink_metadata(source).map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; // Ownership must be changed first to avoid interfering with mode change. #[cfg(unix)] handle_preserve(&attributes.ownership, || -> CopyResult<()> { use std::os::unix::prelude::MetadataExt; - use uucore::perms::wrap_chown; use uucore::perms::Verbosity; use uucore::perms::VerbosityLevel; + use uucore::perms::wrap_chown; let dest_uid = source_metadata.uid(); let dest_gid = source_metadata.gid(); + let meta = &dest + .symlink_metadata() + .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; - wrap_chown( - dest, - &dest.symlink_metadata().context(context)?, - Some(dest_uid), - Some(dest_gid), - false, - Verbosity { - groups_only: false, - level: VerbosityLevel::Normal, - }, - ) - .map_err(Error::Error)?; - + let try_chown = { + |uid| { + wrap_chown( + dest, + meta, + uid, + Some(dest_gid), + false, + Verbosity { + groups_only: false, + level: VerbosityLevel::Silent, + }, + ) + } + }; + // gnu compatibility: cp doesn't report an error if it fails to set the ownership, + // and will fall back to changing only the gid if possible. + if try_chown(Some(dest_uid)).is_err() { + let _ = try_chown(None); + } Ok(()) })?; @@ -1397,12 +1689,13 @@ pub(crate) fn copy_attributes( // do nothing, since every symbolic link has the same // permissions. if !dest.is_symlink() { - fs::set_permissions(dest, source_metadata.permissions()).context(context)?; + fs::set_permissions(dest, source_metadata.permissions()) + .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; // FIXME: Implement this for windows as well #[cfg(feature = "feat_acl")] exacl::getfacl(source, None) .and_then(|acl| exacl::setfacl(&[dest], &acl, None)) - .map_err(|err| Error::Error(err.to_string()))?; + .map_err(|err| CpError::Error(err.to_string()))?; } Ok(()) @@ -1420,37 +1713,29 @@ pub(crate) fn copy_attributes( Ok(()) })?; - #[cfg(feature = "feat_selinux")] + #[cfg(feature = "selinux")] handle_preserve(&attributes.context, || -> CopyResult<()> { - let context = selinux::SecurityContext::of_path(source, false, false).map_err(|e| { - format!( - "failed to get security context of {}: {}", - source.display(), - e - ) - })?; - if let Some(context) = context { - context.set_for_path(dest, false, false).map_err(|e| { - format!( - "failed to set security context for {}: {}", - dest.display(), - e - ) - })?; + // Get the source context and apply it to the destination + if let Ok(context) = selinux::SecurityContext::of_path(source, false, false) { + if let Some(context) = context { + if let Err(e) = context.set_for_path(dest, false, false) { + return Err(CpError::Error( + translate!("cp-error-selinux-set-context", "path" => dest.display(), "error" => e), + )); + } + } + } else { + return Err(CpError::Error( + translate!("cp-error-selinux-get-context", "path" => source.display()), + )); } - Ok(()) })?; handle_preserve(&attributes.xattr, || -> CopyResult<()> { #[cfg(all(unix, not(target_os = "android")))] { - let xattrs = xattr::list(source)?; - for attr in xattrs { - if let Some(attr_value) = xattr::get(source, attr.clone())? { - xattr::set(dest, attr, &attr_value[..])?; - } - } + copy_extended_attrs(source, dest)?; } #[cfg(not(all(unix, not(target_os = "android"))))] { @@ -1475,16 +1760,29 @@ pub(crate) fn copy_attributes( fn symlink_file( source: &Path, dest: &Path, - context: &str, symlinked_files: &mut HashSet, ) -> CopyResult<()> { #[cfg(not(windows))] { - std::os::unix::fs::symlink(source, dest).context(context)?; + std::os::unix::fs::symlink(source, dest).map_err(|e| { + CpError::IoErrContext( + e, + translate!("cp-error-cannot-create-symlink", + "dest" => get_filename(dest).unwrap_or("?").quote(), + "source" => get_filename(source).unwrap_or("?").quote()), + ) + })?; } #[cfg(windows)] { - std::os::windows::fs::symlink_file(source, dest).context(context)?; + std::os::windows::fs::symlink_file(source, dest).map_err(|e| { + CpError::IoErrContext( + e, + translate!("cp-error-cannot-create-symlink", + "dest" => get_filename(dest).unwrap_or("?").quote(), + "source" => get_filename(source).unwrap_or("?").quote()), + ) + })?; } if let Ok(file_info) = FileInformation::from_path(dest, false) { symlinked_files.insert(file_info); @@ -1496,10 +1794,11 @@ fn context_for(src: &Path, dest: &Path) -> String { format!("{} -> {}", src.quote(), dest.quote()) } -/// Implements a simple backup copy for the destination file. +/// Implements a simple backup copy for the destination file . +/// if `is_dest_symlink` flag is set to true dest will be renamed to `backup_path` /// TODO: for the backup, should this function be replaced by `copy_file(...)`? -fn backup_dest(dest: &Path, backup_path: &Path) -> CopyResult { - if dest.is_symlink() { +fn backup_dest(dest: &Path, backup_path: &Path, is_dest_symlink: bool) -> CopyResult { + if is_dest_symlink { fs::rename(dest, backup_path)?; } else { fs::copy(dest, backup_path)?; @@ -1520,11 +1819,44 @@ fn is_forbidden_to_copy_to_same_file( ) -> bool { // TODO To match the behavior of GNU cp, we also need to check // that the file is a regular file. + let source_is_symlink = source.is_symlink(); + let dest_is_symlink = dest.is_symlink(); + // only disable dereference if both source and dest is symlink and dereference flag is disabled let dereference_to_compare = - options.dereference(source_in_command_line) || !source.is_symlink(); - paths_refer_to_same_file(source, dest, dereference_to_compare) - && !(options.force() && options.backup != BackupMode::NoBackup) - && !(dest.is_symlink() && options.backup != BackupMode::NoBackup) + options.dereference(source_in_command_line) || (!source_is_symlink || !dest_is_symlink); + if !paths_refer_to_same_file(source, dest, dereference_to_compare) { + return false; + } + if options.backup != BackupMode::None { + if options.force() && !source_is_symlink { + return false; + } + if source_is_symlink && !options.dereference { + return false; + } + if dest_is_symlink { + return false; + } + if !dest_is_symlink && !source_is_symlink && dest != source { + return false; + } + } + if options.copy_mode == CopyMode::Link { + return false; + } + if options.copy_mode == CopyMode::SymLink && dest_is_symlink { + return false; + } + // If source and dest are both the same symlink but with different names, then allow the copy. + // This can occur, for example, if source and dest are both hardlinks to the same symlink. + if dest_is_symlink + && source_is_symlink + && source.file_name() != dest.file_name() + && !options.dereference + { + return false; + } + true } /// Back up, remove, or leave intact the destination file, depending on the options. @@ -1533,59 +1865,108 @@ fn handle_existing_dest( dest: &Path, options: &Options, source_in_command_line: bool, + copied_files: &HashMap, ) -> CopyResult<()> { // Disallow copying a file to itself, unless `--force` and // `--backup` are both specified. if is_forbidden_to_copy_to_same_file(source, dest, options, source_in_command_line) { - return Err(format!("{} and {} are the same file", source.quote(), dest.quote()).into()); + return Err(translate!("cp-error-same-file", + "source" => source.quote(), + "dest" => dest.quote()) + .into()); + } + + if options.update == UpdateMode::None { + if options.debug { + println!("skipped {}", dest.quote()); + } + return Err(CpError::Skipped(false)); } - options.overwrite.verify(dest)?; + if options.update != UpdateMode::IfOlder { + options.overwrite.verify(dest, options.debug)?; + } + let mut is_dest_removed = false; let backup_path = backup_control::get_backup_path(options.backup, dest, &options.backup_suffix); if let Some(backup_path) = backup_path { if paths_refer_to_same_file(source, &backup_path, true) { - return Err(format!( - "backing up {} might destroy source; {} not copied", - dest.quote(), - source.quote() - ) + return Err(translate!("cp-error-backing-up-destroy-source", "dest" => dest.quote(), "source" => source.quote()) .into()); - } else { - backup_dest(dest, &backup_path)?; } + is_dest_removed = dest.is_symlink(); + backup_dest(dest, &backup_path, is_dest_removed)?; } - match options.overwrite { - // FIXME: print that the file was removed if --verbose is enabled - OverwriteMode::Clobber(ClobberMode::Force) => { - if is_symlink_loop(dest) || fs::metadata(dest)?.permissions().readonly() { - fs::remove_file(dest)?; - } - } - OverwriteMode::Clobber(ClobberMode::RemoveDestination) => { - fs::remove_file(dest)?; - } - OverwriteMode::Clobber(ClobberMode::Standard) => { - // Consider the following files: - // - // * `src/f` - a regular file - // * `src/link` - a hard link to `src/f` - // * `dest/src/f` - a different regular file - // - // In this scenario, if we do `cp -a src/ dest/`, it is - // possible that the order of traversal causes `src/link` - // to get copied first (to `dest/src/link`). In that case, - // in order to make sure `dest/src/link` is a hard link to - // `dest/src/f` and `dest/src/f` has the contents of - // `src/f`, we delete the existing file to allow the hard - // linking. - if options.preserve_hard_links() { - fs::remove_file(dest)?; + if !is_dest_removed { + delete_dest_if_needed_and_allowed( + source, + dest, + options, + source_in_command_line, + copied_files, + )?; + } + + Ok(()) +} + +/// Checks if: +/// * `dest` needs to be deleted before the copy operation can proceed +/// * the provided options allow this deletion +/// +/// If so, deletes `dest`. +fn delete_dest_if_needed_and_allowed( + source: &Path, + dest: &Path, + options: &Options, + source_in_command_line: bool, + copied_files: &HashMap, +) -> CopyResult<()> { + let delete_dest = match options.overwrite { + OverwriteMode::Clobber(cl) | OverwriteMode::Interactive(cl) => { + match cl { + // FIXME: print that the file was removed if --verbose is enabled + ClobberMode::Force => { + // TODO + // Using `readonly` here to check if `dest` needs to be deleted is not correct: + // "On Unix-based platforms this checks if any of the owner, group or others write permission bits are set. It does not check if the current user is in the file's assigned group. It also does not check ACLs. Therefore the return value of this function cannot be relied upon to predict whether attempts to read or write the file will actually succeed." + // This results in some copy operations failing, because this necessary deletion is being skipped. + is_symlink_loop(dest) || fs::metadata(dest)?.permissions().readonly() + } + ClobberMode::RemoveDestination => true, + ClobberMode::Standard => { + // Consider the following files: + // + // * `src/f` - a regular file + // * `src/link` - a hard link to `src/f` + // * `dest/src/f` - a different regular file + // + // In this scenario, if we do `cp -a src/ dest/`, it is + // possible that the order of traversal causes `src/link` + // to get copied first (to `dest/src/link`). In that case, + // in order to make sure `dest/src/link` is a hard link to + // `dest/src/f` and `dest/src/f` has the contents of + // `src/f`, we delete the existing file to allow the hard + // linking. + options.preserve_hard_links() && + // only try to remove dest file only if the current source + // is hardlink to a file that is already copied + copied_files.contains_key( + &FileInformation::from_path( + source, + options.dereference(source_in_command_line) + ).map_err(|e| CpError::IoErrContext(e, format!("cannot stat {}", source.quote())))? + ) + } } } - _ => (), + OverwriteMode::NoClobber => false, }; + if delete_dest { + fs::remove_file(dest)?; + } + Ok(()) } @@ -1641,7 +2022,7 @@ fn aligned_ancestors<'a>(source: &'a Path, dest: &'a Path) -> Vec<(&'a Path, &'a fn print_verbose_output( parents: bool, - progress_bar: &Option, + progress_bar: Option<&ProgressBar>, source: &Path, dest: &Path, ) { @@ -1664,7 +2045,10 @@ fn print_paths(parents: bool, source: &Path, dest: &Path) { // a/b -> d/a/b // for (x, y) in aligned_ancestors(source, dest) { - println!("{} -> {}", x.display(), y.display()); + println!( + "{}", + translate!("cp-verbose-created-directory", "source" => x.display(), "dest" => y.display()) + ); } } @@ -1681,23 +2065,20 @@ fn print_paths(parents: bool, source: &Path, dest: &Path) { /// /// * `Ok(())` - The file was copied successfully. /// * `Err(CopyError)` - An error occurred while copying the file. +#[allow(clippy::too_many_arguments)] fn handle_copy_mode( source: &Path, dest: &Path, options: &Options, context: &str, - source_metadata: Metadata, + source_metadata: &Metadata, symlinked_files: &mut HashSet, source_in_command_line: bool, -) -> CopyResult<()> { - let source_file_type = source_metadata.file_type(); - - let source_is_symlink = source_file_type.is_symlink(); - - #[cfg(unix)] - let source_is_fifo = source_file_type.is_fifo(); - #[cfg(not(unix))] - let source_is_fifo = false; + source_is_fifo: bool, + source_is_socket: bool, + #[cfg(unix)] source_is_stream: bool, +) -> CopyResult { + let source_is_symlink = source_metadata.is_symlink(); match options.copy_mode { CopyMode::Link => { @@ -1705,7 +2086,7 @@ fn handle_copy_mode( let backup_path = backup_control::get_backup_path(options.backup, dest, &options.backup_suffix); if let Some(backup_path) = backup_path { - backup_dest(dest, &backup_path)?; + backup_dest(dest, &backup_path, dest.is_symlink())?; fs::remove_file(dest)?; } if options.overwrite == OverwriteMode::Clobber(ClobberMode::Force) { @@ -1719,7 +2100,12 @@ fn handle_copy_mode( } else { fs::hard_link(source, dest) } - .context(context)?; + .map_err(|e| { + CpError::IoErrContext( + e, + translate!("cp-error-cannot-create-hard-link", "dest" => get_filename(dest).unwrap_or("?").quote(), "source" => get_filename(source).unwrap_or("?").quote()) + ) + })?; } CopyMode::Copy => { copy_helper( @@ -1729,19 +2115,22 @@ fn handle_copy_mode( context, source_is_symlink, source_is_fifo, + source_is_socket, symlinked_files, + #[cfg(unix)] + source_is_stream, )?; } CopyMode::SymLink => { if dest.exists() && options.overwrite == OverwriteMode::Clobber(ClobberMode::Force) { fs::remove_file(dest)?; } - symlink_file(source, dest, context, symlinked_files)?; + symlink_file(source, dest, symlinked_files)?; } CopyMode::Update => { if dest.exists() { match options.update { - update_control::UpdateMode::ReplaceAll => { + UpdateMode::All => { copy_helper( source, dest, @@ -1749,34 +2138,47 @@ fn handle_copy_mode( context, source_is_symlink, source_is_fifo, + source_is_socket, symlinked_files, + #[cfg(unix)] + source_is_stream, )?; } - update_control::UpdateMode::ReplaceNone => { + UpdateMode::None => { if options.debug { println!("skipped {}", dest.quote()); } - return Ok(()); + return Ok(PerformedAction::Skipped); + } + UpdateMode::NoneFail => { + return Err(CpError::Error( + translate!("cp-error-not-replacing", "file" => dest.quote()), + )); } - update_control::UpdateMode::ReplaceIfOlder => { + UpdateMode::IfOlder => { let dest_metadata = fs::symlink_metadata(dest)?; let src_time = source_metadata.modified()?; let dest_time = dest_metadata.modified()?; if src_time <= dest_time { - return Ok(()); - } else { - copy_helper( - source, - dest, - options, - context, - source_is_symlink, - source_is_fifo, - symlinked_files, - )?; + return Ok(PerformedAction::Skipped); } + + options.overwrite.verify(dest, options.debug)?; + + copy_helper( + source, + dest, + options, + context, + source_is_symlink, + source_is_fifo, + source_is_socket, + symlinked_files, + #[cfg(unix)] + source_is_stream, + )?; } } } else { @@ -1787,7 +2189,10 @@ fn handle_copy_mode( context, source_is_symlink, source_is_fifo, + source_is_socket, symlinked_files, + #[cfg(unix)] + source_is_stream, )?; } } @@ -1799,9 +2204,9 @@ fn handle_copy_mode( .open(dest) .unwrap(); } - }; + } - Ok(()) + Ok(PerformedAction::Copied) } /// Calculates the permissions for the destination file in a copy operation. @@ -1813,7 +2218,8 @@ fn handle_copy_mode( /// /// * `Ok(Permissions)` - The calculated permissions for the destination file. /// * `Err(CopyError)` - An error occurred while getting the metadata of the destination file. -/// Allow unused variables for Windows (on options) +/// +// Allow unused variables for Windows (on options) #[allow(unused_variables)] fn calculate_dest_permissions( dest: &Path, @@ -1822,7 +2228,10 @@ fn calculate_dest_permissions( context: &str, ) -> CopyResult { if dest.exists() { - Ok(dest.symlink_metadata().context(context)?.permissions()) + Ok(dest + .symlink_metadata() + .map_err(|e| CpError::IoErrContext(e, context.to_owned()))? + .permissions()) } else { #[cfg(unix)] { @@ -1853,37 +2262,39 @@ fn calculate_dest_permissions( /// /// The original permissions of `source` will be copied to `dest` /// after a successful copy. -#[allow(clippy::cognitive_complexity)] +#[allow(clippy::cognitive_complexity, clippy::too_many_arguments)] fn copy_file( - progress_bar: &Option, + progress_bar: Option<&ProgressBar>, source: &Path, dest: &Path, options: &Options, symlinked_files: &mut HashSet, + copied_destinations: &HashSet, copied_files: &mut HashMap, source_in_command_line: bool, ) -> CopyResult<()> { - if (options.update == UpdateMode::ReplaceIfOlder || options.update == UpdateMode::ReplaceNone) - && options.overwrite == OverwriteMode::Interactive(ClobberMode::Standard) - { - // `cp -i --update old new` when `new` exists doesn't copy anything - // and exit with 0 - return Ok(()); - } - + let source_is_symlink = source.is_symlink(); + let dest_is_symlink = dest.is_symlink(); // Fail if dest is a dangling symlink or a symlink this program created previously - if dest.is_symlink() { + if dest_is_symlink { if FileInformation::from_path(dest, false) .map(|info| symlinked_files.contains(&info)) .unwrap_or(false) { - return Err(Error::Error(format!( - "will not copy '{}' through just-created symlink '{}'", - source.display(), - dest.display() - ))); + return Err(CpError::Error( + translate!("cp-error-will-not-copy-through-symlink", "source" => source.quote(), "dest" => dest.quote()), + )); + } + // Fail if cp tries to copy two sources of the same name into a single symlink + // Example: "cp file1 dir1/file1 tmp" where "tmp" is a directory containing a symlink "file1" pointing to a file named "foo". + // foo will contain the contents of "file1" and "dir1/file1" will not be copied over to "tmp/file1" + if copied_destinations.contains(dest) { + return Err(CpError::Error( + translate!("cp-error-will-not-copy-through-symlink", "source" => source.quote(), "dest" => dest.quote()), + )); } - let copy_contents = options.dereference(source_in_command_line) || !source.is_symlink(); + + let copy_contents = options.dereference(source_in_command_line) || !source_is_symlink; if copy_contents && !dest.exists() && !matches!( @@ -1893,22 +2304,23 @@ fn copy_file( && !is_symlink_loop(dest) && std::env::var_os("POSIXLY_CORRECT").is_none() { - return Err(Error::Error(format!( - "not writing through dangling symlink '{}'", - dest.display() - ))); + return Err(CpError::Error( + translate!("cp-error-not-writing-dangling-symlink", "dest" => dest.quote()), + )); } if paths_refer_to_same_file(source, dest, true) && matches!( options.overwrite, OverwriteMode::Clobber(ClobberMode::RemoveDestination) ) + && options.backup == BackupMode::None { fs::remove_file(dest)?; } } if are_hardlinks_to_same_file(source, dest) + && source != dest && matches!( options.overwrite, OverwriteMode::Clobber(ClobberMode::RemoveDestination) @@ -1924,29 +2336,43 @@ fn copy_file( OverwriteMode::Clobber(ClobberMode::RemoveDestination) )) { - if are_hardlinks_to_same_file(source, dest) - && !options.force() - && options.backup == BackupMode::NoBackup - && source != dest - || (source == dest && options.copy_mode == CopyMode::Link) - { - return Ok(()); + if paths_refer_to_same_file(source, dest, true) && options.copy_mode == CopyMode::Link { + if source_is_symlink { + if !dest_is_symlink { + return Ok(()); + } + if !options.dereference { + return Ok(()); + } + } else if options.backup != BackupMode::None && !dest_is_symlink { + if source == dest { + if !options.force() { + return Ok(()); + } + } else { + return Ok(()); + } + } + } + handle_existing_dest(source, dest, options, source_in_command_line, copied_files)?; + if are_hardlinks_to_same_file(source, dest) { + if options.copy_mode == CopyMode::Copy { + return Ok(()); + } + if options.copy_mode == CopyMode::Link && (!source_is_symlink || !dest_is_symlink) { + return Ok(()); + } } - handle_existing_dest(source, dest, options, source_in_command_line)?; } if options.attributes_only - && source.is_symlink() + && source_is_symlink && !matches!( options.overwrite, OverwriteMode::Clobber(ClobberMode::RemoveDestination) ) { - return Err(format!( - "cannot change attribute {}: Source file is a non regular file", - dest.quote() - ) - .into()); + return Err(translate!("cp-error-cannot-change-attribute", "dest" => dest.quote()).into()); } if options.preserve_hard_links() { @@ -1955,15 +2381,16 @@ fn copy_file( // in the destination tree. if let Some(new_source) = copied_files.get( &FileInformation::from_path(source, options.dereference(source_in_command_line)) - .context(format!("cannot stat {}", source.quote()))?, + .map_err(|e| CpError::IoErrContext(e, format!("cannot stat {}", source.quote())))?, ) { - std::fs::hard_link(new_source, dest)?; - return Ok(()); - }; - } + fs::hard_link(new_source, dest)?; - if options.verbose { - print_verbose_output(options.parents, progress_bar, source, dest); + if options.verbose { + print_verbose_output(options.parents, progress_bar, source, dest); + } + + return Ok(()); + } } // Calculate the context upfront before canonicalizing the path @@ -1976,23 +2403,48 @@ fn copy_file( } else { fs::symlink_metadata(source) }; - result.context(context)? + // this is just for gnu tests compatibility + result.map_err(|err| { + if err.to_string().contains("No such file or directory") { + return translate!("cp-error-cannot-stat", "source" => source.quote()); + } + err.to_string() + })? }; let dest_permissions = calculate_dest_permissions(dest, &source_metadata, options, context)?; - handle_copy_mode( + #[cfg(unix)] + let source_is_fifo = source_metadata.file_type().is_fifo(); + #[cfg(unix)] + let source_is_socket = source_metadata.file_type().is_socket(); + #[cfg(not(unix))] + let source_is_fifo = false; + #[cfg(not(unix))] + let source_is_socket = false; + + let source_is_stream = is_stream(&source_metadata); + + let performed_action = handle_copy_mode( source, dest, options, context, - source_metadata, + &source_metadata, symlinked_files, source_in_command_line, + source_is_fifo, + source_is_socket, + #[cfg(unix)] + source_is_stream, )?; + if options.verbose && performed_action != PerformedAction::Skipped { + print_verbose_output(options.parents, progress_bar, source, dest); + } + // TODO: implement something similar to gnu's lchown - if !dest.is_symlink() { + if !dest_is_symlink { // Here, to match GNU semantics, we quietly ignore an error // if a user does not have the correct ownership to modify // the permissions of a file. @@ -2002,7 +2454,32 @@ fn copy_file( fs::set_permissions(dest, dest_permissions).ok(); } - copy_attributes(source, dest, &options.attributes)?; + if options.dereference(source_in_command_line) { + if let Ok(src) = canonicalize(source, MissingHandling::Normal, ResolveMode::Physical) { + if src.exists() { + copy_attributes(&src, dest, &options.attributes)?; + } + } + } else if source_is_stream && !source.exists() { + // Some stream files may not exist after we have copied it, + // like anonymous pipes. Thus, we can't really copy its + // attributes. However, this is already handled in the stream + // copy function (see `copy_stream` under platform/linux.rs). + } else { + copy_attributes(source, dest, &options.attributes)?; + } + + #[cfg(feature = "selinux")] + if options.set_selinux_context && uucore::selinux::is_selinux_enabled() { + // Set the given selinux permissions on the copied file. + if let Err(e) = + uucore::selinux::set_selinux_security_context(dest, options.context.as_ref()) + { + return Err(CpError::Error( + translate!("cp-error-selinux-error", "error" => e), + )); + } + } copied_files.insert( FileInformation::from_path(source, options.dereference(source_in_command_line))?, @@ -2016,6 +2493,19 @@ fn copy_file( Ok(()) } +fn is_stream(metadata: &Metadata) -> bool { + #[cfg(unix)] + { + let file_type = metadata.file_type(); + file_type.is_fifo() || file_type.is_char_device() || file_type.is_block_device() + } + #[cfg(not(unix))] + { + let _ = metadata; + false + } +} + #[cfg(unix)] fn handle_no_preserve_mode(options: &Options, org_mode: u32) -> u32 { let (is_preserve_mode, is_explicit_no_preserve_mode) = options.preserve_mode(); @@ -2027,23 +2517,22 @@ fn handle_no_preserve_mode(options: &Options, org_mode: u32) -> u32 { #[cfg(not(any( target_os = "android", target_os = "macos", - target_os = "macos-12", target_os = "freebsd", target_os = "redox", )))] { const MODE_RW_UGO: u32 = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; const S_IRWXUGO: u32 = S_IRWXU | S_IRWXG | S_IRWXO; - match is_explicit_no_preserve_mode { - true => return MODE_RW_UGO, - false => return org_mode & S_IRWXUGO, + return if is_explicit_no_preserve_mode { + MODE_RW_UGO + } else { + org_mode & S_IRWXUGO }; } #[cfg(any( target_os = "android", target_os = "macos", - target_os = "macos-12", target_os = "freebsd", target_os = "redox", ))] @@ -2051,9 +2540,10 @@ fn handle_no_preserve_mode(options: &Options, org_mode: u32) -> u32 { const MODE_RW_UGO: u32 = (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) as u32; const S_IRWXUGO: u32 = (S_IRWXU | S_IRWXG | S_IRWXO) as u32; - match is_explicit_no_preserve_mode { - true => return MODE_RW_UGO, - false => return org_mode & S_IRWXUGO, + return if is_explicit_no_preserve_mode { + MODE_RW_UGO + } else { + org_mode & S_IRWXUGO }; } } @@ -2063,6 +2553,7 @@ fn handle_no_preserve_mode(options: &Options, org_mode: u32) -> u32 { /// Copy the file from `source` to `dest` either using the normal `fs::copy` or a /// copy-on-write scheme if --reflink is specified and the filesystem supports it. +#[allow(clippy::too_many_arguments)] fn copy_helper( source: &Path, dest: &Path, @@ -2070,7 +2561,9 @@ fn copy_helper( context: &str, source_is_symlink: bool, source_is_fifo: bool, + source_is_socket: bool, symlinked_files: &mut HashSet, + #[cfg(unix)] source_is_stream: bool, ) -> CopyResult<()> { if options.parents { let parent = dest.parent().unwrap_or(dest); @@ -2078,19 +2571,17 @@ fn copy_helper( } if path_ends_with_terminator(dest) && !dest.is_dir() { - return Err(Error::NotADirectory(dest.to_path_buf())); + return Err(CpError::NotADirectory(dest.to_path_buf())); } - if source.as_os_str() == "/dev/null" { - /* workaround a limitation of fs::copy - * https://github.com/rust-lang/rust/issues/79390 - */ - File::create(dest).context(dest.display().to_string())?; + if source_is_socket && options.recursive && !options.copy_contents { + #[cfg(unix)] + copy_socket(dest, options.overwrite, options.debug)?; } else if source_is_fifo && options.recursive && !options.copy_contents { #[cfg(unix)] - copy_fifo(dest, options.overwrite)?; + copy_fifo(dest, options.overwrite, options.debug)?; } else if source_is_symlink { - copy_link(source, dest, symlinked_files)?; + copy_link(source, dest, symlinked_files, options)?; } else { let copy_debug = copy_on_write( source, @@ -2098,8 +2589,8 @@ fn copy_helper( options.reflink_mode, options.sparse_mode, context, - #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] - source_is_fifo, + #[cfg(unix)] + source_is_stream, )?; if !options.attributes_only && options.debug { @@ -2113,17 +2604,24 @@ fn copy_helper( // "Copies" a FIFO by creating a new one. This workaround is because Rust's // built-in fs::copy does not handle FIFOs (see rust-lang/rust/issues/79390). #[cfg(unix)] -fn copy_fifo(dest: &Path, overwrite: OverwriteMode) -> CopyResult<()> { +fn copy_fifo(dest: &Path, overwrite: OverwriteMode, debug: bool) -> CopyResult<()> { if dest.exists() { - overwrite.verify(dest)?; + overwrite.verify(dest, debug)?; fs::remove_file(dest)?; } - let name = CString::new(dest.as_os_str().as_bytes()).unwrap(); - let err = unsafe { mkfifo(name.as_ptr(), 0o666) }; - if err == -1 { - return Err(format!("cannot create fifo {}: File exists", dest.quote()).into()); + make_fifo(dest) + .map_err(|_| translate!("cp-error-cannot-create-fifo", "path" => dest.quote()).into()) +} + +#[cfg(unix)] +fn copy_socket(dest: &Path, overwrite: OverwriteMode, debug: bool) -> CopyResult<()> { + if dest.exists() { + overwrite.verify(dest, debug)?; + fs::remove_file(dest)?; } + + UnixListener::bind(dest)?; Ok(()) } @@ -2131,6 +2629,7 @@ fn copy_link( source: &Path, dest: &Path, symlinked_files: &mut HashSet, + options: &Options, ) -> CopyResult<()> { // Here, we will copy the symlink itself (actually, just recreate it) let link = fs::read_link(source)?; @@ -2139,19 +2638,16 @@ fn copy_link( if dest.is_symlink() || dest.is_file() { fs::remove_file(dest)?; } - symlink_file(&link, dest, &context_for(&link, dest), symlinked_files) + symlink_file(&link, dest, symlinked_files)?; + copy_attributes(source, dest, &options.attributes) } /// Generate an error message if `target` is not the correct `target_type` pub fn verify_target_type(target: &Path, target_type: &TargetType) -> CopyResult<()> { match (target_type, target.is_dir()) { - (&TargetType::Directory, false) => { - Err(format!("target: {} is not a directory", target.quote()).into()) - } - (&TargetType::File, true) => Err(format!( - "cannot overwrite directory {} with non-directory", - target.quote() - ) + (&TargetType::Directory, false) => Err(translate!("cp-error-target-not-directory", "target" => target.quote()) + .into()), + (&TargetType::File, true) => Err(translate!("cp-error-cannot-overwrite-directory-with-non-directory", "dir" => target.quote()) .into()), _ => Ok(()), } @@ -2212,7 +2708,7 @@ fn disk_usage_directory(p: &Path) -> io::Result { #[cfg(test)] mod tests { - use crate::{aligned_ancestors, localize_to_target}; + use crate::{Attributes, Preserve, aligned_ancestors, localize_to_target}; use std::path::Path; #[test] @@ -2234,4 +2730,52 @@ mod tests { ]; assert_eq!(actual, expected); } + #[test] + fn test_diff_attrs() { + assert_eq!( + Attributes::ALL.diff(&Attributes { + context: Preserve::Yes { required: true }, + xattr: Preserve::Yes { required: true }, + ..Attributes::ALL + }), + Attributes { + #[cfg(unix)] + ownership: Preserve::No { explicit: true }, + mode: Preserve::No { explicit: true }, + timestamps: Preserve::No { explicit: true }, + context: Preserve::No { explicit: true }, + links: Preserve::No { explicit: true }, + xattr: Preserve::No { explicit: true } + } + ); + assert_eq!( + Attributes { + context: Preserve::Yes { required: true }, + xattr: Preserve::Yes { required: true }, + ..Attributes::ALL + } + .diff(&Attributes::NONE), + Attributes { + context: Preserve::Yes { required: true }, + xattr: Preserve::Yes { required: true }, + ..Attributes::ALL + } + ); + assert_eq!( + Attributes::NONE.diff(&Attributes { + context: Preserve::Yes { required: true }, + xattr: Preserve::Yes { required: true }, + ..Attributes::ALL + }), + Attributes { + #[cfg(unix)] + ownership: Preserve::No { explicit: true }, + mode: Preserve::No { explicit: true }, + timestamps: Preserve::No { explicit: true }, + context: Preserve::No { explicit: true }, + links: Preserve::No { explicit: true }, + xattr: Preserve::No { explicit: true } + } + ); + } } diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs index 674e66ea575..b1e0db46da6 100644 --- a/src/uu/cp/src/platform/linux.rs +++ b/src/uu/cp/src/platform/linux.rs @@ -2,27 +2,24 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore ficlone reflink ftruncate pwrite fiemap +// spell-checker:ignore ficlone reflink ftruncate pwrite fiemap lseek + +use libc::{SEEK_DATA, SEEK_HOLE}; use std::fs::{File, OpenOptions}; use std::io::Read; -use std::os::unix::fs::OpenOptionsExt; +use std::os::unix::fs::FileExt; +use std::os::unix::fs::MetadataExt; +use std::os::unix::fs::{FileTypeExt, OpenOptionsExt}; use std::os::unix::io::AsRawFd; use std::path::Path; - -use quick_error::ResultExt; - +use uucore::buf_copy; use uucore::mode::get_umask; +use uucore::translate; -use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; - -// From /usr/include/linux/fs.h: -// #define FICLONE _IOW(0x94, 9, int) -// Use a macro as libc::ioctl expects u32 or u64 depending on the arch -macro_rules! FICLONE { - () => { - 0x40049409 - }; -} +use crate::{ + CopyDebug, CopyResult, CpError, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode, + is_stream, +}; /// The fallback behavior for [`clone`] on failed system call. #[derive(Clone, Copy)] @@ -32,6 +29,25 @@ enum CloneFallback { /// Use [`std::fs::copy`]. FSCopy, + + /// Use [`sparse_copy`] + SparseCopy, + + /// Use [`sparse_copy_without_hole`] + SparseCopyWithoutHole, +} + +/// Type of method used for copying files +#[derive(Clone, Copy)] +enum CopyMethod { + /// Do a sparse copy + SparseCopy, + /// Use [`std::fs::copy`]. + FSCopy, + /// Default (can either be [`CopyMethod::SparseCopy`] or [`CopyMethod::FSCopy`]) + Default, + /// Use [`sparse_copy_without_hole`] + SparseCopyWithoutHole, } /// Use the Linux `ioctl_ficlone` API to do a copy-on-write clone. @@ -46,24 +62,126 @@ where let dst_file = File::create(&dest)?; let src_fd = src_file.as_raw_fd(); let dst_fd = dst_file.as_raw_fd(); - let result = unsafe { libc::ioctl(dst_fd, FICLONE!(), src_fd) }; + // Using .try_into().unwrap() is required as glibc, musl & android all have different type for ioctl() + #[allow(clippy::unnecessary_fallible_conversions)] + let result = unsafe { + libc::ioctl( + dst_fd, + linux_raw_sys::ioctl::FICLONE.try_into().unwrap(), + src_fd, + ) + }; if result == 0 { return Ok(()); } match fallback { CloneFallback::Error => Err(std::io::Error::last_os_error()), CloneFallback::FSCopy => std::fs::copy(source, dest).map(|_| ()), + CloneFallback::SparseCopy => sparse_copy(source, dest), + CloneFallback::SparseCopyWithoutHole => sparse_copy_without_hole(source, dest), + } +} + +/// Checks whether a file contains any non null bytes i.e. any byte != 0x0 +/// This function returns a tuple of (bool, u64, u64) signifying a tuple of (whether a file has +/// data, its size, no of blocks it has allocated in disk) +#[cfg(any(target_os = "linux", target_os = "android"))] +fn check_for_data(source: &Path) -> Result<(bool, u64, u64), std::io::Error> { + let mut src_file = File::open(source)?; + let metadata = src_file.metadata()?; + + let size = metadata.size(); + let blocks = metadata.blocks(); + // checks edge case of virtual files in /proc which have a size of zero but contains data + if size == 0 { + let mut buf: Vec = vec![0; metadata.blksize() as usize]; // Directly use metadata.blksize() + let _ = src_file.read(&mut buf)?; + return Ok((buf.iter().any(|&x| x != 0x0), size, 0)); + } + + let src_fd = src_file.as_raw_fd(); + + let result = unsafe { libc::lseek(src_fd, 0, SEEK_DATA) }; + + match result { + -1 => Ok((false, size, blocks)), // No data found or end of file + _ if result >= 0 => Ok((true, size, blocks)), // Data found + _ => Err(std::io::Error::last_os_error()), } } +#[cfg(any(target_os = "linux", target_os = "android"))] +/// Checks whether a file is sparse i.e. it contains holes, uses the crude heuristic blocks < size / 512 +/// Reference:`` +fn check_sparse_detection(source: &Path) -> Result { + let src_file = File::open(source)?; + let metadata = src_file.metadata()?; + let size = metadata.size(); + let blocks = metadata.blocks(); + + if blocks < size / 512 { + return Ok(true); + } + Ok(false) +} + +/// Optimized [`sparse_copy`] doesn't create holes for large sequences of zeros in non `sparse_files` +/// Used when `--sparse=auto` +#[cfg(any(target_os = "linux", target_os = "android"))] +fn sparse_copy_without_hole

(source: P, dest: P) -> std::io::Result<()> +where + P: AsRef, +{ + let src_file = File::open(source)?; + let dst_file = File::create(dest)?; + let dst_fd = dst_file.as_raw_fd(); + + let size = src_file.metadata()?.size(); + if unsafe { libc::ftruncate(dst_fd, size.try_into().unwrap()) } < 0 { + return Err(std::io::Error::last_os_error()); + } + let src_fd = src_file.as_raw_fd(); + let mut current_offset: isize = 0; + // Maximize the data read at once to 16 MiB to avoid memory hogging with large files + // 16 MiB chunks should saturate an SSD + let step = std::cmp::min(size, 16 * 1024 * 1024) as usize; + let mut buf: Vec = vec![0x0; step]; + loop { + let result = unsafe { libc::lseek(src_fd, current_offset.try_into().unwrap(), SEEK_DATA) } + .try_into() + .unwrap(); + + current_offset = result; + let hole: isize = + unsafe { libc::lseek(src_fd, current_offset.try_into().unwrap(), SEEK_HOLE) } + .try_into() + .unwrap(); + if result == -1 || hole == -1 { + break; + } + if result <= -2 || hole <= -2 { + return Err(std::io::Error::last_os_error()); + } + let len: isize = hole - current_offset; + // Read and write data in chunks of `step` while reusing the same buffer + for i in (0..len).step_by(step) { + // Ensure we don't read past the end of the file or the start of the next hole + let read_len = std::cmp::min((len - i) as usize, step); + let buf = &mut buf[..read_len]; + src_file.read_exact_at(buf, (current_offset + i) as u64)?; + dst_file.write_all_at(buf, (current_offset + i) as u64)?; + } + current_offset = hole; + } + Ok(()) +} /// Perform a sparse copy from one file to another. +/// Creates a holes for large sequences of zeros in `non_sparse_files`, used for `--sparse=always` #[cfg(any(target_os = "linux", target_os = "android"))] fn sparse_copy

(source: P, dest: P) -> std::io::Result<()> where P: AsRef, { - use std::os::unix::prelude::MetadataExt; - let mut src_file = File::open(source)?; let dst_file = File::create(dest)?; let dst_fd = dst_file.as_raw_fd(); @@ -82,23 +200,29 @@ where // https://www.kernel.org/doc/html/latest/filesystems/fiemap.html while current_offset < size { let this_read = src_file.read(&mut buf)?; + let buf = &buf[..this_read]; if buf.iter().any(|&x| x != 0) { - unsafe { - libc::pwrite( - dst_fd, - buf.as_ptr() as *const libc::c_void, - this_read, - current_offset.try_into().unwrap(), - ) - }; + dst_file.write_all_at(buf, current_offset.try_into().unwrap())?; } current_offset += this_read; } Ok(()) } -/// Copy the contents of the given source FIFO to the given file. -fn copy_fifo_contents

(source: P, dest: P) -> std::io::Result +#[cfg(any(target_os = "linux", target_os = "android"))] +/// Checks whether an existing destination is a fifo +fn check_dest_is_fifo(dest: &Path) -> bool { + // If our destination file exists and its a fifo , we do a standard copy . + let file_type = std::fs::metadata(dest); + match file_type { + Ok(f) => f.file_type().is_fifo(), + + _ => false, + } +} + +/// Copy the contents of a stream from `source` to `dest`. +fn copy_stream

(source: P, dest: P) -> std::io::Result where P: AsRef, { @@ -127,59 +251,145 @@ where .write(true) .mode(mode) .open(&dest)?; - let num_bytes_copied = std::io::copy(&mut src_file, &mut dst_file)?; - dst_file.set_permissions(src_file.metadata()?.permissions())?; + + let dest_is_stream = is_stream(&dst_file.metadata()?); + if !dest_is_stream { + // `copy_stream` doesn't clear the dest file, if dest is not a stream, we should clear it manually. + dst_file.set_len(0)?; + } + + let num_bytes_copied = buf_copy::copy_stream(&mut src_file, &mut dst_file) + .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other))?; + Ok(num_bytes_copied) } /// Copies `source` to `dest` using copy-on-write if possible. -/// -/// The `source_is_fifo` flag must be set to `true` if and only if -/// `source` is a FIFO (also known as a named pipe). In this case, -/// copy-on-write is not possible, so we copy the contents using -/// [`std::io::copy`]. pub(crate) fn copy_on_write( source: &Path, dest: &Path, reflink_mode: ReflinkMode, sparse_mode: SparseMode, context: &str, - source_is_fifo: bool, + source_is_stream: bool, ) -> CopyResult { let mut copy_debug = CopyDebug { offload: OffloadReflinkDebug::Unknown, reflink: OffloadReflinkDebug::Unsupported, sparse_detection: SparseDebug::No, }; - let result = match (reflink_mode, sparse_mode) { (ReflinkMode::Never, SparseMode::Always) => { copy_debug.sparse_detection = SparseDebug::Zeros; - copy_debug.offload = OffloadReflinkDebug::Avoided; + // Default SparseDebug val for SparseMode::Always copy_debug.reflink = OffloadReflinkDebug::No; - sparse_copy(source, dest) + if source_is_stream { + copy_debug.offload = OffloadReflinkDebug::Avoided; + copy_stream(source, dest).map(|_| ()) + } else { + let mut copy_method = CopyMethod::Default; + let result = handle_reflink_never_sparse_always(source, dest); + if let Ok((debug, method)) = result { + copy_debug = debug; + copy_method = method; + } + + match copy_method { + CopyMethod::FSCopy => std::fs::copy(source, dest).map(|_| ()), + _ => sparse_copy(source, dest), + } + } } - (ReflinkMode::Never, _) => { - copy_debug.sparse_detection = SparseDebug::No; + (ReflinkMode::Never, SparseMode::Never) => { copy_debug.reflink = OffloadReflinkDebug::No; - std::fs::copy(source, dest).map(|_| ()) + + if source_is_stream { + copy_debug.offload = OffloadReflinkDebug::Avoided; + copy_stream(source, dest).map(|_| ()) + } else { + let result = handle_reflink_never_sparse_never(source); + if let Ok(debug) = result { + copy_debug = debug; + } + std::fs::copy(source, dest).map(|_| ()) + } + } + (ReflinkMode::Never, SparseMode::Auto) => { + copy_debug.reflink = OffloadReflinkDebug::No; + + if source_is_stream { + copy_debug.offload = OffloadReflinkDebug::Avoided; + copy_stream(source, dest).map(|_| ()) + } else { + let mut copy_method = CopyMethod::Default; + let result = handle_reflink_never_sparse_auto(source, dest); + if let Ok((debug, method)) = result { + copy_debug = debug; + copy_method = method; + } + + match copy_method { + CopyMethod::SparseCopyWithoutHole => sparse_copy_without_hole(source, dest), + _ => std::fs::copy(source, dest).map(|_| ()), + } + } } (ReflinkMode::Auto, SparseMode::Always) => { - copy_debug.offload = OffloadReflinkDebug::Avoided; - copy_debug.sparse_detection = SparseDebug::Zeros; - copy_debug.reflink = OffloadReflinkDebug::Unsupported; - sparse_copy(source, dest) + copy_debug.sparse_detection = SparseDebug::Zeros; // Default SparseDebug val for + // SparseMode::Always + if source_is_stream { + copy_debug.offload = OffloadReflinkDebug::Avoided; + copy_stream(source, dest).map(|_| ()) + } else { + let mut copy_method = CopyMethod::Default; + let result = handle_reflink_auto_sparse_always(source, dest); + if let Ok((debug, method)) = result { + copy_debug = debug; + copy_method = method; + } + + match copy_method { + CopyMethod::FSCopy => clone(source, dest, CloneFallback::FSCopy), + _ => clone(source, dest, CloneFallback::SparseCopy), + } + } } - (ReflinkMode::Auto, _) => { - copy_debug.sparse_detection = SparseDebug::No; - copy_debug.reflink = OffloadReflinkDebug::Unsupported; - if source_is_fifo { - copy_fifo_contents(source, dest).map(|_| ()) + (ReflinkMode::Auto, SparseMode::Never) => { + copy_debug.reflink = OffloadReflinkDebug::No; + if source_is_stream { + copy_debug.offload = OffloadReflinkDebug::Avoided; + copy_stream(source, dest).map(|_| ()) } else { + let result = handle_reflink_auto_sparse_never(source); + if let Ok(debug) = result { + copy_debug = debug; + } + clone(source, dest, CloneFallback::FSCopy) } } + (ReflinkMode::Auto, SparseMode::Auto) => { + if source_is_stream { + copy_debug.offload = OffloadReflinkDebug::Unsupported; + copy_stream(source, dest).map(|_| ()) + } else { + let mut copy_method = CopyMethod::Default; + let result = handle_reflink_auto_sparse_auto(source, dest); + if let Ok((debug, method)) = result { + copy_debug = debug; + copy_method = method; + } + + match copy_method { + CopyMethod::SparseCopyWithoutHole => { + clone(source, dest, CloneFallback::SparseCopyWithoutHole) + } + _ => clone(source, dest, CloneFallback::FSCopy), + } + } + } + (ReflinkMode::Always, SparseMode::Auto) => { copy_debug.sparse_detection = SparseDebug::No; copy_debug.reflink = OffloadReflinkDebug::Yes; @@ -187,9 +397,217 @@ pub(crate) fn copy_on_write( clone(source, dest, CloneFallback::Error) } (ReflinkMode::Always, _) => { - return Err("`--reflink=always` can be used only with --sparse=auto".into()) + return Err(translate!("cp-error-reflink-always-sparse-auto").into()); + } + }; + result.map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; + Ok(copy_debug) +} + +/// Handles debug results when flags are "--reflink=auto" and "--sparse=always" and specifies what +/// type of copy should be used +fn handle_reflink_auto_sparse_always( + source: &Path, + dest: &Path, +) -> Result<(CopyDebug, CopyMethod), std::io::Error> { + let mut copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unknown, + reflink: OffloadReflinkDebug::Unsupported, + sparse_detection: SparseDebug::Zeros, + }; + let mut copy_method = CopyMethod::Default; + let (data_flag, size, blocks) = check_for_data(source)?; + let sparse_flag = check_sparse_detection(source)?; + + if data_flag || size < 512 { + copy_debug.offload = OffloadReflinkDebug::Avoided; + } + match (sparse_flag, data_flag, blocks) { + (true, true, 0) => { + // Handling funny files with 0 block allocation but has data + // in it + copy_method = CopyMethod::FSCopy; + copy_debug.sparse_detection = SparseDebug::SeekHoleZeros; } + (false, true, 0) => copy_method = CopyMethod::FSCopy, + + (true, false, 0) => copy_debug.sparse_detection = SparseDebug::SeekHole, + (true, true, _) => copy_debug.sparse_detection = SparseDebug::SeekHoleZeros, + + (true, false, _) => copy_debug.sparse_detection = SparseDebug::SeekHole, + + (_, _, _) => (), + } + if check_dest_is_fifo(dest) { + copy_method = CopyMethod::FSCopy; + } + Ok((copy_debug, copy_method)) +} + +/// Handles debug results when flags are "--reflink=auto" and "--sparse=auto" and specifies what +/// type of copy should be used +fn handle_reflink_never_sparse_never(source: &Path) -> Result { + let mut copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unknown, + reflink: OffloadReflinkDebug::No, + sparse_detection: SparseDebug::No, + }; + let (data_flag, size, _blocks) = check_for_data(source)?; + let sparse_flag = check_sparse_detection(source)?; + + if sparse_flag { + copy_debug.sparse_detection = SparseDebug::SeekHole; + } + + if data_flag || size < 512 { + copy_debug.offload = OffloadReflinkDebug::Avoided; + } + Ok(copy_debug) +} + +/// Handles debug results when flags are "--reflink=auto" and "--sparse=never", files will be copied +/// through cloning them with fallback switching to [`std::fs::copy`] +fn handle_reflink_auto_sparse_never(source: &Path) -> Result { + let mut copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unknown, + reflink: OffloadReflinkDebug::No, + sparse_detection: SparseDebug::No, }; - result.context(context)?; + + let (data_flag, size, _blocks) = check_for_data(source)?; + let sparse_flag = check_sparse_detection(source)?; + + if sparse_flag { + copy_debug.sparse_detection = SparseDebug::SeekHole; + } + + if data_flag || size < 512 { + copy_debug.offload = OffloadReflinkDebug::Avoided; + } Ok(copy_debug) } + +/// Handles debug results when flags are "--reflink=auto" and "--sparse=auto" and specifies what +/// type of copy should be used +fn handle_reflink_auto_sparse_auto( + source: &Path, + dest: &Path, +) -> Result<(CopyDebug, CopyMethod), std::io::Error> { + let mut copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unknown, + reflink: OffloadReflinkDebug::Unsupported, + sparse_detection: SparseDebug::No, + }; + + let mut copy_method = CopyMethod::Default; + let (data_flag, size, blocks) = check_for_data(source)?; + let sparse_flag = check_sparse_detection(source)?; + + if (data_flag && size != 0) || (size > 0 && size < 512) { + copy_debug.offload = OffloadReflinkDebug::Yes; + } + + if data_flag && size == 0 { + // Handling /proc/ files + copy_debug.offload = OffloadReflinkDebug::Unsupported; + } + if sparse_flag { + if blocks == 0 && data_flag { + // Handling other "virtual" files + copy_debug.offload = OffloadReflinkDebug::Unsupported; + + copy_method = CopyMethod::FSCopy; // Doing a standard copy for the virtual files + } else { + copy_method = CopyMethod::SparseCopyWithoutHole; + } // Since sparse_flag is true, sparse_detection shall be SeekHole for any non virtual + // regular sparse file and the file will be sparsely copied + copy_debug.sparse_detection = SparseDebug::SeekHole; + } + + if check_dest_is_fifo(dest) { + copy_method = CopyMethod::FSCopy; + } + Ok((copy_debug, copy_method)) +} + +/// Handles debug results when flags are "--reflink=never" and "--sparse=auto" and specifies what +/// type of copy should be used +fn handle_reflink_never_sparse_auto( + source: &Path, + dest: &Path, +) -> Result<(CopyDebug, CopyMethod), std::io::Error> { + let mut copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unknown, + reflink: OffloadReflinkDebug::No, + sparse_detection: SparseDebug::No, + }; + + let (data_flag, size, blocks) = check_for_data(source)?; + let sparse_flag = check_sparse_detection(source)?; + + let mut copy_method = CopyMethod::Default; + if data_flag || size < 512 { + copy_debug.offload = OffloadReflinkDebug::Avoided; + } + + if sparse_flag { + if blocks == 0 && data_flag { + copy_method = CopyMethod::FSCopy; // Handles virtual files which have size > 0 but no + // disk allocation + } else { + copy_method = CopyMethod::SparseCopyWithoutHole; // Handles regular sparse-files + } + copy_debug.sparse_detection = SparseDebug::SeekHole; + } + + if check_dest_is_fifo(dest) { + copy_method = CopyMethod::FSCopy; + } + Ok((copy_debug, copy_method)) +} + +/// Handles debug results when flags are "--reflink=never" and "--sparse=always" and specifies what +/// type of copy should be used +fn handle_reflink_never_sparse_always( + source: &Path, + dest: &Path, +) -> Result<(CopyDebug, CopyMethod), std::io::Error> { + let mut copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unknown, + reflink: OffloadReflinkDebug::No, + sparse_detection: SparseDebug::Zeros, + }; + let mut copy_method = CopyMethod::SparseCopy; + + let (data_flag, size, blocks) = check_for_data(source)?; + let sparse_flag = check_sparse_detection(source)?; + + if data_flag || size < 512 { + copy_debug.offload = OffloadReflinkDebug::Avoided; + } + match (sparse_flag, data_flag, blocks) { + (true, true, 0) => { + // Handling funny files with 0 block allocation but has data + // in it, e.g. files in /sys and other virtual files + copy_method = CopyMethod::FSCopy; + copy_debug.sparse_detection = SparseDebug::SeekHoleZeros; + } + (false, true, 0) => copy_method = CopyMethod::FSCopy, // Handling data containing zero sized + // files in /proc + (true, false, 0) => copy_debug.sparse_detection = SparseDebug::SeekHole, // Handles files + // with 0 blocks allocated in disk and + (true, true, _) => copy_debug.sparse_detection = SparseDebug::SeekHoleZeros, // Any + // sparse_files with data in it will display SeekHoleZeros + (true, false, _) => { + copy_debug.offload = OffloadReflinkDebug::Unknown; + copy_debug.sparse_detection = SparseDebug::SeekHole; + } + + (_, _, _) => (), + } + if check_dest_is_fifo(dest) { + copy_method = CopyMethod::FSCopy; + } + + Ok((copy_debug, copy_method)) +} diff --git a/src/uu/cp/src/platform/macos.rs b/src/uu/cp/src/platform/macos.rs index 77bdbbbdb83..226d5d710f0 100644 --- a/src/uu/cp/src/platform/macos.rs +++ b/src/uu/cp/src/platform/macos.rs @@ -4,29 +4,34 @@ // file that was distributed with this source code. // spell-checker:ignore reflink use std::ffi::CString; -use std::fs::{self, File}; -use std::io; +use std::fs::{self, File, OpenOptions}; use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::OpenOptionsExt; use std::path::Path; -use quick_error::ResultExt; +use uucore::buf_copy; +use uucore::translate; -use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; +use uucore::mode::get_umask; + +use crate::{ + CopyDebug, CopyResult, CpError, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode, + is_stream, +}; /// Copies `source` to `dest` using copy-on-write if possible. -/// -/// The `source_is_fifo` flag must be set to `true` if and only if -/// `source` is a FIFO (also known as a named pipe). pub(crate) fn copy_on_write( source: &Path, dest: &Path, reflink_mode: ReflinkMode, sparse_mode: SparseMode, context: &str, - source_is_fifo: bool, + source_is_stream: bool, ) -> CopyResult { if sparse_mode != SparseMode::Auto { - return Err("--sparse is only supported on linux".to_string().into()); + return Err(translate!("cp-error-sparse-not-supported") + .to_string() + .into()); } let mut copy_debug = CopyDebug { offload: OffloadReflinkDebug::Unknown, @@ -64,7 +69,7 @@ pub(crate) fn copy_on_write( // clonefile(2) fails if the destination exists. Remove it and try again. Do not // bother to check if removal worked because we're going to try to clone again. // first lets make sure the dest file is not read only - if fs::metadata(dest).map_or(false, |md| !md.permissions().readonly()) { + if fs::metadata(dest).is_ok_and(|md| !md.permissions().readonly()) { // remove and copy again // TODO: rewrite this to better match linux behavior // linux first opens the source file and destination file then uses the file @@ -81,16 +86,32 @@ pub(crate) fn copy_on_write( // support COW). match reflink_mode { ReflinkMode::Always => { - return Err(format!("failed to clone {source:?} from {dest:?}: {error}").into()) + return Err(translate!("cp-error-failed-to-clone", "source" => source.display(), "dest" => dest.display(), "error" => error) + .into()); } _ => { copy_debug.reflink = OffloadReflinkDebug::Yes; - if source_is_fifo { + if source_is_stream { let mut src_file = File::open(source)?; - let mut dst_file = File::create(dest)?; - io::copy(&mut src_file, &mut dst_file).context(context)? + let mode = 0o622 & !get_umask(); + let mut dst_file = OpenOptions::new() + .create(true) + .write(true) + .mode(mode) + .open(dest)?; + + let dest_is_stream = is_stream(&dst_file.metadata()?); + if !dest_is_stream { + // `copy_stream` doesn't clear the dest file, if dest is not a stream, we should clear it manually. + dst_file.set_len(0)?; + } + + buf_copy::copy_stream(&mut src_file, &mut dst_file) + .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other)) + .map_err(|e| CpError::IoErrContext(e, context.to_owned()))? } else { - fs::copy(source, dest).context(context)? + fs::copy(source, dest) + .map_err(|e| CpError::IoErrContext(e, context.to_owned()))? } } }; diff --git a/src/uu/cp/src/platform/mod.rs b/src/uu/cp/src/platform/mod.rs index c7942706868..2071e928f41 100644 --- a/src/uu/cp/src/platform/mod.rs +++ b/src/uu/cp/src/platform/mod.rs @@ -2,6 +2,18 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. + +#[cfg(all( + unix, + not(any(target_os = "macos", target_os = "linux", target_os = "android")) +))] +mod other_unix; +#[cfg(all( + unix, + not(any(target_os = "macos", target_os = "linux", target_os = "android")) +))] +pub(crate) use self::other_unix::copy_on_write; + #[cfg(target_os = "macos")] mod macos; #[cfg(target_os = "macos")] @@ -12,7 +24,13 @@ mod linux; #[cfg(any(target_os = "linux", target_os = "android"))] pub(crate) use self::linux::copy_on_write; -#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] +#[cfg(not(any( + unix, + any(target_os = "macos", target_os = "linux", target_os = "android") +)))] mod other; -#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] +#[cfg(not(any( + unix, + any(target_os = "macos", target_os = "linux", target_os = "android") +)))] pub(crate) use self::other::copy_on_write; diff --git a/src/uu/cp/src/platform/other.rs b/src/uu/cp/src/platform/other.rs index 7ca1a5ded7c..143c35e185b 100644 --- a/src/uu/cp/src/platform/other.rs +++ b/src/uu/cp/src/platform/other.rs @@ -5,10 +5,11 @@ // spell-checker:ignore reflink use std::fs; use std::path::Path; +use uucore::translate; -use quick_error::ResultExt; - -use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; +use crate::{ + CopyDebug, CopyResult, CpError, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode, +}; /// Copies `source` to `dest` for systems without copy-on-write pub(crate) fn copy_on_write( @@ -19,19 +20,21 @@ pub(crate) fn copy_on_write( context: &str, ) -> CopyResult { if reflink_mode != ReflinkMode::Never { - return Err("--reflink is only supported on linux and macOS" + return Err(translate!("cp-error-reflink-not-supported") .to_string() .into()); } if sparse_mode != SparseMode::Auto { - return Err("--sparse is only supported on linux".to_string().into()); + return Err(translate!("cp-error-sparse-not-supported") + .to_string() + .into()); } let copy_debug = CopyDebug { offload: OffloadReflinkDebug::Unsupported, reflink: OffloadReflinkDebug::Unsupported, sparse_detection: SparseDebug::Unsupported, }; - fs::copy(source, dest).context(context)?; + fs::copy(source, dest).map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; Ok(copy_debug) } diff --git a/src/uu/cp/src/platform/other_unix.rs b/src/uu/cp/src/platform/other_unix.rs new file mode 100644 index 00000000000..2db85c56af1 --- /dev/null +++ b/src/uu/cp/src/platform/other_unix.rs @@ -0,0 +1,69 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore reflink +use std::fs::{self, File, OpenOptions}; +use std::os::unix::fs::OpenOptionsExt; +use std::path::Path; + +use uucore::buf_copy; +use uucore::mode::get_umask; +use uucore::translate; + +use crate::{ + CopyDebug, CopyResult, CpError, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode, + is_stream, +}; + +/// Copies `source` to `dest` for systems without copy-on-write +pub(crate) fn copy_on_write( + source: &Path, + dest: &Path, + reflink_mode: ReflinkMode, + sparse_mode: SparseMode, + context: &str, + source_is_stream: bool, +) -> CopyResult { + if reflink_mode != ReflinkMode::Never { + return Err(translate!("cp-error-reflink-not-supported") + .to_string() + .into()); + } + if sparse_mode != SparseMode::Auto { + return Err(translate!("cp-error-sparse-not-supported") + .to_string() + .into()); + } + let copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unsupported, + reflink: OffloadReflinkDebug::Unsupported, + sparse_detection: SparseDebug::Unsupported, + }; + + if source_is_stream { + let mut src_file = File::open(source)?; + let mode = 0o622 & !get_umask(); + let mut dst_file = OpenOptions::new() + .create(true) + .write(true) + .mode(mode) + .open(dest)?; + + let dest_is_stream = is_stream(&dst_file.metadata()?); + if !dest_is_stream { + // `copy_stream` doesn't clear the dest file, if dest is not a stream, we should clear it manually. + dst_file.set_len(0)?; + } + + buf_copy::copy_stream(&mut src_file, &mut dst_file) + .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other)) + .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; + + return Ok(copy_debug); + } + + fs::copy(source, dest).map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; + + Ok(copy_debug) +} diff --git a/src/uu/csplit/Cargo.toml b/src/uu/csplit/Cargo.toml index 8f06524de12..b07c47648db 100644 --- a/src/uu/csplit/Cargo.toml +++ b/src/uu/csplit/Cargo.toml @@ -1,15 +1,18 @@ [package] name = "uu_csplit" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "csplit ~ (uutils) Output pieces of FILE separated by PATTERN(s) to files 'xx00', 'xx01', ..., and output byte counts of each piece to standard output" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/ls" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/csplit.rs" @@ -19,6 +22,7 @@ clap = { workspace = true } thiserror = { workspace = true } regex = { workspace = true } uucore = { workspace = true, features = ["entries", "fs", "format"] } +fluent = { workspace = true } [[bin]] name = "csplit" diff --git a/src/uu/csplit/csplit.md b/src/uu/csplit/csplit.md deleted file mode 100644 index 1d428fc8ee6..00000000000 --- a/src/uu/csplit/csplit.md +++ /dev/null @@ -1,11 +0,0 @@ -# csplit - -``` -csplit [OPTION]... FILE PATTERN... -``` - -Split a file into sections determined by context lines - -## After Help - -Output pieces of FILE separated by PATTERN(s) to files 'xx00', 'xx01', ..., and output byte counts of each piece to standard output. diff --git a/src/uu/csplit/locales/en-US.ftl b/src/uu/csplit/locales/en-US.ftl new file mode 100644 index 00000000000..ba0ce032e49 --- /dev/null +++ b/src/uu/csplit/locales/en-US.ftl @@ -0,0 +1,29 @@ +csplit-about = Split a file into sections determined by context lines +csplit-usage = csplit [OPTION]... FILE PATTERN... +csplit-after-help = Output pieces of FILE separated by PATTERN(s) to files 'xx00', 'xx01', ..., and output byte counts of each piece to standard output. + +# Help messages +csplit-help-suffix-format = use sprintf FORMAT instead of %02d +csplit-help-prefix = use PREFIX instead of 'xx' +csplit-help-keep-files = do not remove output files on errors +csplit-help-suppress-matched = suppress the lines matching PATTERN +csplit-help-digits = use specified number of digits instead of 2 +csplit-help-quiet = do not print counts of output file sizes +csplit-help-elide-empty-files = remove empty output files + +# Error messages +csplit-error-line-out-of-range = { $pattern }: line number out of range +csplit-error-line-out-of-range-on-repetition = { $pattern }: line number out of range on repetition { $repetition } +csplit-error-match-not-found = { $pattern }: match not found +csplit-error-match-not-found-on-repetition = { $pattern }: match not found on repetition { $repetition } +csplit-error-line-number-is-zero = 0: line number must be greater than zero +csplit-error-line-number-smaller-than-previous = line number '{ $current }' is smaller than preceding line number, { $previous } +csplit-error-invalid-pattern = { $pattern }: invalid pattern +csplit-error-invalid-number = invalid number: { $number } +csplit-error-suffix-format-incorrect = incorrect conversion specification in suffix +csplit-error-suffix-format-too-many-percents = too many % conversion specifications in suffix +csplit-error-not-regular-file = { $file } is not a regular file +csplit-warning-line-number-same-as-previous = line number '{ $line_number }' is the same as preceding line number +csplit-stream-not-utf8 = stream did not contain valid UTF-8 +csplit-read-error = read error +csplit-write-split-not-created = trying to write to a split that was not created diff --git a/src/uu/csplit/locales/fr-FR.ftl b/src/uu/csplit/locales/fr-FR.ftl new file mode 100644 index 00000000000..0c2e217ee8c --- /dev/null +++ b/src/uu/csplit/locales/fr-FR.ftl @@ -0,0 +1,29 @@ +csplit-about = Diviser un fichier en sections déterminées par des lignes de contexte +csplit-usage = csplit [OPTION]... FICHIER MOTIF... +csplit-after-help = Sortir les morceaux de FICHIER séparés par MOTIF(S) dans les fichiers 'xx00', 'xx01', ..., et sortir le nombre d'octets de chaque morceau sur la sortie standard. + +# Messages d'aide +csplit-help-suffix-format = utiliser le FORMAT sprintf au lieu de %02d +csplit-help-prefix = utiliser PRÉFIXE au lieu de 'xx' +csplit-help-keep-files = ne pas supprimer les fichiers de sortie en cas d'erreurs +csplit-help-suppress-matched = supprimer les lignes correspondant au MOTIF +csplit-help-digits = utiliser le nombre spécifié de chiffres au lieu de 2 +csplit-help-quiet = ne pas afficher le nombre d'octets des fichiers de sortie +csplit-help-elide-empty-files = supprimer les fichiers de sortie vides + +# Messages d'erreur +csplit-error-line-out-of-range = { $pattern } : numéro de ligne hors limites +csplit-error-line-out-of-range-on-repetition = { $pattern } : numéro de ligne hors limites à la répétition { $repetition } +csplit-error-match-not-found = { $pattern } : correspondance non trouvée +csplit-error-match-not-found-on-repetition = { $pattern } : correspondance non trouvée à la répétition { $repetition } +csplit-error-line-number-is-zero = 0 : le numéro de ligne doit être supérieur à zéro +csplit-error-line-number-smaller-than-previous = le numéro de ligne '{ $current }' est plus petit que le numéro de ligne précédent, { $previous } +csplit-error-invalid-pattern = { $pattern } : motif invalide +csplit-error-invalid-number = nombre invalide : { $number } +csplit-error-suffix-format-incorrect = spécification de conversion incorrecte dans le suffixe +csplit-error-suffix-format-too-many-percents = trop de spécifications de conversion % dans le suffixe +csplit-error-not-regular-file = { $file } n'est pas un fichier régulier +csplit-warning-line-number-same-as-previous = le numéro de ligne '{ $line_number }' est identique au numéro de ligne précédent +csplit-stream-not-utf8 = le flux ne contenait pas d'UTF-8 valide +csplit-read-error = erreur de lecture +csplit-write-split-not-created = tentative d'écriture dans une division qui n'a pas été créée diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index e4d7c243c23..a3e10e2b061 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -6,17 +6,18 @@ #![allow(rustdoc::private_intra_doc_links)] use std::cmp::Ordering; -use std::io::{self, BufReader}; +use std::ffi::OsString; +use std::io::{self, BufReader, ErrorKind}; use std::{ - fs::{remove_file, File}, + fs::{File, remove_file}, io::{BufRead, BufWriter, Write}, }; -use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; +use clap::{Arg, ArgAction, ArgMatches, Command}; use regex::Regex; use uucore::display::Quotable; use uucore::error::{FromIo, UResult}; -use uucore::{crash_if_err, format_usage, help_about, help_section, help_usage}; +use uucore::format_usage; mod csplit_error; mod patterns; @@ -25,9 +26,8 @@ mod split_name; use crate::csplit_error::CsplitError; use crate::split_name::SplitName; -const ABOUT: &str = help_about!("csplit.md"); -const AFTER_HELP: &str = help_section!("after help", "csplit.md"); -const USAGE: &str = help_usage!("csplit.md"); +use uucore::LocalizedCommand; +use uucore::translate; mod options { pub const SUFFIX_FORMAT: &str = "suffix-format"; @@ -43,7 +43,7 @@ mod options { /// Command line options for csplit. pub struct CsplitOptions { - split_name: crate::SplitName, + split_name: SplitName, keep_files: bool, quiet: bool, elide_empty_files: bool, @@ -51,25 +51,51 @@ pub struct CsplitOptions { } impl CsplitOptions { - fn new(matches: &ArgMatches) -> Self { + fn new(matches: &ArgMatches) -> Result { let keep_files = matches.get_flag(options::KEEP_FILES); let quiet = matches.get_flag(options::QUIET); let elide_empty_files = matches.get_flag(options::ELIDE_EMPTY_FILES); let suppress_matched = matches.get_flag(options::SUPPRESS_MATCHED); - Self { - split_name: crash_if_err!( - 1, - SplitName::new( - matches.get_one::(options::PREFIX).cloned(), - matches.get_one::(options::SUFFIX_FORMAT).cloned(), - matches.get_one::(options::DIGITS).cloned() - ) - ), + Ok(Self { + split_name: SplitName::new( + matches.get_one::(options::PREFIX).cloned(), + matches.get_one::(options::SUFFIX_FORMAT).cloned(), + matches.get_one::(options::DIGITS).cloned(), + )?, keep_files, quiet, elide_empty_files, suppress_matched, + }) + } +} + +pub struct LinesWithNewlines { + inner: T, +} + +impl LinesWithNewlines { + fn new(s: T) -> Self { + Self { inner: s } + } +} + +impl Iterator for LinesWithNewlines { + type Item = io::Result; + + fn next(&mut self) -> Option { + fn ret(v: Vec) -> io::Result { + String::from_utf8(v).map_err(|_| { + io::Error::new(ErrorKind::InvalidData, translate!("csplit-stream-not-utf8")) + }) + } + + let mut v = Vec::new(); + match self.inner.read_until(b'\n', &mut v) { + Ok(0) => None, + Ok(_) => Some(ret(v)), + Err(e) => Some(Err(e)), } } } @@ -86,28 +112,29 @@ impl CsplitOptions { /// - [`CsplitError::MatchNotFound`] if no line matched a regular expression. /// - [`CsplitError::MatchNotFoundOnRepetition`], like previous but after applying the pattern /// more than once. -pub fn csplit( - options: &CsplitOptions, - patterns: Vec, - input: T, -) -> Result<(), CsplitError> +pub fn csplit(options: &CsplitOptions, patterns: &[String], input: T) -> Result<(), CsplitError> where T: BufRead, { - let mut input_iter = InputSplitter::new(input.lines().enumerate()); + let enumerated_input_lines = LinesWithNewlines::new(input) + .map(|line| line.map_err_context(|| translate!("csplit-read-error"))) + .enumerate(); + let mut input_iter = InputSplitter::new(enumerated_input_lines); let mut split_writer = SplitWriter::new(options); - let patterns: Vec = patterns::get_patterns(&patterns[..])?; + let patterns: Vec = patterns::get_patterns(patterns)?; let ret = do_csplit(&mut split_writer, patterns, &mut input_iter); - // consume the rest - input_iter.rewind_buffer(); - if let Some((_, line)) = input_iter.next() { - split_writer.new_writer()?; - split_writer.writeln(&line?)?; - for (_, line) in input_iter { + // consume the rest, unless there was an error + if ret.is_ok() { + input_iter.rewind_buffer(); + if let Some((_, line)) = input_iter.next() { + split_writer.new_writer()?; split_writer.writeln(&line?)?; + for (_, line) in input_iter { + split_writer.writeln(&line?)?; + } + split_writer.finish_split(); } - split_writer.finish_split(); } // delete files on error by default if ret.is_err() && !options.keep_files { @@ -122,7 +149,7 @@ fn do_csplit( input_iter: &mut InputSplitter, ) -> Result<(), CsplitError> where - I: Iterator)>, + I: Iterator)>, { // split the file based on patterns for pattern in patterns { @@ -176,10 +203,10 @@ where (Err(err), _) => return Err(err), // continue the splitting process (Ok(()), _) => (), - }; + } } } - }; + } } Ok(()) } @@ -199,17 +226,22 @@ struct SplitWriter<'a> { dev_null: bool, } -impl<'a> Drop for SplitWriter<'a> { +impl Drop for SplitWriter<'_> { fn drop(&mut self) { if self.options.elide_empty_files && self.size == 0 { let file_name = self.options.split_name.get(self.counter); - remove_file(file_name).expect("Failed to elide split"); + // In the case of `echo a | csplit -z - %a%1`, the file + // `xx00` does not exist because the positive offset + // advanced past the end of the input. Since there is no + // file to remove in that case, `remove_file` would return + // an error, so we just ignore it. + let _ = remove_file(file_name); } } } -impl<'a> SplitWriter<'a> { - fn new(options: &CsplitOptions) -> SplitWriter { +impl SplitWriter<'_> { + fn new(options: &CsplitOptions) -> SplitWriter<'_> { SplitWriter { options, counter: 0, @@ -239,7 +271,7 @@ impl<'a> SplitWriter<'a> { self.dev_null = true; } - /// Writes the line to the current split, appending a newline character. + /// Writes the line to the current split. /// If [`self.dev_null`] is true, then the line is discarded. /// /// # Errors @@ -251,10 +283,9 @@ impl<'a> SplitWriter<'a> { Some(ref mut current_writer) => { let bytes = line.as_bytes(); current_writer.write_all(bytes)?; - current_writer.write_all(b"\n")?; - self.size += bytes.len() + 1; + self.size += bytes.len(); } - None => panic!("trying to write to a split that was not created"), + None => panic!("{}", translate!("csplit-write-split-not-created")), } } Ok(()) @@ -310,18 +341,18 @@ impl<'a> SplitWriter<'a> { input_iter: &mut InputSplitter, ) -> Result<(), CsplitError> where - I: Iterator)>, + I: Iterator)>, { input_iter.rewind_buffer(); input_iter.set_size_of_buffer(1); let mut ret = Err(CsplitError::LineOutOfRange(pattern_as_str.to_string())); while let Some((ln, line)) = input_iter.next() { - let l = line?; + let line = line?; match n.cmp(&(&ln + 1)) { Ordering::Less => { assert!( - input_iter.add_line_to_buffer(ln, l).is_none(), + input_iter.add_line_to_buffer(ln, line).is_none(), "the buffer is big enough to contain 1 line" ); ret = Ok(()); @@ -330,7 +361,7 @@ impl<'a> SplitWriter<'a> { Ordering::Equal => { assert!( self.options.suppress_matched - || input_iter.add_line_to_buffer(ln, l).is_none(), + || input_iter.add_line_to_buffer(ln, line).is_none(), "the buffer is big enough to contain 1 line" ); ret = Ok(()); @@ -338,7 +369,7 @@ impl<'a> SplitWriter<'a> { } Ordering::Greater => (), } - self.writeln(&l)?; + self.writeln(&line)?; } self.finish_split(); ret @@ -353,7 +384,7 @@ impl<'a> SplitWriter<'a> { /// In addition to errors reading/writing from/to a file, the following errors may be returned: /// - if no line matched, an [`CsplitError::MatchNotFound`]. /// - if there are not enough lines to accommodate the offset, an - /// [`CsplitError::LineOutOfRange`]. + /// [`CsplitError::LineOutOfRange`]. #[allow(clippy::cognitive_complexity)] fn do_to_match( &mut self, @@ -363,7 +394,7 @@ impl<'a> SplitWriter<'a> { input_iter: &mut InputSplitter, ) -> Result<(), CsplitError> where - I: Iterator)>, + I: Iterator)>, { if offset >= 0 { // The offset is zero or positive, no need for a buffer on the lines read. @@ -375,20 +406,29 @@ impl<'a> SplitWriter<'a> { input_iter.set_size_of_buffer(1); while let Some((ln, line)) = input_iter.next() { - let l = line?; - if regex.is_match(&l) { + let line = line?; + let l = line + .strip_suffix("\r\n") + .unwrap_or_else(|| line.strip_suffix('\n').unwrap_or(&line)); + if regex.is_match(l) { + let mut next_line_suppress_matched = false; match (self.options.suppress_matched, offset) { // no offset, add the line to the next split (false, 0) => { assert!( - input_iter.add_line_to_buffer(ln, l).is_none(), + input_iter.add_line_to_buffer(ln, line).is_none(), "the buffer is big enough to contain 1 line" ); } // a positive offset, some more lines need to be added to the current split - (false, _) => self.writeln(&l)?, + (false, _) => self.writeln(&line)?, + // suppress matched option true, but there is a positive offset, so the line is printed + (true, 1..) => { + next_line_suppress_matched = true; + self.writeln(&line)?; + } _ => (), - }; + } offset -= 1; // write the extra lines required by the offset @@ -403,13 +443,18 @@ impl<'a> SplitWriter<'a> { pattern_as_str.to_string(), )); } - }; + } offset -= 1; } self.finish_split(); + + // if we have to suppress one line after we take the next and do nothing + if next_line_suppress_matched { + input_iter.next(); + } return Ok(()); } - self.writeln(&l)?; + self.writeln(&line)?; } } else { // With a negative offset we use a buffer to keep the lines within the offset. @@ -420,26 +465,35 @@ impl<'a> SplitWriter<'a> { let offset_usize = -offset as usize; input_iter.set_size_of_buffer(offset_usize); while let Some((ln, line)) = input_iter.next() { - let l = line?; - if regex.is_match(&l) { + let line = line?; + let l = line + .strip_suffix("\r\n") + .unwrap_or_else(|| line.strip_suffix('\n').unwrap_or(&line)); + if regex.is_match(l) { for line in input_iter.shrink_buffer_to_size() { self.writeln(&line)?; } - if !self.options.suppress_matched { + if self.options.suppress_matched { + // since offset_usize is for sure greater than 0 + // the first element of the buffer should be removed and this + // line inserted to be coherent with GNU implementation + input_iter.add_line_to_buffer(ln, line); + } else { // add 1 to the buffer size to make place for the matched line input_iter.set_size_of_buffer(offset_usize + 1); assert!( - input_iter.add_line_to_buffer(ln, l).is_none(), + input_iter.add_line_to_buffer(ln, line).is_none(), "should be big enough to hold every lines" ); } + self.finish_split(); if input_iter.buffer_len() < offset_usize { return Err(CsplitError::LineOutOfRange(pattern_as_str.to_string())); } return Ok(()); } - if let Some(line) = input_iter.add_line_to_buffer(ln, l) { + if let Some(line) = input_iter.add_line_to_buffer(ln, line) { self.writeln(&line)?; } } @@ -458,7 +512,7 @@ impl<'a> SplitWriter<'a> { /// This is used to pass matching lines to the next split and to support patterns with a negative offset. struct InputSplitter where - I: Iterator)>, + I: Iterator)>, { iter: I, buffer: Vec<::Item>, @@ -471,7 +525,7 @@ where impl InputSplitter where - I: Iterator)>, + I: Iterator)>, { fn new(iter: I) -> Self { Self { @@ -535,7 +589,7 @@ where impl Iterator for InputSplitter where - I: Iterator)>, + I: Iterator)>, { type Item = ::Item; @@ -552,10 +606,10 @@ where #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); // get the file to split - let file_name = matches.get_one::(options::FILE).unwrap(); + let file_name = matches.get_one::(options::FILE).unwrap(); // get the patterns to split on let patterns: Vec = matches @@ -563,54 +617,50 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .unwrap() .map(|s| s.to_string()) .collect(); - let options = CsplitOptions::new(&matches); + let options = CsplitOptions::new(&matches)?; if file_name == "-" { let stdin = io::stdin(); - Ok(csplit(&options, patterns, stdin.lock())?) + Ok(csplit(&options, &patterns, stdin.lock())?) } else { let file = File::open(file_name) - .map_err_context(|| format!("cannot access {}", file_name.quote()))?; - let file_metadata = file - .metadata() - .map_err_context(|| format!("cannot access {}", file_name.quote()))?; - if !file_metadata.is_file() { - return Err(CsplitError::NotRegularFile(file_name.to_string()).into()); - } - Ok(csplit(&options, patterns, BufReader::new(file))?) + .map_err_context(|| format!("cannot open {} for reading", file_name.quote()))?; + Ok(csplit(&options, &patterns, BufReader::new(file))?) } } pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) + .about(translate!("csplit-about")) + .override_usage(format_usage(&translate!("csplit-usage"))) + .args_override_self(true) .infer_long_args(true) .arg( Arg::new(options::SUFFIX_FORMAT) .short('b') .long(options::SUFFIX_FORMAT) .value_name("FORMAT") - .help("use sprintf FORMAT instead of %02d"), + .help(translate!("csplit-help-suffix-format")), ) .arg( Arg::new(options::PREFIX) .short('f') .long(options::PREFIX) .value_name("PREFIX") - .help("use PREFIX instead of 'xx'"), + .help(translate!("csplit-help-prefix")), ) .arg( Arg::new(options::KEEP_FILES) .short('k') .long(options::KEEP_FILES) - .help("do not remove output files on errors") + .help(translate!("csplit-help-keep-files")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::SUPPRESS_MATCHED) .long(options::SUPPRESS_MATCHED) - .help("suppress the lines matching PATTERN") + .help(translate!("csplit-help-suppress-matched")) .action(ArgAction::SetTrue), ) .arg( @@ -618,36 +668,38 @@ pub fn uu_app() -> Command { .short('n') .long(options::DIGITS) .value_name("DIGITS") - .help("use specified number of digits instead of 2"), + .help(translate!("csplit-help-digits")), ) .arg( Arg::new(options::QUIET) - .short('s') + .short('q') .long(options::QUIET) + .visible_short_alias('s') .visible_alias("silent") - .help("do not print counts of output file sizes") + .help(translate!("csplit-help-quiet")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::ELIDE_EMPTY_FILES) .short('z') .long(options::ELIDE_EMPTY_FILES) - .help("remove empty output files") + .help(translate!("csplit-help-elide-empty-files")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::FILE) .hide(true) .required(true) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::PATTERN) .hide(true) - .action(clap::ArgAction::Append) + .action(ArgAction::Append) .required(true), ) - .after_help(AFTER_HELP) + .after_help(translate!("csplit-after-help")) } #[cfg(test)] @@ -675,7 +727,7 @@ mod tests { assert_eq!(input_splitter.buffer_len(), 1); } item => panic!("wrong item: {item:?}"), - }; + } match input_splitter.next() { Some((1, Ok(line))) => { @@ -684,7 +736,7 @@ mod tests { assert_eq!(input_splitter.buffer_len(), 2); } item => panic!("wrong item: {item:?}"), - }; + } match input_splitter.next() { Some((2, Ok(line))) => { @@ -696,7 +748,7 @@ mod tests { assert_eq!(input_splitter.buffer_len(), 2); } item => panic!("wrong item: {item:?}"), - }; + } input_splitter.rewind_buffer(); @@ -706,7 +758,7 @@ mod tests { assert_eq!(input_splitter.buffer_len(), 1); } item => panic!("wrong item: {item:?}"), - }; + } match input_splitter.next() { Some((2, Ok(line))) => { @@ -714,7 +766,7 @@ mod tests { assert_eq!(input_splitter.buffer_len(), 0); } item => panic!("wrong item: {item:?}"), - }; + } match input_splitter.next() { Some((3, Ok(line))) => { @@ -722,7 +774,7 @@ mod tests { assert_eq!(input_splitter.buffer_len(), 0); } item => panic!("wrong item: {item:?}"), - }; + } assert!(input_splitter.next().is_none()); } @@ -748,7 +800,7 @@ mod tests { assert_eq!(input_splitter.buffer_len(), 1); } item => panic!("wrong item: {item:?}"), - }; + } match input_splitter.next() { Some((1, Ok(line))) => { @@ -757,7 +809,7 @@ mod tests { assert_eq!(input_splitter.buffer_len(), 2); } item => panic!("wrong item: {item:?}"), - }; + } match input_splitter.next() { Some((2, Ok(line))) => { @@ -766,7 +818,7 @@ mod tests { assert_eq!(input_splitter.buffer_len(), 3); } item => panic!("wrong item: {item:?}"), - }; + } input_splitter.rewind_buffer(); @@ -777,7 +829,7 @@ mod tests { assert_eq!(input_splitter.buffer_len(), 3); } item => panic!("wrong item: {item:?}"), - }; + } match input_splitter.next() { Some((0, Ok(line))) => { @@ -785,7 +837,7 @@ mod tests { assert_eq!(input_splitter.buffer_len(), 2); } item => panic!("wrong item: {item:?}"), - }; + } match input_splitter.next() { Some((1, Ok(line))) => { @@ -793,7 +845,7 @@ mod tests { assert_eq!(input_splitter.buffer_len(), 1); } item => panic!("wrong item: {item:?}"), - }; + } match input_splitter.next() { Some((2, Ok(line))) => { @@ -801,7 +853,7 @@ mod tests { assert_eq!(input_splitter.buffer_len(), 0); } item => panic!("wrong item: {item:?}"), - }; + } match input_splitter.next() { Some((3, Ok(line))) => { @@ -809,7 +861,7 @@ mod tests { assert_eq!(input_splitter.buffer_len(), 0); } item => panic!("wrong item: {item:?}"), - }; + } assert!(input_splitter.next().is_none()); } diff --git a/src/uu/csplit/src/csplit_error.rs b/src/uu/csplit/src/csplit_error.rs index 4a83b637b07..d73400bd7d1 100644 --- a/src/uu/csplit/src/csplit_error.rs +++ b/src/uu/csplit/src/csplit_error.rs @@ -2,49 +2,55 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. + use std::io; use thiserror::Error; - use uucore::display::Quotable; use uucore::error::UError; +use uucore::translate; /// Errors thrown by the csplit command #[derive(Debug, Error)] pub enum CsplitError { #[error("IO error: {}", _0)] - IoError(io::Error), - #[error("{}: line number out of range", ._0.quote())] + IoError(#[from] io::Error), + #[error("{}", translate!("csplit-error-line-out-of-range", "pattern" => _0.quote()))] LineOutOfRange(String), - #[error("{}: line number out of range on repetition {}", ._0.quote(), _1)] + #[error("{}", translate!("csplit-error-line-out-of-range-on-repetition", "pattern" => _0.quote(), "repetition" => _1))] LineOutOfRangeOnRepetition(String, usize), - #[error("{}: match not found", ._0.quote())] + #[error("{}", translate!("csplit-error-match-not-found", "pattern" => _0.quote()))] MatchNotFound(String), - #[error("{}: match not found on repetition {}", ._0.quote(), _1)] + #[error("{}", translate!("csplit-error-match-not-found-on-repetition", "pattern" => _0.quote(), "repetition" => _1))] MatchNotFoundOnRepetition(String, usize), - #[error("0: line number must be greater than zero")] + #[error("{}", translate!("csplit-error-line-number-is-zero"))] LineNumberIsZero, - #[error("line number '{}' is smaller than preceding line number, {}", _0, _1)] + #[error("{}", translate!("csplit-error-line-number-smaller-than-previous", "current" => _0, "previous" => _1))] LineNumberSmallerThanPrevious(usize, usize), - #[error("{}: invalid pattern", ._0.quote())] + #[error("{}", translate!("csplit-error-invalid-pattern", "pattern" => _0.quote()))] InvalidPattern(String), - #[error("invalid number: {}", ._0.quote())] + #[error("{}", translate!("csplit-error-invalid-number", "number" => _0.quote()))] InvalidNumber(String), - #[error("incorrect conversion specification in suffix")] + #[error("{}", translate!("csplit-error-suffix-format-incorrect"))] SuffixFormatIncorrect, - #[error("too many % conversion specifications in suffix")] + #[error("{}", translate!("csplit-error-suffix-format-too-many-percents"))] SuffixFormatTooManyPercents, - #[error("{} is not a regular file", ._0.quote())] + #[error("{}", translate!("csplit-error-not-regular-file", "file" => _0.quote()))] NotRegularFile(String), + #[error("{}", _0)] + UError(Box), } -impl From for CsplitError { - fn from(error: io::Error) -> Self { - Self::IoError(error) +impl From> for CsplitError { + fn from(error: Box) -> Self { + Self::UError(error) } } impl UError for CsplitError { fn code(&self) -> i32 { - 1 + match self { + Self::UError(e) => e.code(), + _ => 1, + } } } diff --git a/src/uu/csplit/src/patterns.rs b/src/uu/csplit/src/patterns.rs index 6e7483b7f9f..4bf191c8f35 100644 --- a/src/uu/csplit/src/patterns.rs +++ b/src/uu/csplit/src/patterns.rs @@ -7,6 +7,7 @@ use crate::csplit_error::CsplitError; use regex::Regex; use uucore::show_warning; +use uucore::translate; /// The definition of a pattern to match on a line. #[derive(Debug)] @@ -25,14 +26,14 @@ pub enum Pattern { SkipToMatch(Regex, i32, ExecutePattern), } -impl ToString for Pattern { - fn to_string(&self) -> String { +impl std::fmt::Display for Pattern { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::UpToLine(n, _) => n.to_string(), - Self::UpToMatch(regex, 0, _) => format!("/{}/", regex.as_str()), - Self::UpToMatch(regex, offset, _) => format!("/{}/{:+}", regex.as_str(), offset), - Self::SkipToMatch(regex, 0, _) => format!("%{}%", regex.as_str()), - Self::SkipToMatch(regex, offset, _) => format!("%{}%{:+}", regex.as_str(), offset), + Self::UpToLine(n, _) => write!(f, "{n}"), + Self::UpToMatch(regex, 0, _) => write!(f, "/{}/", regex.as_str()), + Self::UpToMatch(regex, offset, _) => write!(f, "/{}/{offset:+}", regex.as_str()), + Self::SkipToMatch(regex, 0, _) => write!(f, "%{}%", regex.as_str()), + Self::SkipToMatch(regex, offset, _) => write!(f, "%{}%{offset:+}", regex.as_str()), } } } @@ -106,8 +107,8 @@ pub fn get_patterns(args: &[String]) -> Result, CsplitError> { fn extract_patterns(args: &[String]) -> Result, CsplitError> { let mut patterns = Vec::with_capacity(args.len()); let to_match_reg = - Regex::new(r"^(/(?P.+)/|%(?P.+)%)(?P[\+-]\d+)?$").unwrap(); - let execute_ntimes_reg = Regex::new(r"^\{(?P\d+)|\*\}$").unwrap(); + Regex::new(r"^(/(?P.+)/|%(?P.+)%)(?P[\+-]?[0-9]+)?$").unwrap(); + let execute_ntimes_reg = Regex::new(r"^\{(?P[0-9]+)|\*\}$").unwrap(); let mut iter = args.iter().peekable(); while let Some(arg) = iter.next() { @@ -168,7 +169,10 @@ fn validate_line_numbers(patterns: &[Pattern]) -> Result<(), CsplitError> { (_, 0) => Err(CsplitError::LineNumberIsZero), // two consecutive numbers should not be equal (n, m) if n == m => { - show_warning!("line number '{}' is the same as preceding line number", n); + show_warning!( + "{}", + translate!("csplit-warning-line-number-same-as-previous", "line_number" => n) + ); Ok(n) } // a number cannot be greater than the one that follows @@ -199,15 +203,15 @@ mod tests { match patterns.first() { Some(Pattern::UpToLine(24, ExecutePattern::Times(1))) => (), _ => panic!("expected UpToLine pattern"), - }; + } match patterns.get(1) { Some(Pattern::UpToLine(42, ExecutePattern::Always)) => (), _ => panic!("expected UpToLine pattern"), - }; + } match patterns.get(2) { Some(Pattern::UpToLine(50, ExecutePattern::Times(5))) => (), _ => panic!("expected UpToLine pattern"), - }; + } } #[test] @@ -219,49 +223,57 @@ mod tests { "{*}", "/test3.*end$/", "{4}", - "/test4.*end$/+3", - "/test5.*end$/-3", + "/test4.*end$/3", + "/test5.*end$/+3", + "/test6.*end$/-3", ] .into_iter() .map(|v| v.to_string()) .collect(); let patterns = get_patterns(input.as_slice()).unwrap(); - assert_eq!(patterns.len(), 5); + assert_eq!(patterns.len(), 6); match patterns.first() { Some(Pattern::UpToMatch(reg, 0, ExecutePattern::Times(1))) => { let parsed_reg = format!("{reg}"); assert_eq!(parsed_reg, "test1.*end$"); } _ => panic!("expected UpToMatch pattern"), - }; + } match patterns.get(1) { Some(Pattern::UpToMatch(reg, 0, ExecutePattern::Always)) => { let parsed_reg = format!("{reg}"); assert_eq!(parsed_reg, "test2.*end$"); } _ => panic!("expected UpToMatch pattern"), - }; + } match patterns.get(2) { Some(Pattern::UpToMatch(reg, 0, ExecutePattern::Times(5))) => { let parsed_reg = format!("{reg}"); assert_eq!(parsed_reg, "test3.*end$"); } _ => panic!("expected UpToMatch pattern"), - }; + } match patterns.get(3) { Some(Pattern::UpToMatch(reg, 3, ExecutePattern::Times(1))) => { let parsed_reg = format!("{reg}"); assert_eq!(parsed_reg, "test4.*end$"); } _ => panic!("expected UpToMatch pattern"), - }; + } match patterns.get(4) { - Some(Pattern::UpToMatch(reg, -3, ExecutePattern::Times(1))) => { + Some(Pattern::UpToMatch(reg, 3, ExecutePattern::Times(1))) => { let parsed_reg = format!("{reg}"); assert_eq!(parsed_reg, "test5.*end$"); } _ => panic!("expected UpToMatch pattern"), - }; + } + match patterns.get(5) { + Some(Pattern::UpToMatch(reg, -3, ExecutePattern::Times(1))) => { + let parsed_reg = format!("{reg}"); + assert_eq!(parsed_reg, "test6.*end$"); + } + _ => panic!("expected UpToMatch pattern"), + } } #[test] @@ -273,49 +285,57 @@ mod tests { "{*}", "%test3.*end$%", "{4}", - "%test4.*end$%+3", - "%test5.*end$%-3", + "%test4.*end$%3", + "%test5.*end$%+3", + "%test6.*end$%-3", ] .into_iter() .map(|v| v.to_string()) .collect(); let patterns = get_patterns(input.as_slice()).unwrap(); - assert_eq!(patterns.len(), 5); + assert_eq!(patterns.len(), 6); match patterns.first() { Some(Pattern::SkipToMatch(reg, 0, ExecutePattern::Times(1))) => { let parsed_reg = format!("{reg}"); assert_eq!(parsed_reg, "test1.*end$"); } _ => panic!("expected SkipToMatch pattern"), - }; + } match patterns.get(1) { Some(Pattern::SkipToMatch(reg, 0, ExecutePattern::Always)) => { let parsed_reg = format!("{reg}"); assert_eq!(parsed_reg, "test2.*end$"); } _ => panic!("expected SkipToMatch pattern"), - }; + } match patterns.get(2) { Some(Pattern::SkipToMatch(reg, 0, ExecutePattern::Times(5))) => { let parsed_reg = format!("{reg}"); assert_eq!(parsed_reg, "test3.*end$"); } _ => panic!("expected SkipToMatch pattern"), - }; + } match patterns.get(3) { Some(Pattern::SkipToMatch(reg, 3, ExecutePattern::Times(1))) => { let parsed_reg = format!("{reg}"); assert_eq!(parsed_reg, "test4.*end$"); } _ => panic!("expected SkipToMatch pattern"), - }; + } match patterns.get(4) { - Some(Pattern::SkipToMatch(reg, -3, ExecutePattern::Times(1))) => { + Some(Pattern::SkipToMatch(reg, 3, ExecutePattern::Times(1))) => { let parsed_reg = format!("{reg}"); assert_eq!(parsed_reg, "test5.*end$"); } _ => panic!("expected SkipToMatch pattern"), - }; + } + match patterns.get(5) { + Some(Pattern::SkipToMatch(reg, -3, ExecutePattern::Times(1))) => { + let parsed_reg = format!("{reg}"); + assert_eq!(parsed_reg, "test6.*end$"); + } + _ => panic!("expected SkipToMatch pattern"), + } } #[test] diff --git a/src/uu/csplit/src/split_name.rs b/src/uu/csplit/src/split_name.rs index e2432f3ce10..33c606b4888 100644 --- a/src/uu/csplit/src/split_name.rs +++ b/src/uu/csplit/src/split_name.rs @@ -4,7 +4,7 @@ // file that was distributed with this source code. // spell-checker:ignore (regex) diuox -use uucore::format::{num_format::UnsignedInt, Format, FormatError}; +use uucore::format::{Format, FormatError, num_format::UnsignedInt}; use crate::csplit_error::CsplitError; @@ -12,14 +12,14 @@ use crate::csplit_error::CsplitError; /// format. pub struct SplitName { prefix: Vec, - format: Format, + format: Format, } impl SplitName { - /// Creates a new SplitName with the given user-defined options: + /// Creates a new [`SplitName`] with the given user-defined options: /// - `prefix_opt` specifies a prefix for all splits. /// - `format_opt` specifies a custom format for the suffix part of the filename, using the - /// `sprintf` format notation. + /// `sprintf` format notation. /// - `n_digits_opt` defines the width of the split number. /// /// # Caveats @@ -47,12 +47,9 @@ impl SplitName { .transpose()? .unwrap_or(2); - let format_string = match format_opt { - Some(f) => f, - None => format!("%0{n_digits}u"), - }; + let format_string = format_opt.unwrap_or_else(|| format!("%0{n_digits}u")); - let format = match Format::::parse(format_string) { + let format = match Format::::parse(format_string) { Ok(format) => Ok(format), Err(FormatError::TooManySpecs(_)) => Err(CsplitError::SuffixFormatTooManyPercents), Err(_) => Err(CsplitError::SuffixFormatIncorrect), @@ -84,7 +81,7 @@ mod tests { match split_name { Err(CsplitError::InvalidNumber(_)) => (), _ => panic!("should fail with InvalidNumber"), - }; + } } #[test] @@ -93,7 +90,7 @@ mod tests { match split_name { Err(CsplitError::SuffixFormatIncorrect) => (), _ => panic!("should fail with SuffixFormatIncorrect"), - }; + } } #[test] @@ -102,7 +99,7 @@ mod tests { match split_name { Err(CsplitError::SuffixFormatIncorrect) => (), _ => panic!("should fail with SuffixFormatIncorrect"), - }; + } } #[test] @@ -247,6 +244,6 @@ mod tests { match split_name { Err(CsplitError::SuffixFormatTooManyPercents) => (), _ => panic!("should fail with SuffixFormatTooManyPercents"), - }; + } } } diff --git a/src/uu/cut/Cargo.toml b/src/uu/cut/Cargo.toml index 17ec0c52d87..360ec1fee08 100644 --- a/src/uu/cut/Cargo.toml +++ b/src/uu/cut/Cargo.toml @@ -1,15 +1,18 @@ [package] name = "uu_cut" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "cut ~ (uutils) display byte/field columns of input lines" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/cut" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/cut.rs" @@ -19,6 +22,7 @@ clap = { workspace = true } uucore = { workspace = true, features = ["ranges"] } memchr = { workspace = true } bstr = { workspace = true } +fluent = { workspace = true } [[bin]] name = "cut" diff --git a/src/uu/cut/cut.md b/src/uu/cut/cut.md deleted file mode 100644 index 5c21d23dcf9..00000000000 --- a/src/uu/cut/cut.md +++ /dev/null @@ -1,112 +0,0 @@ -# cut - - - -``` -cut OPTION... [FILE]... -``` - -Prints specified byte or field columns from each line of stdin or the input files - -## After Help - -Each call must specify a mode (what to use for columns), -a sequence (which columns to print), and provide a data source - -### Specifying a mode - -Use `--bytes` (`-b`) or `--characters` (`-c`) to specify byte mode - -Use `--fields` (`-f`) to specify field mode, where each line is broken into -fields identified by a delimiter character. For example for a typical CSV -you could use this in combination with setting comma as the delimiter - -### Specifying a sequence - -A sequence is a group of 1 or more numbers or inclusive ranges separated -by a commas. - -``` -cut -f 2,5-7 some_file.txt -``` - -will display the 2nd, 5th, 6th, and 7th field for each source line - -Ranges can extend to the end of the row by excluding the second number - -``` -cut -f 3- some_file.txt -``` - -will display the 3rd field and all fields after for each source line - -The first number of a range can be excluded, and this is effectively the -same as using 1 as the first number: it causes the range to begin at the -first column. Ranges can also display a single column - -``` -cut -f 1,3-5 some_file.txt -``` - -will display the 1st, 3rd, 4th, and 5th field for each source line - -The `--complement` option, when used, inverts the effect of the sequence - -``` -cut --complement -f 4-6 some_file.txt -``` - -will display the every field but the 4th, 5th, and 6th - -### Specifying a data source - -If no `sourcefile` arguments are specified, stdin is used as the source of -lines to print - -If `sourcefile` arguments are specified, stdin is ignored and all files are -read in consecutively if a `sourcefile` is not successfully read, a warning -will print to stderr, and the eventual status code will be 1, but cut -will continue to read through proceeding `sourcefiles` - -To print columns from both STDIN and a file argument, use `-` (dash) as a -`sourcefile` argument to represent stdin. - -### Field Mode options - -The fields in each line are identified by a delimiter (separator) - -#### Set the delimiter - -Set the delimiter which separates fields in the file using the -`--delimiter` (`-d`) option. Setting the delimiter is optional. -If not set, a default delimiter of Tab will be used. - -If the `-w` option is provided, fields will be separated by any number -of whitespace characters (Space and Tab). The output delimiter will -be a Tab unless explicitly specified. Only one of `-d` or `-w` option can be specified. -This is an extension adopted from FreeBSD. - -#### Optionally Filter based on delimiter - -If the `--only-delimited` (`-s`) flag is provided, only lines which -contain the delimiter will be printed - -#### Replace the delimiter - -If the `--output-delimiter` option is provided, the argument used for -it will replace the delimiter character in each line printed. This is -useful for transforming tabular data - e.g. to convert a CSV to a -TSV (tab-separated file) - -### Line endings - -When the `--zero-terminated` (`-z`) option is used, cut sees \\0 (null) as the -'line ending' character (both for the purposes of reading lines and -separating printed lines) instead of \\n (newline). This is useful for -tabular data where some of the cells may contain newlines - -``` -echo 'ab\\0cd' | cut -z -c 1 -``` - -will result in 'a\\0c\\0' diff --git a/src/uu/cut/locales/en-US.ftl b/src/uu/cut/locales/en-US.ftl new file mode 100644 index 00000000000..3c412ced8d0 --- /dev/null +++ b/src/uu/cut/locales/en-US.ftl @@ -0,0 +1,114 @@ +cut-about = Prints specified byte or field columns from each line of stdin or the input files +cut-usage = cut OPTION... [FILE]... +cut-after-help = Each call must specify a mode (what to use for columns), + a sequence (which columns to print), and provide a data source + + ### Specifying a mode + + Use --bytes (-b) or --characters (-c) to specify byte mode + + Use --fields (-f) to specify field mode, where each line is broken into + fields identified by a delimiter character. For example for a typical CSV + you could use this in combination with setting comma as the delimiter + + ### Specifying a sequence + + A sequence is a group of 1 or more numbers or inclusive ranges separated + by a commas. + + cut -f 2,5-7 some_file.txt + + will display the 2nd, 5th, 6th, and 7th field for each source line + + Ranges can extend to the end of the row by excluding the second number + + cut -f 3- some_file.txt + + will display the 3rd field and all fields after for each source line + + The first number of a range can be excluded, and this is effectively the + same as using 1 as the first number: it causes the range to begin at the + first column. Ranges can also display a single column + + cut -f 1,3-5 some_file.txt + + will display the 1st, 3rd, 4th, and 5th field for each source line + + The --complement option, when used, inverts the effect of the sequence + + cut --complement -f 4-6 some_file.txt + + will display the every field but the 4th, 5th, and 6th + + ### Specifying a data source + + If no sourcefile arguments are specified, stdin is used as the source of + lines to print + + If sourcefile arguments are specified, stdin is ignored and all files are + read in consecutively if a sourcefile is not successfully read, a warning + will print to stderr, and the eventual status code will be 1, but cut + will continue to read through proceeding sourcefiles + + To print columns from both STDIN and a file argument, use - (dash) as a + sourcefile argument to represent stdin. + + ### Field Mode options + + The fields in each line are identified by a delimiter (separator) + + #### Set the delimiter + + Set the delimiter which separates fields in the file using the + --delimiter (-d) option. Setting the delimiter is optional. + If not set, a default delimiter of Tab will be used. + + If the -w option is provided, fields will be separated by any number + of whitespace characters (Space and Tab). The output delimiter will + be a Tab unless explicitly specified. Only one of -d or -w option can be specified. + This is an extension adopted from FreeBSD. + + #### Optionally Filter based on delimiter + + If the --only-delimited (-s) flag is provided, only lines which + contain the delimiter will be printed + + #### Replace the delimiter + + If the --output-delimiter option is provided, the argument used for + it will replace the delimiter character in each line printed. This is + useful for transforming tabular data - e.g. to convert a CSV to a + TSV (tab-separated file) + + ### Line endings + + When the --zero-terminated (-z) option is used, cut sees \\0 (null) as the + 'line ending' character (both for the purposes of reading lines and + separating printed lines) instead of \\n (newline). This is useful for + tabular data where some of the cells may contain newlines + + echo 'ab\\0cd' | cut -z -c 1 + + will result in 'a\\0c\\0' + +# Help messages +cut-help-bytes = filter byte columns from the input source +cut-help-characters = alias for character mode +cut-help-delimiter = specify the delimiter character that separates fields in the input source. Defaults to Tab. +cut-help-whitespace-delimited = Use any number of whitespace (Space, Tab) to separate fields in the input source (FreeBSD extension). +cut-help-fields = filter field columns from the input source +cut-help-complement = invert the filter - instead of displaying only the filtered columns, display all but those columns +cut-help-only-delimited = in field mode, only print lines which contain the delimiter +cut-help-zero-terminated = instead of filtering columns based on line, filter columns based on \\0 (NULL character) +cut-help-output-delimiter = in field mode, replace the delimiter in output lines with this option's argument + +# Error messages +cut-error-is-directory = Is a directory +cut-error-write-error = write error +cut-error-delimiter-and-whitespace-conflict = invalid input: Only one of --delimiter (-d) or -w option can be specified +cut-error-delimiter-must-be-single-character = the delimiter must be a single character +cut-error-multiple-mode-args = invalid usage: expects no more than one of --fields (-f), --chars (-c) or --bytes (-b) +cut-error-missing-mode-arg = invalid usage: expects one of --fields (-f), --chars (-c) or --bytes (-b) +cut-error-delimiter-only-with-fields = invalid input: The '--delimiter' ('-d') option only usable if printing a sequence of fields +cut-error-whitespace-only-with-fields = invalid input: The '-w' option only usable if printing a sequence of fields +cut-error-only-delimited-only-with-fields = invalid input: The '--only-delimited' ('-s') option only usable if printing a sequence of fields diff --git a/src/uu/cut/locales/fr-FR.ftl b/src/uu/cut/locales/fr-FR.ftl new file mode 100644 index 00000000000..cab0d8ccd35 --- /dev/null +++ b/src/uu/cut/locales/fr-FR.ftl @@ -0,0 +1,114 @@ +cut-about = Affiche les colonnes d'octets ou de champs spécifiées de chaque ligne de stdin ou des fichiers d'entrée +cut-usage = cut OPTION... [FICHIER]... +cut-after-help = Chaque appel doit spécifier un mode (quoi utiliser pour les colonnes), + une séquence (quelles colonnes afficher), et fournir une source de données + + ### Spécifier un mode + + Utilisez --bytes (-b) ou --characters (-c) pour spécifier le mode octet + + Utilisez --fields (-f) pour spécifier le mode champ, où chaque ligne est divisée en + champs identifiés par un caractère délimiteur. Par exemple pour un CSV typique + vous pourriez utiliser ceci en combinaison avec la définition de la virgule comme délimiteur + + ### Spécifier une séquence + + Une séquence est un groupe de 1 ou plusieurs nombres ou plages inclusives séparés + par des virgules. + + cut -f 2,5-7 quelque_fichier.txt + + affichera les 2ème, 5ème, 6ème, et 7ème champs pour chaque ligne source + + Les plages peuvent s'étendre jusqu'à la fin de la ligne en excluant le second nombre + + cut -f 3- quelque_fichier.txt + + affichera le 3ème champ et tous les champs suivants pour chaque ligne source + + Le premier nombre d'une plage peut être exclu, et ceci est effectivement + identique à utiliser 1 comme premier nombre : cela fait commencer la plage à la + première colonne. Les plages peuvent aussi afficher une seule colonne + + cut -f 1,3-5 quelque_fichier.txt + + affichera les 1er, 3ème, 4ème, et 5ème champs pour chaque ligne source + + L'option --complement, quand utilisée, inverse l'effet de la séquence + + cut --complement -f 4-6 quelque_fichier.txt + + affichera tous les champs sauf les 4ème, 5ème, et 6ème + + ### Spécifier une source de données + + Si aucun argument de fichier source n'est spécifié, stdin est utilisé comme source + de lignes à afficher + + Si des arguments de fichier source sont spécifiés, stdin est ignoré et tous les fichiers sont + lus consécutivement si un fichier source n'est pas lu avec succès, un avertissement + sera affiché sur stderr, et le code de statut final sera 1, mais cut + continuera à lire les fichiers sources suivants + + Pour afficher les colonnes depuis STDIN et un argument de fichier, utilisez - (tiret) comme + argument de fichier source pour représenter stdin. + + ### Options du Mode Champ + + Les champs dans chaque ligne sont identifiés par un délimiteur (séparateur) + + #### Définir le délimiteur + + Définissez le délimiteur qui sépare les champs dans le fichier en utilisant l'option + --delimiter (-d). Définir le délimiteur est optionnel. + Si non défini, un délimiteur par défaut de Tab sera utilisé. + + Si l'option -w est fournie, les champs seront séparés par tout nombre + de caractères d'espacement (Espace et Tab). Le délimiteur de sortie sera + un Tab sauf si explicitement spécifié. Seulement une des options -d ou -w peut être spécifiée. + Ceci est une extension adoptée de FreeBSD. + + #### Filtrage optionnel basé sur le délimiteur + + Si le drapeau --only-delimited (-s) est fourni, seules les lignes qui + contiennent le délimiteur seront affichées + + #### Remplacer le délimiteur + + Si l'option --output-delimiter est fournie, l'argument utilisé pour + elle remplacera le caractère délimiteur dans chaque ligne affichée. Ceci est + utile pour transformer les données tabulaires - par ex. pour convertir un CSV en + TSV (fichier séparé par tabulations) + + ### Fins de ligne + + Quand l'option --zero-terminated (-z) est utilisée, cut voit \\0 (null) comme le + caractère de 'fin de ligne' (à la fois pour lire les lignes et + séparer les lignes affichées) au lieu de \\n (nouvelle ligne). Ceci est utile pour + les données tabulaires où certaines cellules peuvent contenir des nouvelles lignes + + echo 'ab\\0cd' | cut -z -c 1 + + donnera comme résultat 'a\\0c\\0' + +# Messages d'aide +cut-help-bytes = filtrer les colonnes d'octets depuis la source d'entrée +cut-help-characters = alias pour le mode caractère +cut-help-delimiter = spécifier le caractère délimiteur qui sépare les champs dans la source d'entrée. Par défaut Tab. +cut-help-whitespace-delimited = Utiliser tout nombre d'espaces (Espace, Tab) pour séparer les champs dans la source d'entrée (extension FreeBSD). +cut-help-fields = filtrer les colonnes de champs depuis la source d'entrée +cut-help-complement = inverser le filtre - au lieu d'afficher seulement les colonnes filtrées, afficher toutes sauf ces colonnes +cut-help-only-delimited = en mode champ, afficher seulement les lignes qui contiennent le délimiteur +cut-help-zero-terminated = au lieu de filtrer les colonnes basées sur la ligne, filtrer les colonnes basées sur \\0 (caractère NULL) +cut-help-output-delimiter = en mode champ, remplacer le délimiteur dans les lignes de sortie avec l'argument de cette option + +# Messages d'erreur +cut-error-is-directory = Est un répertoire +cut-error-write-error = erreur d'écriture +cut-error-delimiter-and-whitespace-conflict = entrée invalide : Seulement une des options --delimiter (-d) ou -w peut être spécifiée +cut-error-delimiter-must-be-single-character = le délimiteur doit être un caractère unique +cut-error-multiple-mode-args = usage invalide : attend au plus une des options --fields (-f), --chars (-c) ou --bytes (-b) +cut-error-missing-mode-arg = usage invalide : attend une des options --fields (-f), --chars (-c) ou --bytes (-b) +cut-error-delimiter-only-with-fields = entrée invalide : L'option '--delimiter' ('-d') n'est utilisable que si on affiche une séquence de champs +cut-error-whitespace-only-with-fields = entrée invalide : L'option '-w' n'est utilisable que si on affiche une séquence de champs +cut-error-only-delimited-only-with-fields = entrée invalide : L'option '--only-delimited' ('-s') n'est utilisable que si on affiche une séquence de champs diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 1b9194c170b..aea44c98875 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -6,29 +6,26 @@ // spell-checker:ignore (ToDO) delim sourcefiles use bstr::io::BufReadExt; -use clap::{builder::ValueParser, crate_version, Arg, ArgAction, ArgMatches, Command}; +use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser}; use std::ffi::OsString; use std::fs::File; -use std::io::{stdin, stdout, BufReader, BufWriter, IsTerminal, Read, Write}; -#[cfg(unix)] -use std::os::unix::ffi::OsStrExt; +use std::io::{BufRead, BufReader, BufWriter, IsTerminal, Read, Write, stdin, stdout}; use std::path::Path; use uucore::display::Quotable; -use uucore::error::{set_exit_code, FromIo, UResult, USimpleError}; +use uucore::error::{FromIo, UResult, USimpleError, set_exit_code}; use uucore::line_ending::LineEnding; +use uucore::os_str_as_bytes; use self::searcher::Searcher; use matcher::{ExactMatcher, Matcher, WhitespaceMatcher}; +use uucore::LocalizedCommand; use uucore::ranges::Range; -use uucore::{format_usage, help_about, help_section, help_usage, show_error, show_if_err}; +use uucore::translate; +use uucore::{format_usage, show_error, show_if_err}; mod matcher; mod searcher; -const USAGE: &str = help_usage!("cut.md"); -const ABOUT: &str = help_about!("cut.md"); -const AFTER_HELP: &str = help_section!("after help", "cut.md"); - struct Options<'a> { out_delimiter: Option<&'a [u8]>, line_ending: LineEnding, @@ -59,15 +56,7 @@ impl Default for Delimiter<'_> { impl<'a> From<&'a OsString> for Delimiter<'a> { fn from(s: &'a OsString) -> Self { - Self::Slice(os_string_as_bytes(s).unwrap()) - } -} - -fn stdout_writer() -> Box { - if std::io::stdout().is_terminal() { - Box::new(stdout()) - } else { - Box::new(BufWriter::new(stdout())) as Box + Self::Slice(os_str_as_bytes(s).unwrap()) } } @@ -79,10 +68,14 @@ fn list_to_ranges(list: &str, complement: bool) -> Result, String> { } } -fn cut_bytes(reader: R, ranges: &[Range], opts: &Options) -> UResult<()> { +fn cut_bytes( + reader: R, + out: &mut W, + ranges: &[Range], + opts: &Options, +) -> UResult<()> { let newline_char = opts.line_ending.into(); let mut buf_in = BufReader::new(reader); - let mut out = stdout_writer(); let out_delim = opts.out_delimiter.unwrap_or(b"\t"); let result = buf_in.for_byte_record(newline_char, |line| { @@ -112,9 +105,10 @@ fn cut_bytes(reader: R, ranges: &[Range], opts: &Options) -> UResult<() Ok(()) } -// Output delimiter is explicitly specified -fn cut_fields_explicit_out_delim( +/// Output delimiter is explicitly specified +fn cut_fields_explicit_out_delim( reader: R, + out: &mut W, matcher: &M, ranges: &[Range], only_delimited: bool, @@ -122,7 +116,6 @@ fn cut_fields_explicit_out_delim( out_delim: &[u8], ) -> UResult<()> { let mut buf_in = BufReader::new(reader); - let mut out = stdout_writer(); let result = buf_in.for_byte_record_with_terminator(newline_char, |line| { let mut fields_pos = 1; @@ -132,8 +125,9 @@ fn cut_fields_explicit_out_delim( if delim_search.peek().is_none() { if !only_delimited { + // Always write the entire line, even if it doesn't end with `newline_char` out.write_all(line)?; - if line[line.len() - 1] != newline_char { + if line.is_empty() || line[line.len() - 1] != newline_char { out.write_all(&[newline_char])?; } } @@ -196,16 +190,16 @@ fn cut_fields_explicit_out_delim( Ok(()) } -// Output delimiter is the same as input delimiter -fn cut_fields_implicit_out_delim( +/// Output delimiter is the same as input delimiter +fn cut_fields_implicit_out_delim( reader: R, + out: &mut W, matcher: &M, ranges: &[Range], only_delimited: bool, newline_char: u8, ) -> UResult<()> { let mut buf_in = BufReader::new(reader); - let mut out = stdout_writer(); let result = buf_in.for_byte_record_with_terminator(newline_char, |line| { let mut fields_pos = 1; @@ -215,8 +209,9 @@ fn cut_fields_implicit_out_delim( if delim_search.peek().is_none() { if !only_delimited { + // Always write the entire line, even if it doesn't end with `newline_char` out.write_all(line)?; - if line[line.len() - 1] != newline_char { + if line.is_empty() || line[line.len() - 1] != newline_char { out.write_all(&[newline_char])?; } } @@ -266,15 +261,57 @@ fn cut_fields_implicit_out_delim( Ok(()) } -fn cut_fields(reader: R, ranges: &[Range], opts: &Options) -> UResult<()> { +/// The input delimiter is identical to `newline_char` +fn cut_fields_newline_char_delim( + reader: R, + out: &mut W, + ranges: &[Range], + newline_char: u8, + out_delim: &[u8], +) -> UResult<()> { + let buf_in = BufReader::new(reader); + + let segments: Vec<_> = buf_in.split(newline_char).filter_map(|x| x.ok()).collect(); + let mut print_delim = false; + + for &Range { low, high } in ranges { + for i in low..=high { + // "- 1" is necessary because fields start from 1 whereas a Vec starts from 0 + if let Some(segment) = segments.get(i - 1) { + if print_delim { + out.write_all(out_delim)?; + } else { + print_delim = true; + } + out.write_all(segment.as_slice())?; + } else { + break; + } + } + } + out.write_all(&[newline_char])?; + Ok(()) +} + +fn cut_fields( + reader: R, + out: &mut W, + ranges: &[Range], + opts: &Options, +) -> UResult<()> { let newline_char = opts.line_ending.into(); let field_opts = opts.field_opts.as_ref().unwrap(); // it is safe to unwrap() here - field_opts will always be Some() for cut_fields() call match field_opts.delimiter { + Delimiter::Slice(delim) if delim == [newline_char] => { + let out_delim = opts.out_delimiter.unwrap_or(delim); + cut_fields_newline_char_delim(reader, out, ranges, newline_char, out_delim) + } Delimiter::Slice(delim) => { let matcher = ExactMatcher::new(delim); match opts.out_delimiter { Some(out_delim) => cut_fields_explicit_out_delim( reader, + out, &matcher, ranges, field_opts.only_delimited, @@ -283,6 +320,7 @@ fn cut_fields(reader: R, ranges: &[Range], opts: &Options) -> UResult<( ), None => cut_fields_implicit_out_delim( reader, + out, &matcher, ranges, field_opts.only_delimited, @@ -294,6 +332,7 @@ fn cut_fields(reader: R, ranges: &[Range], opts: &Options) -> UResult<( let matcher = WhitespaceMatcher {}; cut_fields_explicit_out_delim( reader, + out, &matcher, ranges, field_opts.only_delimited, @@ -304,13 +343,19 @@ fn cut_fields(reader: R, ranges: &[Range], opts: &Options) -> UResult<( } } -fn cut_files(mut filenames: Vec, mode: &Mode) { +fn cut_files(mut filenames: Vec, mode: &Mode) { let mut stdin_read = false; if filenames.is_empty() { - filenames.push("-".to_owned()); + filenames.push(OsString::from("-")); } + let mut out: Box = if stdout().is_terminal() { + Box::new(stdout()) + } else { + Box::new(BufWriter::new(stdout())) as Box + }; + for filename in &filenames { if filename == "-" { if stdin_read { @@ -318,100 +363,84 @@ fn cut_files(mut filenames: Vec, mode: &Mode) { } show_if_err!(match mode { - Mode::Bytes(ref ranges, ref opts) => cut_bytes(stdin(), ranges, opts), - Mode::Characters(ref ranges, ref opts) => cut_bytes(stdin(), ranges, opts), - Mode::Fields(ref ranges, ref opts) => cut_fields(stdin(), ranges, opts), + Mode::Bytes(ranges, opts) => cut_bytes(stdin(), &mut out, ranges, opts), + Mode::Characters(ranges, opts) => cut_bytes(stdin(), &mut out, ranges, opts), + Mode::Fields(ranges, opts) => cut_fields(stdin(), &mut out, ranges, opts), }); stdin_read = true; } else { - let path = Path::new(&filename[..]); + let path = Path::new(filename); if path.is_dir() { - show_error!("{}: Is a directory", filename.maybe_quote()); + show_error!( + "{}: {}", + filename.to_string_lossy().maybe_quote(), + translate!("cut-error-is-directory") + ); set_exit_code(1); continue; } - show_if_err!(File::open(path) - .map_err_context(|| filename.maybe_quote().to_string()) - .and_then(|file| { - match &mode { - Mode::Bytes(ranges, opts) | Mode::Characters(ranges, opts) => { - cut_bytes(file, ranges, opts) + show_if_err!( + File::open(path) + .map_err_context(|| filename.to_string_lossy().to_string()) + .and_then(|file| { + match &mode { + Mode::Bytes(ranges, opts) | Mode::Characters(ranges, opts) => { + cut_bytes(file, &mut out, ranges, opts) + } + Mode::Fields(ranges, opts) => cut_fields(file, &mut out, ranges, opts), } - Mode::Fields(ranges, opts) => cut_fields(file, ranges, opts), - } - })); + }) + ); } } -} -// Helper function for processing delimiter values (which could be non UTF-8) -// It converts OsString to &[u8] for unix targets only -// On non-unix (i.e. Windows) it will just return an error if delimiter value is not UTF-8 -fn os_string_as_bytes(os_string: &OsString) -> UResult<&[u8]> { - #[cfg(unix)] - let bytes = os_string.as_bytes(); - - #[cfg(not(unix))] - let bytes = os_string - .to_str() - .ok_or_else(|| { - uucore::error::UUsageError::new( - 1, - "invalid UTF-8 was detected in one or more arguments", - ) - })? - .as_bytes(); - - Ok(bytes) + show_if_err!( + out.flush() + .map_err_context(|| translate!("cut-error-write-error")) + ); } -// Get delimiter and output delimiter from `-d`/`--delimiter` and `--output-delimiter` options respectively -// Allow either delimiter to have a value that is neither UTF-8 nor ASCII to align with GNU behavior -fn get_delimiters( - matches: &ArgMatches, - delimiter_is_equal: bool, -) -> UResult<(Delimiter, Option<&[u8]>)> { +/// Get delimiter and output delimiter from `-d`/`--delimiter` and `--output-delimiter` options respectively +/// Allow either delimiter to have a value that is neither UTF-8 nor ASCII to align with GNU behavior +fn get_delimiters(matches: &ArgMatches) -> UResult<(Delimiter<'_>, Option<&[u8]>)> { let whitespace_delimited = matches.get_flag(options::WHITESPACE_DELIMITED); let delim_opt = matches.get_one::(options::DELIMITER); let delim = match delim_opt { Some(_) if whitespace_delimited => { return Err(USimpleError::new( 1, - "invalid input: Only one of --delimiter (-d) or -w option can be specified", + translate!("cut-error-delimiter-and-whitespace-conflict"), )); } Some(os_string) => { - // GNU's `cut` supports `-d=` to set the delimiter to `=`. - // Clap parsing is limited in this situation, see: - // https://github.com/uutils/coreutils/issues/2424#issuecomment-863825242 - if delimiter_is_equal { - Delimiter::Slice(b"=") - } else if os_string == "''" || os_string.is_empty() { + if os_string == "''" || os_string.is_empty() { // treat `''` as empty delimiter Delimiter::Slice(b"\0") } else { // For delimiter `-d` option value - allow both UTF-8 (possibly multi-byte) characters // and Non UTF-8 (and not ASCII) single byte "characters", like `b"\xAD"` to align with GNU behavior - let bytes = os_string_as_bytes(os_string)?; + let bytes = os_str_as_bytes(os_string)?; if os_string.to_str().is_some_and(|s| s.chars().count() > 1) || os_string.to_str().is_none() && bytes.len() > 1 { return Err(USimpleError::new( 1, - "the delimiter must be a single character", + translate!("cut-error-delimiter-must-be-single-character"), )); - } else { - Delimiter::from(os_string) } + Delimiter::from(os_string) + } + } + None => { + if whitespace_delimited { + Delimiter::Whitespace + } else { + Delimiter::default() } } - None => match whitespace_delimited { - true => Delimiter::Whitespace, - false => Delimiter::default(), - }, }; let out_delim = matches .get_one::(options::OUTPUT_DELIMITER) @@ -419,7 +448,7 @@ fn get_delimiters( if os_string.is_empty() || os_string == "''" { b"\0" } else { - os_string_as_bytes(os_string).unwrap() + os_str_as_bytes(os_string).unwrap() } }); Ok((delim, out_delim)) @@ -440,15 +469,26 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let args = args.collect::>(); + // GNU's `cut` supports `-d=` to set the delimiter to `=`. + // Clap parsing is limited in this situation, see: + // https://github.com/uutils/coreutils/issues/2424#issuecomment-863825242 + let args: Vec = args + .into_iter() + .map(|x| { + if x == "-d=" { + "--delimiter==".into() + } else { + x + } + }) + .collect(); - let delimiter_is_equal = args.contains(&OsString::from("-d=")); // special case - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let complement = matches.get_flag(options::COMPLEMENT); let only_delimited = matches.get_flag(options::ONLY_DELIMITED); - let (delimiter, out_delimiter) = get_delimiters(&matches, delimiter_is_equal)?; + let (delimiter, out_delimiter) = get_delimiters(&matches)?; let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO_TERMINATED)); // Only one, and only one of cutting mode arguments, i.e. `-b`, `-c`, `-f`, @@ -469,42 +509,50 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { matches.get_one::(options::CHARACTERS), matches.get_one::(options::FIELDS), ) { - (1, Some(byte_ranges), None, None) => list_to_ranges(byte_ranges, complement).map(|ranges| { - Mode::Bytes( - ranges, - Options { - out_delimiter, - line_ending, - field_opts: None, - }, - ) - }), - (1, None, Some(char_ranges), None) => list_to_ranges(char_ranges, complement).map(|ranges| { - Mode::Characters( - ranges, - Options { - out_delimiter, - line_ending, - field_opts: None, - }, - ) - }), - (1, None, None, Some(field_ranges)) => list_to_ranges(field_ranges, complement).map(|ranges| { - Mode::Fields( - ranges, - Options { - out_delimiter, - line_ending, - field_opts: Some(FieldOptions { - only_delimited, - delimiter, - })}, - ) - }), - (2.., _, _, _) => Err( - "invalid usage: expects no more than one of --fields (-f), --chars (-c) or --bytes (-b)".into() - ), - _ => Err("invalid usage: expects one of --fields (-f), --chars (-c) or --bytes (-b)".into()), + (1, Some(byte_ranges), None, None) => { + list_to_ranges(byte_ranges, complement).map(|ranges| { + Mode::Bytes( + ranges, + Options { + out_delimiter, + line_ending, + field_opts: None, + }, + ) + }) + } + + (1, None, Some(char_ranges), None) => { + list_to_ranges(char_ranges, complement).map(|ranges| { + Mode::Characters( + ranges, + Options { + out_delimiter, + line_ending, + field_opts: None, + }, + ) + }) + } + + (1, None, None, Some(field_ranges)) => { + list_to_ranges(field_ranges, complement).map(|ranges| { + Mode::Fields( + ranges, + Options { + out_delimiter, + line_ending, + field_opts: Some(FieldOptions { + delimiter, + only_delimited, + }), + }, + ) + }) + } + + (2.., _, _, _) => Err(translate!("cut-error-multiple-mode-args")), + _ => Err(translate!("cut-error-missing-mode-arg")), }; let mode_parse = match mode_parse { @@ -513,24 +561,24 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Mode::Bytes(_, _) | Mode::Characters(_, _) if matches.contains_id(options::DELIMITER) => { - Err("invalid input: The '--delimiter' ('-d') option only usable if printing a sequence of fields".into()) + Err(translate!("cut-error-delimiter-only-with-fields")) } Mode::Bytes(_, _) | Mode::Characters(_, _) if matches.get_flag(options::WHITESPACE_DELIMITED) => { - Err("invalid input: The '-w' option only usable if printing a sequence of fields".into()) + Err(translate!("cut-error-whitespace-only-with-fields")) } Mode::Bytes(_, _) | Mode::Characters(_, _) if matches.get_flag(options::ONLY_DELIMITED) => { - Err("invalid input: The '--only-delimited' ('-s') option only usable if printing a sequence of fields".into()) + Err(translate!("cut-error-only-delimited-only-with-fields")) } _ => Ok(mode), }, }; - let files: Vec = matches - .get_many::(options::FILE) + let files: Vec = matches + .get_many::(options::FILE) .unwrap_or_default() .cloned() .collect(); @@ -546,10 +594,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) - .override_usage(format_usage(USAGE)) - .about(ABOUT) - .after_help(AFTER_HELP) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) + .override_usage(format_usage(&translate!("cut-usage"))) + .about(translate!("cut-about")) + .after_help(translate!("cut-after-help")) .infer_long_args(true) // While `args_override_self(true)` for some arguments, such as `-d` // and `--output-delimiter`, is consistent to the behavior of GNU cut, @@ -563,7 +612,7 @@ pub fn uu_app() -> Command { Arg::new(options::BYTES) .short('b') .long(options::BYTES) - .help("filter byte columns from the input source") + .help(translate!("cut-help-bytes")) .allow_hyphen_values(true) .value_name("LIST") .action(ArgAction::Append), @@ -572,7 +621,7 @@ pub fn uu_app() -> Command { Arg::new(options::CHARACTERS) .short('c') .long(options::CHARACTERS) - .help("alias for character mode") + .help(translate!("cut-help-characters")) .allow_hyphen_values(true) .value_name("LIST") .action(ArgAction::Append), @@ -582,13 +631,13 @@ pub fn uu_app() -> Command { .short('d') .long(options::DELIMITER) .value_parser(ValueParser::os_string()) - .help("specify the delimiter character that separates fields in the input source. Defaults to Tab.") + .help(translate!("cut-help-delimiter")) .value_name("DELIM"), ) .arg( Arg::new(options::WHITESPACE_DELIMITED) .short('w') - .help("Use any number of whitespace (Space, Tab) to separate fields in the input source (FreeBSD extension).") + .help(translate!("cut-help-whitespace-delimited")) .value_name("WHITESPACE") .action(ArgAction::SetTrue), ) @@ -596,7 +645,7 @@ pub fn uu_app() -> Command { Arg::new(options::FIELDS) .short('f') .long(options::FIELDS) - .help("filter field columns from the input source") + .help(translate!("cut-help-fields")) .allow_hyphen_values(true) .value_name("LIST") .action(ArgAction::Append), @@ -604,34 +653,35 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::COMPLEMENT) .long(options::COMPLEMENT) - .help("invert the filter - instead of displaying only the filtered columns, display all but those columns") + .help(translate!("cut-help-complement")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::ONLY_DELIMITED) .short('s') .long(options::ONLY_DELIMITED) - .help("in field mode, only print lines which contain the delimiter") + .help(translate!("cut-help-only-delimited")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::ZERO_TERMINATED) .short('z') .long(options::ZERO_TERMINATED) - .help("instead of filtering columns based on line, filter columns based on \\0 (NULL character)") + .help(translate!("cut-help-zero-terminated")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::OUTPUT_DELIMITER) .long(options::OUTPUT_DELIMITER) .value_parser(ValueParser::os_string()) - .help("in field mode, replace the delimiter in output lines with this option's argument") + .help(translate!("cut-help-output-delimiter")) .value_name("NEW_DELIM"), ) .arg( Arg::new(options::FILE) - .hide(true) - .action(ArgAction::Append) - .value_hint(clap::ValueHint::FilePath) + .hide(true) + .action(ArgAction::Append) + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) } diff --git a/src/uu/cut/src/matcher.rs b/src/uu/cut/src/matcher.rs index 953e083b139..b4294129442 100644 --- a/src/uu/cut/src/matcher.rs +++ b/src/uu/cut/src/matcher.rs @@ -23,7 +23,7 @@ impl<'a> ExactMatcher<'a> { } } -impl<'a> Matcher for ExactMatcher<'a> { +impl Matcher for ExactMatcher<'_> { fn next_match(&self, haystack: &[u8]) -> Option<(usize, usize)> { let mut pos = 0usize; loop { @@ -34,9 +34,8 @@ impl<'a> Matcher for ExactMatcher<'a> { || haystack[match_idx + 1..].starts_with(&self.needle[1..]) { return Some((match_idx, match_idx + self.needle.len())); - } else { - pos = match_idx + 1; } + pos = match_idx + 1; } None => { return None; diff --git a/src/uu/cut/src/searcher.rs b/src/uu/cut/src/searcher.rs index 21424790eea..dc252d804f7 100644 --- a/src/uu/cut/src/searcher.rs +++ b/src/uu/cut/src/searcher.rs @@ -27,7 +27,7 @@ impl<'a, 'b, M: Matcher> Searcher<'a, 'b, M> { // Iterate over field delimiters // Returns (first, last) positions of each sequence, where `haystack[first..last]` // corresponds to the delimiter. -impl<'a, 'b, M: Matcher> Iterator for Searcher<'a, 'b, M> { +impl Iterator for Searcher<'_, '_, M> { type Item = (usize, usize); fn next(&mut self) -> Option { @@ -61,7 +61,7 @@ mod exact_searcher_tests { let matcher = ExactMatcher::new("a".as_bytes()); let iter = Searcher::new(&matcher, "".as_bytes()); let items: Vec<(usize, usize)> = iter.collect(); - assert_eq!(vec![] as Vec<(usize, usize)>, items); + assert!(items.is_empty()); } fn test_multibyte(line: &[u8], expected: &[(usize, usize)]) { @@ -140,7 +140,7 @@ mod whitespace_searcher_tests { let matcher = WhitespaceMatcher {}; let iter = Searcher::new(&matcher, "".as_bytes()); let items: Vec<(usize, usize)> = iter.collect(); - assert_eq!(vec![] as Vec<(usize, usize)>, items); + assert!(items.is_empty()); } fn test_multispace(line: &[u8], expected: &[(usize, usize)]) { diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index bd7dd254271..40d5e8dda0a 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -1,25 +1,34 @@ -# spell-checker:ignore datetime +# spell-checker:ignore datetime tzdb zoneinfo [package] name = "uu_date" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "date ~ (uutils) display or set the current time" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/date" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/date.rs" [dependencies] -chrono = { workspace = true } clap = { workspace = true } -uucore = { workspace = true } +chrono = { workspace = true } # TODO: Eventually we'll want to remove this +jiff = { workspace = true, features = [ + "tzdb-bundle-platform", + "tzdb-zoneinfo", + "tzdb-concatenated", +] } +uucore = { workspace = true, features = ["parser"] } parse_datetime = { workspace = true } +fluent = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } diff --git a/src/uu/date/date-usage.md b/src/uu/date/date-usage.md deleted file mode 100644 index bf2dc469d3a..00000000000 --- a/src/uu/date/date-usage.md +++ /dev/null @@ -1,87 +0,0 @@ -# `date` usage - - - -FORMAT controls the output. Interpreted sequences are: - -| Sequence | Description | Example | -| -------- | -------------------------------------------------------------------- | ---------------------- | -| %% | a literal % | % | -| %a | locale's abbreviated weekday name | Sun | -| %A | locale's full weekday name | Sunday | -| %b | locale's abbreviated month name | Jan | -| %B | locale's full month name | January | -| %c | locale's date and time | Thu Mar 3 23:05:25 2005| -| %C | century; like %Y, except omit last two digits | 20 | -| %d | day of month | 01 | -| %D | date; same as %m/%d/%y | 12/31/99 | -| %e | day of month, space padded; same as %_d | 3 | -| %F | full date; same as %Y-%m-%d | 2005-03-03 | -| %g | last two digits of year of ISO week number (see %G) | 05 | -| %G | year of ISO week number (see %V); normally useful only with %V | 2005 | -| %h | same as %b | Jan | -| %H | hour (00..23) | 23 | -| %I | hour (01..12) | 11 | -| %j | day of year (001..366) | 062 | -| %k | hour, space padded ( 0..23); same as %_H | 3 | -| %l | hour, space padded ( 1..12); same as %_I | 9 | -| %m | month (01..12) | 03 | -| %M | minute (00..59) | 30 | -| %n | a newline | \n | -| %N | nanoseconds (000000000..999999999) | 123456789 | -| %p | locale's equivalent of either AM or PM; blank if not known | PM | -| %P | like %p, but lower case | pm | -| %q | quarter of year (1..4) | 1 | -| %r | locale's 12-hour clock time | 11:11:04 PM | -| %R | 24-hour hour and minute; same as %H:%M | 23:30 | -| %s | seconds since 1970-01-01 00:00:00 UTC | 1615432800 | -| %S | second (00..60) | 30 | -| %t | a tab | \t | -| %T | time; same as %H:%M:%S | 23:30:30 | -| %u | day of week (1..7); 1 is Monday | 4 | -| %U | week number of year, with Sunday as first day of week (00..53) | 10 | -| %V | ISO week number, with Monday as first day of week (01..53) | 12 | -| %w | day of week (0..6); 0 is Sunday | 4 | -| %W | week number of year, with Monday as first day of week (00..53) | 11 | -| %x | locale's date representation | 03/03/2005 | -| %X | locale's time representation | 23:30:30 | -| %y | last two digits of year (00..99) | 05 | -| %Y | year | 2005 | -| %z | +hhmm numeric time zone | -0400 | -| %:z | +hh:mm numeric time zone | -04:00 | -| %::z | +hh:mm:ss numeric time zone | -04:00:00 | -| %:::z | numeric time zone with : to necessary precision | -04, +05:30 | -| %Z | alphabetic time zone abbreviation | EDT | - -By default, date pads numeric fields with zeroes. -The following optional flags may follow '%': - -* `-` (hyphen) do not pad the field -* `_` (underscore) pad with spaces -* `0` (zero) pad with zeros -* `^` use upper case if possible -* `#` use opposite case if possible - -After any flags comes an optional field width, as a decimal number; -then an optional modifier, which is either -E to use the locale's alternate representations if available, or -O to use the locale's alternate numeric symbols if available. - -Examples: -Convert seconds since the epoch (1970-01-01 UTC) to a date - -``` -date --date='@2147483647' -``` - -Show the time on the west coast of the US (use tzselect(1) to find TZ) - -``` -TZ='America/Los_Angeles' date -``` - -Show the local time for 9AM next Friday on the west coast of the US - -``` -date --date='TZ="America/Los_Angeles" 09:00 next Fri' -``` diff --git a/src/uu/date/date.md b/src/uu/date/date.md deleted file mode 100644 index 97f1340169b..00000000000 --- a/src/uu/date/date.md +++ /dev/null @@ -1,10 +0,0 @@ - - -# date - -``` -date [OPTION]... [+FORMAT]... -date [OPTION]... [MMDDhhmm[[CC]YY][.ss]] -``` - -Print or set the system date and time diff --git a/src/uu/date/locales/en-US.ftl b/src/uu/date/locales/en-US.ftl new file mode 100644 index 00000000000..8ad3a6ec3e6 --- /dev/null +++ b/src/uu/date/locales/en-US.ftl @@ -0,0 +1,104 @@ +date-about = + Print or set the system date and time + +date-usage = + date [OPTION]... [+FORMAT]... + date [OPTION]... [MMDDhhmm[[CC]YY][.ss]] + + FORMAT controls the output. Interpreted sequences are: + { "| Sequence | Description | Example |" } + { "| -------- | -------------------------------------------------------------------- | ---------------------- |" } + { "| %% | a literal % | % |" } + { "| %a | locale's abbreviated weekday name | Sun |" } + { "| %A | locale's full weekday name | Sunday |" } + { "| %b | locale's abbreviated month name | Jan |" } + { "| %B | locale's full month name | January |" } + { "| %c | locale's date and time | Thu Mar 3 23:05:25 2005|" } + { "| %C | century; like %Y, except omit last two digits | 20 |" } + { "| %d | day of month | 01 |" } + { "| %D | date; same as %m/%d/%y | 12/31/99 |" } + { "| %e | day of month, space padded; same as %_d | 3 |" } + { "| %F | full date; same as %Y-%m-%d | 2005-03-03 |" } + { "| %g | last two digits of year of ISO week number (see %G) | 05 |" } + { "| %G | year of ISO week number (see %V); normally useful only with %V | 2005 |" } + { "| %h | same as %b | Jan |" } + { "| %H | hour (00..23) | 23 |" } + { "| %I | hour (01..12) | 11 |" } + { "| %j | day of year (001..366) | 062 |" } + { "| %k | hour, space padded ( 0..23); same as %_H | 3 |" } + { "| %l | hour, space padded ( 1..12); same as %_I | 9 |" } + { "| %m | month (01..12) | 03 |" } + { "| %M | minute (00..59) | 30 |" } + { "| %n | a newline | \\n |" } + { "| %N | nanoseconds (000000000..999999999) | 123456789 |" } + { "| %p | locale's equivalent of either AM or PM; blank if not known | PM |" } + { "| %P | like %p, but lower case | pm |" } + { "| %q | quarter of year (1..4) | 1 |" } + { "| %r | locale's 12-hour clock time | 11:11:04 PM |" } + { "| %R | 24-hour hour and minute; same as %H:%M | 23:30 |" } + { "| %s | seconds since 1970-01-01 00:00:00 UTC | 1615432800 |" } + { "| %S | second (00..60) | 30 |" } + { "| %t | a tab | \\t |" } + { "| %T | time; same as %H:%M:%S | 23:30:30 |" } + { "| %u | day of week (1..7); 1 is Monday | 4 |" } + { "| %U | week number of year, with Sunday as first day of week (00..53) | 10 |" } + { "| %V | ISO week number, with Monday as first day of week (01..53) | 12 |" } + { "| %w | day of week (0..6); 0 is Sunday | 4 |" } + { "| %W | week number of year, with Monday as first day of week (00..53) | 11 |" } + { "| %x | locale's date representation | 03/03/2005 |" } + { "| %X | locale's time representation | 23:30:30 |" } + { "| %y | last two digits of year (00..99) | 05 |" } + { "| %Y | year | 2005 |" } + { "| %z | +hhmm numeric time zone | -0400 |" } + { "| %:z | +hh:mm numeric time zone | -04:00 |" } + { "| %::z | +hh:mm:ss numeric time zone | -04:00:00 |" } + { "| %:::z | numeric time zone with : to necessary precision | -04, +05:30 |" } + { "| %Z | alphabetic time zone abbreviation | EDT |" } + + By default, date pads numeric fields with zeroes. + The following optional flags may follow '%': + { "* `-` (hyphen) do not pad the field" } + { "* `_` (underscore) pad with spaces" } + { "* `0` (zero) pad with zeros" } + { "* `^` use upper case if possible" } + { "* `#` use opposite case if possible" } + After any flags comes an optional field width, as a decimal number; + then an optional modifier, which is either + { "* `E` to use the locale's alternate representations if available, or" } + { "* `O` to use the locale's alternate numeric symbols if available." } + Examples: + Convert seconds since the epoch (1970-01-01 UTC) to a date + + date --date='@2147483647' + + Show the time on the west coast of the US (use tzselect(1) to find TZ) + + TZ='America/Los_Angeles' date + +date-help-date = display time described by STRING, not 'now' +date-help-file = like --date; once for each line of DATEFILE +date-help-iso-8601 = output date/time in ISO 8601 format. + FMT='date' for date only (the default), + 'hours', 'minutes', 'seconds', or 'ns' + for date and time to the indicated precision. + Example: 2006-08-14T02:34:56-06:00 +date-help-rfc-email = output date and time in RFC 5322 format. + Example: Mon, 14 Aug 2006 02:34:56 -0600 +date-help-rfc-3339 = output date/time in RFC 3339 format. + FMT='date', 'seconds', or 'ns' + for date and time to the indicated precision. + Example: 2006-08-14 02:34:56-06:00 +date-help-debug = annotate the parsed date, and warn about questionable usage to stderr +date-help-reference = display the last modification time of FILE +date-help-set = set time described by STRING +date-help-set-macos = set time described by STRING (not available on mac yet) +date-help-set-redox = set time described by STRING (not available on redox yet) +date-help-universal = print or set Coordinated Universal Time (UTC) + +date-error-invalid-date = invalid date '{$date}' +date-error-invalid-format = invalid format '{$format}' ({$error}) +date-error-expected-file-got-directory = expected file, got directory '{$path}' +date-error-date-overflow = date overflow '{$date}' +date-error-setting-date-not-supported-macos = setting the date is not supported by macOS +date-error-setting-date-not-supported-redox = setting the date is not supported by Redox +date-error-cannot-set-date = cannot set date diff --git a/src/uu/date/locales/fr-FR.ftl b/src/uu/date/locales/fr-FR.ftl new file mode 100644 index 00000000000..204121f9218 --- /dev/null +++ b/src/uu/date/locales/fr-FR.ftl @@ -0,0 +1,101 @@ +date-about = afficher ou définir la date système +date-usage = [OPTION]... [+FORMAT] + date [-u|--utc|--universal] [MMDDhhmm[[CC]YY][.ss]] + + FORMAT contrôle la sortie. Les séquences interprétées sont : + { "| Séquence | Description | Exemple |" } + { "| -------- | -------------------------------------------------------------- | ---------------------- |" } + { "| %% | un % littéral | % |" } + { "| %a | nom abrégé du jour de la semaine selon la locale | dim |" } + { "| %A | nom complet du jour de la semaine selon la locale | dimanche |" } + { "| %b | nom abrégé du mois selon la locale | jan |" } + { "| %B | nom complet du mois selon la locale | janvier |" } + { "| %c | date et heure selon la locale | jeu 3 mar 23:05:25 2005|" } + { "| %C | siècle ; comme %Y, sauf qu'on omet les deux derniers chiffres | 20 |" } + { "| %d | jour du mois | 01 |" } + { "| %D | date ; identique à %m/%d/%y | 12/31/99 |" } + { "| %e | jour du mois, rempli avec des espaces ; identique à %_d | 3 |" } + { "| %F | date complète ; identique à %Y-%m-%d | 2005-03-03 |" } + { "| %g | deux derniers chiffres de l'année du numéro de semaine ISO (voir %G) | 05 |" } + { "| %G | année du numéro de semaine ISO (voir %V) ; normalement utile seulement avec %V | 2005 |" } + { "| %h | identique à %b | jan |" } + { "| %H | heure (00..23) | 23 |" } + { "| %I | heure (01..12) | 11 |" } + { "| %j | jour de l'année (001..366) | 062 |" } + { "| %k | heure, remplie avec des espaces ( 0..23) ; identique à %_H | 3 |" } + { "| %l | heure, remplie avec des espaces ( 1..12) ; identique à %_I | 9 |" } + { "| %m | mois (01..12) | 03 |" } + { "| %M | minute (00..59) | 30 |" } + { "| %n | une nouvelle ligne | \\n |" } + { "| %N | nanosecondes (000000000..999999999) | 123456789 |" } + { "| %p | équivalent locale de AM ou PM ; vide si inconnu | PM |" } + { "| %P | comme %p, mais en minuscules | pm |" } + { "| %q | trimestre de l'année (1..4) | 1 |" } + { "| %r | heure sur 12 heures selon la locale | 11:11:04 PM |" } + { "| %R | heure sur 24 heures et minute ; identique à %H:%M | 23:30 |" } + { "| %s | secondes depuis 1970-01-01 00:00:00 UTC | 1615432800 |" } + { "| %S | seconde (00..60) | 30 |" } + { "| %t | une tabulation | \\t |" } + { "| %T | heure ; identique à %H:%M:%S | 23:30:30 |" } + { "| %u | jour de la semaine (1..7) ; 1 est lundi | 4 |" } + { "| %U | numéro de semaine de l'année, avec dimanche comme premier jour de la semaine (00..53) | 10 |" } + { "| %V | numéro de semaine ISO, avec lundi comme premier jour de la semaine (01..53) | 12 |" } + { "| %w | jour de la semaine (0..6) ; 0 est dimanche | 4 |" } + { "| %W | numéro de semaine de l'année, avec lundi comme premier jour de la semaine (00..53) | 11 |" } + { "| %x | représentation de la date selon la locale | 03/03/2005 |" } + { "| %X | représentation de l'heure selon la locale | 23:30:30 |" } + { "| %y | deux derniers chiffres de l'année (00..99) | 05 |" } + { "| %Y | année | 2005 |" } + { "| %z | fuseau horaire numérique +hhmm | -0400 |" } + { "| %:z | fuseau horaire numérique +hh:mm | -04:00 |" } + { "| %::z | fuseau horaire numérique +hh:mm:ss | -04:00:00 |" } + { "| %:::z | fuseau horaire numérique avec : à la précision nécessaire | -04, +05:30 |" } + { "| %Z | abréviation alphabétique du fuseau horaire | EDT |" } + + Par défaut, date remplit les champs numériques avec des zéros. + Les indicateurs optionnels suivants peuvent suivre '%' : + { "* `-` (tiret) ne pas remplir le champ" } + { "* `_` (soulignement) remplir avec des espaces" } + { "* `0` (zéro) remplir avec des zéros" } + { "* `^` utiliser des majuscules si possible" } + { "* `#` utiliser l'inverse si possible" } + Après tout indicateur vient une largeur de champ optionnelle, comme nombre décimal ; + puis un modificateur optionnel, qui est soit + { "* `E` pour utiliser les représentations alternatives de la locale si disponibles, ou" } + { "* `O` pour utiliser les symboles numériques alternatifs de la locale si disponibles." } + Exemples : + Convertir les secondes depuis l'époque (1970-01-01 UTC) en date + + date --date='@2147483647' + + Montrer l'heure sur la côte ouest des États-Unis (utiliser tzselect(1) pour trouver TZ) + + TZ='America/Los_Angeles' date + +date-help-date = afficher l'heure décrite par CHAÃŽNE, pas 'maintenant' +date-help-file = comme --date ; une fois pour chaque ligne de FICHIER_DATE +date-help-iso-8601 = afficher la date/heure au format ISO 8601. + FMT='date' pour la date seulement (par défaut), + 'hours', 'minutes', 'seconds', ou 'ns' + pour la date et l'heure à la précision indiquée. + Exemple : 2006-08-14T02:34:56-06:00 +date-help-rfc-email = afficher la date et l'heure au format RFC 5322. + Exemple : Mon, 14 Aug 2006 02:34:56 -0600 +date-help-rfc-3339 = afficher la date/heure au format RFC 3339. + FMT='date', 'seconds', ou 'ns' + pour la date et l'heure à la précision indiquée. + Exemple : 2006-08-14 02:34:56-06:00 +date-help-debug = annoter la date analysée et avertir des usages douteux sur stderr +date-help-reference = afficher l'heure de dernière modification du FICHIER +date-help-set = définir l'heure décrite par CHAÃŽNE +date-help-set-macos = définir l'heure décrite par CHAÃŽNE (pas encore disponible sur mac) +date-help-set-redox = définir l'heure décrite par CHAÃŽNE (pas encore disponible sur redox) +date-help-universal = afficher ou définir le Temps Universel Coordonné (UTC) + +date-error-invalid-date = date invalide '{$date}' +date-error-invalid-format = format invalide '{$format}' ({$error}) +date-error-expected-file-got-directory = fichier attendu, répertoire obtenu '{$path}' +date-error-date-overflow = débordement de date '{$date}' +date-error-setting-date-not-supported-macos = la définition de la date n'est pas prise en charge par macOS +date-error-setting-date-not-supported-redox = la définition de la date n'est pas prise en charge par Redox +date-error-cannot-set-date = impossible de définir la date diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index bc50a8a2c58..1685844d9c2 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -3,26 +3,26 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (chrono) Datelike Timelike ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes +// spell-checker:ignore strtime ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes -use chrono::format::{Item, StrftimeItems}; -use chrono::{DateTime, FixedOffset, Local, Offset, TimeDelta, Utc}; -#[cfg(windows)] -use chrono::{Datelike, Timelike}; -use clap::{crate_version, Arg, ArgAction, Command}; +use clap::{Arg, ArgAction, Command}; +use jiff::fmt::strtime; +use jiff::tz::TimeZone; +use jiff::{SignedDuration, Timestamp, Zoned}; #[cfg(all(unix, not(target_os = "macos"), not(target_os = "redox")))] -use libc::{clock_settime, timespec, CLOCK_REALTIME}; +use libc::{CLOCK_REALTIME, clock_settime, timespec}; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; -use uucore::display::Quotable; use uucore::error::FromIo; use uucore::error::{UResult, USimpleError}; -use uucore::{format_usage, help_about, help_usage, show}; +use uucore::translate; +use uucore::{format_usage, show}; #[cfg(windows)] use windows_sys::Win32::{Foundation::SYSTEMTIME, System::SystemInformation::SetSystemTime}; -use uucore::shortcut_value_parser::ShortcutValueParser; +use uucore::LocalizedCommand; +use uucore::parser::shortcut_value_parser::ShortcutValueParser; // Options const DATE: &str = "date"; @@ -31,9 +31,6 @@ const MINUTES: &str = "minutes"; const SECONDS: &str = "seconds"; const NS: &str = "ns"; -const ABOUT: &str = help_about!("date.md"); -const USAGE: &str = help_usage!("date.md"); - const OPT_DATE: &str = "date"; const OPT_FORMAT: &str = "format"; const OPT_FILE: &str = "file"; @@ -46,35 +43,12 @@ const OPT_REFERENCE: &str = "reference"; const OPT_UNIVERSAL: &str = "universal"; const OPT_UNIVERSAL_2: &str = "utc"; -// Help strings - -static ISO_8601_HELP_STRING: &str = "output date/time in ISO 8601 format. - FMT='date' for date only (the default), - 'hours', 'minutes', 'seconds', or 'ns' - for date and time to the indicated precision. - Example: 2006-08-14T02:34:56-06:00"; - -static RFC_5322_HELP_STRING: &str = "output date and time in RFC 5322 format. - Example: Mon, 14 Aug 2006 02:34:56 -0600"; - -static RFC_3339_HELP_STRING: &str = "output date/time in RFC 3339 format. - FMT='date', 'seconds', or 'ns' - for date and time to the indicated precision. - Example: 2006-08-14 02:34:56-06:00"; - -#[cfg(not(any(target_os = "macos", target_os = "redox")))] -static OPT_SET_HELP_STRING: &str = "set time described by STRING"; -#[cfg(target_os = "macos")] -static OPT_SET_HELP_STRING: &str = "set time described by STRING (not available on mac yet)"; -#[cfg(target_os = "redox")] -static OPT_SET_HELP_STRING: &str = "set time described by STRING (not available on redox yet)"; - /// Settings for this program, parsed from the command line struct Settings { utc: bool, format: Format, date_source: DateSource, - set_to: Option>, + set_to: Option, } /// Various ways of displaying the date @@ -91,7 +65,8 @@ enum DateSource { Now, Custom(String), File(PathBuf), - Human(TimeDelta), + Stdin, + Human(SignedDuration), } enum Iso8601Format { @@ -102,7 +77,7 @@ enum Iso8601Format { Ns, } -impl<'a> From<&'a str> for Iso8601Format { +impl From<&str> for Iso8601Format { fn from(s: &str) -> Self { match s { HOURS => Self::Hours, @@ -122,7 +97,7 @@ enum Rfc3339Format { Ns, } -impl<'a> From<&'a str> for Rfc3339Format { +impl From<&str> for Rfc3339Format { fn from(s: &str) -> Self { match s { DATE => Self::Date, @@ -137,13 +112,13 @@ impl<'a> From<&'a str> for Rfc3339Format { #[uucore::main] #[allow(clippy::cognitive_complexity)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let format = if let Some(form) = matches.get_one::(OPT_FORMAT) { if !form.starts_with('+') { return Err(USimpleError::new( 1, - format!("invalid date {}", form.quote()), + translate!("date-error-invalid-date", "date" => form), )); } let form = form[1..].to_string(); @@ -165,15 +140,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let date_source = if let Some(date) = matches.get_one::(OPT_DATE) { - let ref_time = Local::now(); - if let Ok(new_time) = parse_datetime::parse_datetime_at_date(ref_time, date.as_str()) { - let duration = new_time.signed_duration_since(ref_time); + if let Ok(duration) = parse_offset(date.as_str()) { DateSource::Human(duration) } else { DateSource::Custom(date.into()) } } else if let Some(file) = matches.get_one::(OPT_FILE) { - DateSource::File(file.into()) + match file.as_ref() { + "-" => DateSource::Stdin, + _ => DateSource::File(file.into()), + } } else { DateSource::Now }; @@ -183,7 +159,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Some(Err((input, _err))) => { return Err(USimpleError::new( 1, - format!("invalid date {}", input.quote()), + translate!("date-error-invalid-date", "date" => input), )); } Some(Ok(date)) => Some(date), @@ -198,102 +174,88 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if let Some(date) = settings.set_to { // All set time functions expect UTC datetimes. - let date: DateTime = if settings.utc { - date.with_timezone(&Utc) + let date = if settings.utc { + date.with_time_zone(TimeZone::UTC) } else { - date.into() + date }; return set_system_datetime(date); + } + + // Get the current time, either in the local time zone or UTC. + let now = if settings.utc { + Timestamp::now().to_zoned(TimeZone::UTC) } else { - // Get the current time, either in the local time zone or UTC. - let now: DateTime = if settings.utc { - let now = Utc::now(); - now.with_timezone(&now.offset().fix()) - } else { - let now = Local::now(); - now.with_timezone(now.offset()) - }; + Zoned::now() + }; - // Iterate over all dates - whether it's a single date or a file. - let dates: Box> = match settings.date_source { - DateSource::Custom(ref input) => { - let date = parse_date(input.clone()); - let iter = std::iter::once(date); - Box::new(iter) - } - DateSource::Human(relative_time) => { - // Get the current DateTime for things like "1 year ago" - let current_time = DateTime::::from(Local::now()); - // double check the result is overflow or not of the current_time + relative_time - // it may cause a panic of chrono::datetime::DateTime add - match current_time.checked_add_signed(relative_time) { - Some(date) => { - let iter = std::iter::once(Ok(date)); - Box::new(iter) - } - None => { - return Err(USimpleError::new( - 1, - format!("invalid date {}", relative_time), - )); - } + // Iterate over all dates - whether it's a single date or a file. + let dates: Box> = match settings.date_source { + DateSource::Custom(ref input) => { + let date = parse_date(input); + let iter = std::iter::once(date); + Box::new(iter) + } + DateSource::Human(relative_time) => { + // Double check the result is overflow or not of the current_time + relative_time + // it may cause a panic of chrono::datetime::DateTime add + match now.checked_add(relative_time) { + Ok(date) => { + let iter = std::iter::once(Ok(date)); + Box::new(iter) } - } - DateSource::File(ref path) => { - if path.is_dir() { + Err(_) => { return Err(USimpleError::new( - 2, - format!("expected file, got directory {}", path.quote()), + 1, + translate!("date-error-date-overflow", "date" => relative_time), )); } - let file = File::open(path) - .map_err_context(|| path.as_os_str().to_string_lossy().to_string())?; - let lines = BufReader::new(file).lines(); - let iter = lines.map_while(Result::ok).map(parse_date); - Box::new(iter) } - DateSource::Now => { - let iter = std::iter::once(Ok(now)); - Box::new(iter) + } + DateSource::Stdin => { + let lines = BufReader::new(std::io::stdin()).lines(); + let iter = lines.map_while(Result::ok).map(parse_date); + Box::new(iter) + } + DateSource::File(ref path) => { + if path.is_dir() { + return Err(USimpleError::new( + 2, + translate!("date-error-expected-file-got-directory", "path" => path.to_string_lossy()), + )); } - }; + let file = File::open(path) + .map_err_context(|| path.as_os_str().to_string_lossy().to_string())?; + let lines = BufReader::new(file).lines(); + let iter = lines.map_while(Result::ok).map(parse_date); + Box::new(iter) + } + DateSource::Now => { + let iter = std::iter::once(Ok(now)); + Box::new(iter) + } + }; - let format_string = make_format_string(&settings); + let format_string = make_format_string(&settings); - // Format all the dates - for date in dates { - match date { - Ok(date) => { - // GNU `date` uses `%N` for nano seconds, however crate::chrono uses `%f` - let format_string = &format_string.replace("%N", "%f"); - // Refuse to pass this string to chrono as it is crashing in this crate - if format_string.contains("%#z") { - return Err(USimpleError::new( - 1, - format!("invalid format {}", format_string.replace("%f", "%N")), - )); - } - // Hack to work around panic in chrono, - // TODO - remove when a fix for https://github.com/chronotope/chrono/issues/623 is released - let format_items = StrftimeItems::new(format_string); - if format_items.clone().any(|i| i == Item::Error) { - return Err(USimpleError::new( - 1, - format!("invalid format {}", format_string.replace("%f", "%N")), - )); - } - let formatted = date - .format_with_items(format_items) - .to_string() - .replace("%f", "%N"); - println!("{formatted}"); + // Format all the dates + for date in dates { + match date { + // TODO: Switch to lenient formatting. + Ok(date) => match strtime::format(format_string, &date) { + Ok(s) => println!("{s}"), + Err(e) => { + return Err(USimpleError::new( + 1, + translate!("date-error-invalid-format", "format" => format_string, "error" => e), + )); } - Err((input, _err)) => show!(USimpleError::new( - 1, - format!("invalid date {}", input.quote()) - )), - } + }, + Err((input, _err)) => show!(USimpleError::new( + 1, + translate!("date-error-invalid-date", "date" => input) + )), } } @@ -302,16 +264,18 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) + .about(translate!("date-about")) + .override_usage(format_usage(&translate!("date-usage"))) .infer_long_args(true) .arg( Arg::new(OPT_DATE) .short('d') .long(OPT_DATE) .value_name("STRING") - .help("display time described by STRING, not 'now'"), + .allow_hyphen_values(true) + .help(translate!("date-help-date")), ) .arg( Arg::new(OPT_FILE) @@ -319,7 +283,7 @@ pub fn uu_app() -> Command { .long(OPT_FILE) .value_name("DATEFILE") .value_hint(clap::ValueHint::FilePath) - .help("like --date; once for each line of DATEFILE"), + .help(translate!("date-help-file")), ) .arg( Arg::new(OPT_ISO_8601) @@ -331,13 +295,13 @@ pub fn uu_app() -> Command { ])) .num_args(0..=1) .default_missing_value(OPT_DATE) - .help(ISO_8601_HELP_STRING), + .help(translate!("date-help-iso-8601")), ) .arg( Arg::new(OPT_RFC_EMAIL) .short('R') .long(OPT_RFC_EMAIL) - .help(RFC_5322_HELP_STRING) + .help(translate!("date-help-rfc-email")) .action(ArgAction::SetTrue), ) .arg( @@ -345,12 +309,12 @@ pub fn uu_app() -> Command { .long(OPT_RFC_3339) .value_name("FMT") .value_parser(ShortcutValueParser::new([DATE, SECONDS, NS])) - .help(RFC_3339_HELP_STRING), + .help(translate!("date-help-rfc-3339")), ) .arg( Arg::new(OPT_DEBUG) .long(OPT_DEBUG) - .help("annotate the parsed date, and warn about questionable usage to stderr") + .help(translate!("date-help-debug")) .action(ArgAction::SetTrue), ) .arg( @@ -359,21 +323,34 @@ pub fn uu_app() -> Command { .long(OPT_REFERENCE) .value_name("FILE") .value_hint(clap::ValueHint::AnyPath) - .help("display the last modification time of FILE"), + .help(translate!("date-help-reference")), ) .arg( Arg::new(OPT_SET) .short('s') .long(OPT_SET) .value_name("STRING") - .help(OPT_SET_HELP_STRING), + .help({ + #[cfg(not(any(target_os = "macos", target_os = "redox")))] + { + translate!("date-help-set") + } + #[cfg(target_os = "macos")] + { + translate!("date-help-set-macos") + } + #[cfg(target_os = "redox")] + { + translate!("date-help-set-redox") + } + }), ) .arg( Arg::new(OPT_UNIVERSAL) .short('u') .long(OPT_UNIVERSAL) .alias(OPT_UNIVERSAL_2) - .help("print or set Coordinated Universal Time (UTC)") + .help(translate!("date-help-universal")) .action(ArgAction::SetTrue), ) .arg(Arg::new(OPT_FORMAT)) @@ -387,46 +364,69 @@ fn make_format_string(settings: &Settings) -> &str { Iso8601Format::Hours => "%FT%H%:z", Iso8601Format::Minutes => "%FT%H:%M%:z", Iso8601Format::Seconds => "%FT%T%:z", - Iso8601Format::Ns => "%FT%T,%f%:z", + Iso8601Format::Ns => "%FT%T,%N%:z", }, Format::Rfc5322 => "%a, %d %h %Y %T %z", Format::Rfc3339(ref fmt) => match *fmt { Rfc3339Format::Date => "%F", Rfc3339Format::Seconds => "%F %T%:z", - Rfc3339Format::Ns => "%F %T.%f%:z", + Rfc3339Format::Ns => "%F %T.%N%:z", }, Format::Custom(ref fmt) => fmt, - Format::Default => "%c", + Format::Default => "%a %b %e %X %Z %Y", } } /// Parse a `String` into a `DateTime`. /// If it fails, return a tuple of the `String` along with its `ParseError`. +// TODO: Convert `parse_datetime` to jiff and remove wrapper from chrono to jiff structures. fn parse_date + Clone>( s: S, -) -> Result, (String, chrono::format::ParseError)> { - // TODO: The GNU date command can parse a wide variety of inputs. - s.as_ref().parse().map_err(|e| (s.as_ref().into(), e)) +) -> Result { + match parse_datetime::parse_datetime(s.as_ref()) { + Ok(date) => { + let timestamp = + Timestamp::new(date.timestamp(), date.timestamp_subsec_nanos() as i32).unwrap(); + Ok(Zoned::new(timestamp, TimeZone::UTC)) + } + Err(e) => Err((s.as_ref().into(), e)), + } +} + +// TODO: Convert `parse_datetime` to jiff and remove wrapper from chrono to jiff structures. +// Also, consider whether parse_datetime::parse_datetime_at_date can be renamed to something +// like parse_datetime::parse_offset, instead of doing some addition/subtraction. +fn parse_offset(date: &str) -> Result { + let ref_time = chrono::Local::now(); + if let Ok(new_time) = parse_datetime::parse_datetime_at_date(ref_time, date) { + let duration = new_time.signed_duration_since(ref_time); + Ok(SignedDuration::new( + duration.num_seconds(), + duration.subsec_nanos(), + )) + } else { + Err(()) + } } #[cfg(not(any(unix, windows)))] -fn set_system_datetime(_date: DateTime) -> UResult<()> { +fn set_system_datetime(_date: Zoned) -> UResult<()> { unimplemented!("setting date not implemented (unsupported target)"); } #[cfg(target_os = "macos")] -fn set_system_datetime(_date: DateTime) -> UResult<()> { +fn set_system_datetime(_date: Zoned) -> UResult<()> { Err(USimpleError::new( 1, - "setting the date is not supported by macOS".to_string(), + translate!("date-error-setting-date-not-supported-macos"), )) } #[cfg(target_os = "redox")] -fn set_system_datetime(_date: DateTime) -> UResult<()> { +fn set_system_datetime(_date: Zoned) -> UResult<()> { Err(USimpleError::new( 1, - "setting the date is not supported by Redox".to_string(), + translate!("date-error-setting-date-not-supported-redox"), )) } @@ -436,27 +436,29 @@ fn set_system_datetime(_date: DateTime) -> UResult<()> { /// `` /// `` /// `` -fn set_system_datetime(date: DateTime) -> UResult<()> { +fn set_system_datetime(date: Zoned) -> UResult<()> { + let ts = date.timestamp(); let timespec = timespec { - tv_sec: date.timestamp() as _, - tv_nsec: date.timestamp_subsec_nanos() as _, + tv_sec: ts.as_second() as _, + tv_nsec: ts.subsec_nanosecond() as _, }; - let result = unsafe { clock_settime(CLOCK_REALTIME, ×pec) }; + let result = unsafe { clock_settime(CLOCK_REALTIME, &raw const timespec) }; if result == 0 { Ok(()) } else { - Err(std::io::Error::last_os_error().map_err_context(|| "cannot set date".to_string())) + Err(std::io::Error::last_os_error() + .map_err_context(|| translate!("date-error-cannot-set-date"))) } } #[cfg(windows)] /// System call to set date (Windows). /// See here for more: -/// https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-setsystemtime -/// https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime -fn set_system_datetime(date: DateTime) -> UResult<()> { +/// * +/// * +fn set_system_datetime(date: Zoned) -> UResult<()> { let system_time = SYSTEMTIME { wYear: date.year() as u16, wMonth: date.month() as u16, @@ -467,13 +469,14 @@ fn set_system_datetime(date: DateTime) -> UResult<()> { wMinute: date.minute() as u16, wSecond: date.second() as u16, // TODO: be careful of leap seconds - valid range is [0, 999] - how to handle? - wMilliseconds: ((date.nanosecond() / 1_000_000) % 1000) as u16, + wMilliseconds: ((date.subsec_nanosecond() / 1_000_000) % 1000) as u16, }; - let result = unsafe { SetSystemTime(&system_time) }; + let result = unsafe { SetSystemTime(&raw const system_time) }; if result == 0 { - Err(std::io::Error::last_os_error().map_err_context(|| "cannot set date".to_string())) + Err(std::io::Error::last_os_error() + .map_err_context(|| translate!("date-error-cannot-set-date"))) } else { Ok(()) } diff --git a/src/uu/dd/Cargo.toml b/src/uu/dd/Cargo.toml index 2e32722f6fc..4633bc06b72 100644 --- a/src/uu/dd/Cargo.toml +++ b/src/uu/dd/Cargo.toml @@ -1,15 +1,18 @@ [package] name = "uu_dd" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "dd ~ (uutils) copy and convert files" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/dd" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/dd.rs" @@ -18,7 +21,14 @@ path = "src/dd.rs" clap = { workspace = true } gcd = { workspace = true } libc = { workspace = true } -uucore = { workspace = true, features = ["format", "quoting-style"] } +uucore = { workspace = true, features = [ + "format", + "parser", + "quoting-style", + "fs", +] } +thiserror = { workspace = true } +fluent = { workspace = true } [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] signal-hook = { workspace = true } diff --git a/src/uu/dd/dd.md b/src/uu/dd/dd.md deleted file mode 100644 index 504910884c2..00000000000 --- a/src/uu/dd/dd.md +++ /dev/null @@ -1,126 +0,0 @@ -# dd - - - -``` -dd [OPERAND]... -dd OPTION -``` - -Copy, and optionally convert, a file system resource - -## After Help - -### Operands - -- `bs=BYTES` : read and write up to BYTES bytes at a time (default: 512); - overwrites `ibs` and `obs`. -- `cbs=BYTES` : the 'conversion block size' in bytes. Applies to the - `conv=block`, and `conv=unblock` operations. -- `conv=CONVS` : a comma-separated list of conversion options or (for legacy - reasons) file flags. -- `count=N` : stop reading input after N ibs-sized read operations rather - than proceeding until EOF. See `iflag=count_bytes` if stopping after N bytes - is preferred -- `ibs=N` : the size of buffer used for reads (default: 512) -- `if=FILE` : the file used for input. When not specified, stdin is used instead -- `iflag=FLAGS` : a comma-separated list of input flags which specify how the - input source is treated. FLAGS may be any of the input-flags or general-flags - specified below. -- `skip=N` (or `iseek=N`) : skip N ibs-sized records into input before beginning - copy/convert operations. See iflag=seek_bytes if seeking N bytes is preferred. -- `obs=N` : the size of buffer used for writes (default: 512) -- `of=FILE` : the file used for output. When not specified, stdout is used - instead -- `oflag=FLAGS` : comma separated list of output flags which specify how the - output source is treated. FLAGS may be any of the output flags or general - flags specified below -- `seek=N` (or `oseek=N`) : seeks N obs-sized records into output before - beginning copy/convert operations. See oflag=seek_bytes if seeking N bytes is - preferred -- `status=LEVEL` : controls whether volume and performance stats are written to - stderr. - - When unspecified, dd will print stats upon completion. An example is below. - - ```plain - 6+0 records in - 16+0 records out - 8192 bytes (8.2 kB, 8.0 KiB) copied, 0.00057009 s, - 14.4 MB/s - ``` - - The first two lines are the 'volume' stats and the final line is the - 'performance' stats. - The volume stats indicate the number of complete and partial ibs-sized reads, - or obs-sized writes that took place during the copy. The format of the volume - stats is `+`. If records have been truncated (see - `conv=block`), the volume stats will contain the number of truncated records. - - Possible LEVEL values are: - - `progress` : Print periodic performance stats as the copy proceeds. - - `noxfer` : Print final volume stats, but not performance stats. - - `none` : Do not print any stats. - - Printing performance stats is also triggered by the INFO signal (where supported), - or the USR1 signal. Setting the POSIXLY_CORRECT environment variable to any value - (including an empty value) will cause the USR1 signal to be ignored. - -### Conversion Options - -- `ascii` : convert from EBCDIC to ASCII. This is the inverse of the `ebcdic` - option. Implies `conv=unblock`. -- `ebcdic` : convert from ASCII to EBCDIC. This is the inverse of the `ascii` - option. Implies `conv=block`. -- `ibm` : convert from ASCII to EBCDIC, applying the conventions for `[`, `]` - and `~` specified in POSIX. Implies `conv=block`. - -- `ucase` : convert from lower-case to upper-case. -- `lcase` : converts from upper-case to lower-case. - -- `block` : for each newline less than the size indicated by cbs=BYTES, remove - the newline and pad with spaces up to cbs. Lines longer than cbs are truncated. -- `unblock` : for each block of input of the size indicated by cbs=BYTES, remove - right-trailing spaces and replace with a newline character. - -- `sparse` : attempts to seek the output when an obs-sized block consists of - only zeros. -- `swab` : swaps each adjacent pair of bytes. If an odd number of bytes is - present, the final byte is omitted. -- `sync` : pad each ibs-sided block with zeros. If `block` or `unblock` is - specified, pad with spaces instead. -- `excl` : the output file must be created. Fail if the output file is already - present. -- `nocreat` : the output file will not be created. Fail if the output file in - not already present. -- `notrunc` : the output file will not be truncated. If this option is not - present, output will be truncated when opened. -- `noerror` : all read errors will be ignored. If this option is not present, - dd will only ignore Error::Interrupted. -- `fdatasync` : data will be written before finishing. -- `fsync` : data and metadata will be written before finishing. - -### Input flags - -- `count_bytes` : a value to `count=N` will be interpreted as bytes. -- `skip_bytes` : a value to `skip=N` will be interpreted as bytes. -- `fullblock` : wait for ibs bytes from each read. zero-length reads are still - considered EOF. - -### Output flags - -- `append` : open file in append mode. Consider setting conv=notrunc as well. -- `seek_bytes` : a value to seek=N will be interpreted as bytes. - -### General Flags - -- `direct` : use direct I/O for data. -- `directory` : fail unless the given input (if used as an iflag) or - output (if used as an oflag) is a directory. -- `dsync` : use synchronized I/O for data. -- `sync` : use synchronized I/O for data and metadata. -- `nonblock` : use non-blocking I/O. -- `noatime` : do not update access time. -- `nocache` : request that OS drop cache. -- `noctty` : do not assign a controlling tty. -- `nofollow` : do not follow system links. diff --git a/src/uu/dd/locales/en-US.ftl b/src/uu/dd/locales/en-US.ftl new file mode 100644 index 00000000000..8a21f1b595f --- /dev/null +++ b/src/uu/dd/locales/en-US.ftl @@ -0,0 +1,160 @@ +dd-about = Copy, and optionally convert, a file system resource +dd-usage = dd [OPERAND]... + dd OPTION +dd-after-help = ### Operands + + - bs=BYTES : read and write up to BYTES bytes at a time (default: 512); + overwrites ibs and obs. + - cbs=BYTES : the 'conversion block size' in bytes. Applies to the + conv=block, and conv=unblock operations. + - conv=CONVS : a comma-separated list of conversion options or (for legacy + reasons) file flags. + - count=N : stop reading input after N ibs-sized read operations rather + than proceeding until EOF. See iflag=count_bytes if stopping after N bytes + is preferred + - ibs=N : the size of buffer used for reads (default: 512) + - if=FILE : the file used for input. When not specified, stdin is used instead + - iflag=FLAGS : a comma-separated list of input flags which specify how the + input source is treated. FLAGS may be any of the input-flags or general-flags + specified below. + - skip=N (or iseek=N) : skip N ibs-sized records into input before beginning + copy/convert operations. See iflag=seek_bytes if seeking N bytes is preferred. + - obs=N : the size of buffer used for writes (default: 512) + - of=FILE : the file used for output. When not specified, stdout is used + instead + - oflag=FLAGS : comma separated list of output flags which specify how the + output source is treated. FLAGS may be any of the output flags or general + flags specified below + - seek=N (or oseek=N) : seeks N obs-sized records into output before + beginning copy/convert operations. See oflag=seek_bytes if seeking N bytes is + preferred + - status=LEVEL : controls whether volume and performance stats are written to + stderr. + + When unspecified, dd will print stats upon completion. An example is below. + + ```plain + 6+0 records in + 16+0 records out + 8192 bytes (8.2 kB, 8.0 KiB) copied, 0.00057009 s, + 14.4 MB/s + + The first two lines are the 'volume' stats and the final line is the + 'performance' stats. + The volume stats indicate the number of complete and partial ibs-sized reads, + or obs-sized writes that took place during the copy. The format of the volume + stats is +. If records have been truncated (see + conv=block), the volume stats will contain the number of truncated records. + + Possible LEVEL values are: + - progress : Print periodic performance stats as the copy proceeds. + - noxfer : Print final volume stats, but not performance stats. + - none : Do not print any stats. + + Printing performance stats is also triggered by the INFO signal (where supported), + or the USR1 signal. Setting the POSIXLY_CORRECT environment variable to any value + (including an empty value) will cause the USR1 signal to be ignored. + + ### Conversion Options + + - ascii : convert from EBCDIC to ASCII. This is the inverse of the ebcdic + option. Implies conv=unblock. + - ebcdic : convert from ASCII to EBCDIC. This is the inverse of the ascii + option. Implies conv=block. + - ibm : convert from ASCII to EBCDIC, applying the conventions for [, ] + and ~ specified in POSIX. Implies conv=block. + + - ucase : convert from lower-case to upper-case. + - lcase : converts from upper-case to lower-case. + + - block : for each newline less than the size indicated by cbs=BYTES, remove + the newline and pad with spaces up to cbs. Lines longer than cbs are truncated. + - unblock : for each block of input of the size indicated by cbs=BYTES, remove + right-trailing spaces and replace with a newline character. + + - sparse : attempts to seek the output when an obs-sized block consists of + only zeros. + - swab : swaps each adjacent pair of bytes. If an odd number of bytes is + present, the final byte is omitted. + - sync : pad each ibs-sided block with zeros. If block or unblock is + specified, pad with spaces instead. + - excl : the output file must be created. Fail if the output file is already + present. + - nocreat : the output file will not be created. Fail if the output file in + not already present. + - notrunc : the output file will not be truncated. If this option is not + present, output will be truncated when opened. + - noerror : all read errors will be ignored. If this option is not present, + dd will only ignore Error::Interrupted. + - fdatasync : data will be written before finishing. + - fsync : data and metadata will be written before finishing. + + ### Input flags + + - count_bytes : a value to count=N will be interpreted as bytes. + - skip_bytes : a value to skip=N will be interpreted as bytes. + - fullblock : wait for ibs bytes from each read. zero-length reads are still + considered EOF. + + ### Output flags + + - append : open file in append mode. Consider setting conv=notrunc as well. + - seek_bytes : a value to seek=N will be interpreted as bytes. + + ### General Flags + + - direct : use direct I/O for data. + - directory : fail unless the given input (if used as an iflag) or + output (if used as an oflag) is a directory. + - dsync : use synchronized I/O for data. + - sync : use synchronized I/O for data and metadata. + - nonblock : use non-blocking I/O. + - noatime : do not update access time. + - nocache : request that OS drop cache. + - noctty : do not assign a controlling tty. + - nofollow : do not follow system links. + +# Error messages +dd-error-failed-to-open = failed to open { $path } +dd-error-write-error = write error +dd-error-failed-to-seek = failed to seek in output file +dd-error-io-error = IO error +dd-error-cannot-skip-offset = '{ $file }': cannot skip to specified offset +dd-error-cannot-skip-invalid = '{ $file }': cannot skip: Invalid argument +dd-error-cannot-seek-invalid = '{ $output }': cannot seek: Invalid argument +dd-error-not-directory = setting flags for '{ $file }': Not a directory +dd-error-failed-discard-cache-input = failed to discard cache for: 'standard input' +dd-error-failed-discard-cache-output = failed to discard cache for: 'standard output' + +# Parse errors +dd-error-unrecognized-operand = Unrecognized operand '{ $operand }' +dd-error-multiple-format-table = Only one of conv=ascii conv=ebcdic or conv=ibm may be specified +dd-error-multiple-case = Only one of conv=lcase or conv=ucase may be specified +dd-error-multiple-block = Only one of conv=block or conv=unblock may be specified +dd-error-multiple-excl = Only one ov conv=excl or conv=nocreat may be specified +dd-error-invalid-flag = invalid input flag: ‘{ $flag }’ + Try '{ $cmd } --help' for more information. +dd-error-conv-flag-no-match = Unrecognized conv=CONV -> { $flag } +dd-error-multiplier-parse-failure = invalid number: '{ $input }' +dd-error-multiplier-overflow = Multiplier string would overflow on current system -> { $input } +dd-error-block-without-cbs = conv=block or conv=unblock specified without cbs=N +dd-error-status-not-recognized = status=LEVEL not recognized -> { $level } +dd-error-unimplemented = feature not implemented on this system -> { $feature } +dd-error-bs-out-of-range = { $param }=N cannot fit into memory +dd-error-invalid-number = invalid number: ‘{ $input }’ + +# Progress messages +dd-progress-records-in = { $complete }+{ $partial } records in +dd-progress-records-out = { $complete }+{ $partial } records out +dd-progress-truncated-record = { $count -> + [one] { $count } truncated record + *[other] { $count } truncated records +} +dd-progress-byte-copied = { $bytes } byte copied, { $duration } s, { $rate }/s +dd-progress-bytes-copied = { $bytes } bytes copied, { $duration } s, { $rate }/s +dd-progress-bytes-copied-si = { $bytes } bytes ({ $si }) copied, { $duration } s, { $rate }/s +dd-progress-bytes-copied-si-iec = { $bytes } bytes ({ $si }, { $iec }) copied, { $duration } s, { $rate }/s + +# Warnings +dd-warning-zero-multiplier = { $zero } is a zero multiplier; use { $alternative } if that is intended +dd-warning-signal-handler = Internal dd Warning: Unable to register signal handler diff --git a/src/uu/dd/locales/fr-FR.ftl b/src/uu/dd/locales/fr-FR.ftl new file mode 100644 index 00000000000..fb68f809bf8 --- /dev/null +++ b/src/uu/dd/locales/fr-FR.ftl @@ -0,0 +1,160 @@ +dd-about = Copier, et optionnellement convertir, une ressource du système de fichiers +dd-usage = dd [OPÉRANDE]... + dd OPTION +dd-after-help = ### Opérandes + + - bs=OCTETS : lire et écrire jusqu'à OCTETS octets à la fois (par défaut : 512) ; + remplace ibs et obs. + - cbs=OCTETS : la 'taille de bloc de conversion' en octets. S'applique aux + opérations conv=block et conv=unblock. + - conv=CONVS : une liste séparée par des virgules d'options de conversion ou (pour des + raisons historiques) d'indicateurs de fichier. + - count=N : arrêter la lecture de l'entrée après N opérations de lecture de taille ibs + plutôt que de continuer jusqu'à EOF. Voir iflag=count_bytes si l'arrêt après N octets + est préféré + - ibs=N : la taille du tampon utilisé pour les lectures (par défaut : 512) + - if=FICHIER : le fichier utilisé pour l'entrée. Quand non spécifié, stdin est utilisé à la place + - iflag=INDICATEURS : une liste séparée par des virgules d'indicateurs d'entrée qui spécifient comment + la source d'entrée est traitée. INDICATEURS peut être n'importe lequel des indicateurs d'entrée ou + indicateurs généraux spécifiés ci-dessous. + - skip=N (ou iseek=N) : ignorer N enregistrements de taille ibs dans l'entrée avant de commencer + les opérations de copie/conversion. Voir iflag=seek_bytes si la recherche de N octets est préférée. + - obs=N : la taille du tampon utilisé pour les écritures (par défaut : 512) + - of=FICHIER : le fichier utilisé pour la sortie. Quand non spécifié, stdout est utilisé + à la place + - oflag=INDICATEURS : liste séparée par des virgules d'indicateurs de sortie qui spécifient comment la + source de sortie est traitée. INDICATEURS peut être n'importe lequel des indicateurs de sortie ou + indicateurs généraux spécifiés ci-dessous + - seek=N (ou oseek=N) : recherche N enregistrements de taille obs dans la sortie avant de + commencer les opérations de copie/conversion. Voir oflag=seek_bytes si la recherche de N octets est + préférée + - status=NIVEAU : contrôle si les statistiques de volume et de performance sont écrites sur + stderr. + + Quand non spécifié, dd affichera les statistiques à la fin. Un exemple est ci-dessous. + + ```plain + 6+0 enregistrements en entrée + 16+0 enregistrements en sortie + 8192 octets (8.2 kB, 8.0 KiB) copiés, 0.00057009 s, + 14.4 MB/s + + Les deux premières lignes sont les statistiques de 'volume' et la dernière ligne est les + statistiques de 'performance'. + Les statistiques de volume indiquent le nombre de lectures complètes et partielles de taille ibs, + ou d'écritures de taille obs qui ont eu lieu pendant la copie. Le format des statistiques de + volume est +. Si des enregistrements ont été tronqués (voir + conv=block), les statistiques de volume contiendront le nombre d'enregistrements tronqués. + + Les valeurs possibles de NIVEAU sont : + - progress : Afficher les statistiques de performance périodiques pendant la copie. + - 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é), + 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. + + ### Options de conversion + + - ascii : convertir d'EBCDIC vers ASCII. C'est l'inverse de l'option ebcdic. + Implique conv=unblock. + - ebcdic : convertir d'ASCII vers EBCDIC. C'est l'inverse de l'option ascii. + Implique conv=block. + - ibm : convertir d'ASCII vers EBCDIC, en appliquant les conventions pour [, ] + et ~ spécifiées dans POSIX. Implique conv=block. + + - ucase : convertir de minuscules vers majuscules. + - lcase : convertir de majuscules vers minuscules. + + - block : pour chaque nouvelle ligne inférieure à la taille indiquée par cbs=OCTETS, supprimer + la nouvelle ligne et remplir avec des espaces jusqu'à cbs. Les lignes plus longues que cbs sont tronquées. + - unblock : pour chaque bloc d'entrée de la taille indiquée par cbs=OCTETS, supprimer + les espaces de fin à droite et remplacer par un caractère de nouvelle ligne. + + - sparse : tente de rechercher la sortie quand un bloc de taille obs ne contient que + des zéros. + - swab : échange chaque paire d'octets adjacents. Si un nombre impair d'octets est + présent, l'octet final est omis. + - sync : remplit chaque bloc de taille ibs avec des zéros. Si block ou unblock est + spécifié, remplit avec des espaces à la place. + - excl : le fichier de sortie doit être créé. Échoue si le fichier de sortie est déjà + présent. + - nocreat : le fichier de sortie ne sera pas créé. Échoue si le fichier de sortie n'est + pas déjà présent. + - notrunc : le fichier de sortie ne sera pas tronqué. Si cette option n'est pas + présente, la sortie sera tronquée à l'ouverture. + - noerror : toutes les erreurs de lecture seront ignorées. Si cette option n'est pas présente, + dd n'ignorera que Error::Interrupted. + - fdatasync : les données seront écrites avant la fin. + - fsync : les données et les métadonnées seront écrites avant la fin. + + ### Indicateurs d'entrée + + - count_bytes : une valeur pour count=N sera interprétée comme des octets. + - skip_bytes : une valeur pour skip=N sera interprétée comme des octets. + - fullblock : attendre ibs octets de chaque lecture. les lectures de longueur zéro sont toujours + considérées comme EOF. + + ### Indicateurs de sortie + + - append : ouvrir le fichier en mode ajout. Considérez définir conv=notrunc aussi. + - seek_bytes : une valeur pour seek=N sera interprétée comme des octets. + + ### Indicateurs généraux + + - direct : utiliser les E/S directes pour les données. + - directory : échouer sauf si l'entrée donnée (si utilisée comme iflag) ou + la sortie (si utilisée comme oflag) est un répertoire. + - dsync : utiliser les E/S synchronisées pour les données. + - sync : utiliser les E/S synchronisées pour les données et les métadonnées. + - nonblock : utiliser les E/S non-bloquantes. + - noatime : ne pas mettre à jour l'heure d'accès. + - nocache : demander au système d'exploitation de supprimer le cache. + - noctty : ne pas assigner un tty de contrôle. + - nofollow : ne pas suivre les liens système. + +# Error messages +dd-error-failed-to-open = échec de l'ouverture de { $path } +dd-error-write-error = erreur d'écriture +dd-error-failed-to-seek = échec de la recherche dans le fichier de sortie +dd-error-io-error = erreur E/S +dd-error-cannot-skip-offset = '{ $file }' : impossible d'ignorer jusqu'au décalage spécifié +dd-error-cannot-skip-invalid = '{ $file }' : impossible d'ignorer : Argument invalide +dd-error-cannot-seek-invalid = '{ $output }' : impossible de rechercher : Argument invalide +dd-error-not-directory = définir les indicateurs pour '{ $file }' : N'est pas un répertoire +dd-error-failed-discard-cache-input = échec de la suppression du cache pour : 'entrée standard' +dd-error-failed-discard-cache-output = échec de la suppression du cache pour : 'sortie standard' + +# Parse errors +dd-error-unrecognized-operand = Opérande non reconnue '{ $operand }' +dd-error-multiple-format-table = Seul un seul de conv=ascii conv=ebcdic ou conv=ibm peut être spécifié +dd-error-multiple-case = Seul un seul de conv=lcase ou conv=ucase peut être spécifié +dd-error-multiple-block = Seul un seul de conv=block ou conv=unblock peut être spécifié +dd-error-multiple-excl = Seul un seul de conv=excl ou conv=nocreat peut être spécifié +dd-error-invalid-flag = indicateur d'entrée invalide : '{ $flag }' + Essayez '{ $cmd } --help' pour plus d'informations. +dd-error-conv-flag-no-match = conv=CONV non reconnu -> { $flag } +dd-error-multiplier-parse-failure = nombre invalide : ‘{ $input }‘ +dd-error-multiplier-overflow = La chaîne de multiplicateur déborderait sur le système actuel -> { $input } +dd-error-block-without-cbs = conv=block ou conv=unblock spécifié sans cbs=N +dd-error-status-not-recognized = status=NIVEAU non reconnu -> { $level } +dd-error-unimplemented = fonctionnalité non implémentée sur ce système -> { $feature } +dd-error-bs-out-of-range = { $param }=N ne peut pas tenir en mémoire +dd-error-invalid-number = nombre invalide : ‘{ $input }‘ + +# Progress messages +dd-progress-records-in = { $complete }+{ $partial } enregistrements en entrée +dd-progress-records-out = { $complete }+{ $partial } enregistrements en sortie +dd-progress-truncated-record = { $count -> + [one] { $count } enregistrement tronqué + *[other] { $count } enregistrements tronqués +} +dd-progress-byte-copied = { $bytes } octet copié, { $duration } s, { $rate }/s +dd-progress-bytes-copied = { $bytes } octets copiés, { $duration } s, { $rate }/s +dd-progress-bytes-copied-si = { $bytes } octets ({ $si }) copiés, { $duration } s, { $rate }/s +dd-progress-bytes-copied-si-iec = { $bytes } octets ({ $si }, { $iec }) copiés, { $duration } s, { $rate }/s + +# Warnings +dd-warning-zero-multiplier = { $zero } est un multiplicateur zéro ; utilisez { $alternative } si c'est voulu +dd-warning-signal-handler = Avertissement dd interne : Impossible d'enregistrer le gestionnaire de signal diff --git a/src/uu/dd/src/blocks.rs b/src/uu/dd/src/blocks.rs index 8e5557a2c9b..b7449c98be7 100644 --- a/src/uu/dd/src/blocks.rs +++ b/src/uu/dd/src/blocks.rs @@ -133,7 +133,7 @@ mod tests { let buf = [0u8, 1u8, 2u8, 3u8]; let res = block(&buf, 4, false, &mut rs); - assert_eq!(res, vec![vec![0u8, 1u8, 2u8, 3u8],]); + assert_eq!(res, vec![vec![0u8, 1u8, 2u8, 3u8]]); } #[test] @@ -144,7 +144,7 @@ mod tests { assert_eq!( res, - vec![vec![0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE],] + vec![vec![0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE]] ); } @@ -155,7 +155,7 @@ mod tests { let res = block(&buf, 4, false, &mut rs); // Commented section(s) should be truncated and appear for reference only. - assert_eq!(res, vec![vec![0u8, 1u8, 2u8, 3u8 /*, 4u8*/],]); + assert_eq!(res, vec![vec![0u8, 1u8, 2u8, 3u8 /*, 4u8*/]]); assert_eq!(rs.records_truncated, 1); } @@ -238,7 +238,7 @@ mod tests { let buf = [0u8, 1u8, 2u8, 3u8, NEWLINE]; let res = block(&buf, 4, false, &mut rs); - assert_eq!(res, vec![vec![0u8, 1u8, 2u8, 3u8],]); + assert_eq!(res, vec![vec![0u8, 1u8, 2u8, 3u8]]); } #[test] @@ -258,7 +258,7 @@ mod tests { assert_eq!( res, - vec![vec![0u8, 1u8, 2u8, SPACE], vec![SPACE, SPACE, SPACE, SPACE],] + vec![vec![0u8, 1u8, 2u8, SPACE], vec![SPACE, SPACE, SPACE, SPACE]] ); } @@ -270,7 +270,7 @@ mod tests { assert_eq!( res, - vec![vec![SPACE, SPACE, SPACE, SPACE], vec![0u8, 1u8, 2u8, 3u8],] + vec![vec![SPACE, SPACE, SPACE, SPACE], vec![0u8, 1u8, 2u8, 3u8]] ); } @@ -315,7 +315,7 @@ mod tests { let buf = [0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8]; let res = unblock(&buf, 8); - assert_eq!(res, vec![0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, NEWLINE],); + assert_eq!(res, vec![0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, NEWLINE]); } #[test] @@ -323,7 +323,7 @@ mod tests { let buf = [SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE]; let res = unblock(&buf, 8); - assert_eq!(res, vec![NEWLINE],); + assert_eq!(res, vec![NEWLINE]); } #[test] @@ -342,7 +342,7 @@ mod tests { let buf = [0u8, 1u8, 2u8, 3u8, SPACE, SPACE, SPACE, SPACE]; let res = unblock(&buf, 8); - assert_eq!(res, vec![0u8, 1u8, 2u8, 3u8, NEWLINE],); + assert_eq!(res, vec![0u8, 1u8, 2u8, 3u8, NEWLINE]); } #[test] diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index b1c6b563017..0ddeefeb4e7 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -21,8 +21,10 @@ use nix::fcntl::FcntlArg::F_SETFL; #[cfg(any(target_os = "linux", target_os = "android"))] use nix::fcntl::OFlag; use parseargs::Parser; -use progress::{gen_prog_updater, ProgUpdate, ReadStat, StatusLevel, WriteStat}; +use progress::ProgUpdateType; +use progress::{ProgUpdate, ReadStat, StatusLevel, WriteStat, gen_prog_updater}; use uucore::io::OwnedFileDescriptorOrHandle; +use uucore::translate; use std::cmp; use std::env; @@ -30,6 +32,8 @@ use std::ffi::OsString; use std::fs::{File, OpenOptions}; use std::io::{self, Read, Seek, SeekFrom, Stdout, Write}; #[cfg(any(target_os = "linux", target_os = "android"))] +use std::os::fd::AsFd; +#[cfg(any(target_os = "linux", target_os = "android"))] use std::os::unix::fs::OpenOptionsExt; #[cfg(unix)] use std::os::unix::{ @@ -39,31 +43,27 @@ use std::os::unix::{ #[cfg(windows)] use std::os::windows::{fs::MetadataExt, io::AsHandle}; use std::path::Path; -use std::sync::{ - atomic::{AtomicBool, Ordering::Relaxed}, - mpsc, Arc, -}; +use std::sync::atomic::AtomicU8; +use std::sync::{Arc, atomic::Ordering::Relaxed, mpsc}; use std::thread; use std::time::{Duration, Instant}; -use clap::{crate_version, Arg, Command}; +use clap::{Arg, Command}; use gcd::Gcd; #[cfg(target_os = "linux")] use nix::{ errno::Errno, - fcntl::{posix_fadvise, PosixFadviseAdvice}, + fcntl::{PosixFadviseAdvice, posix_fadvise}, }; +use uucore::LocalizedCommand; use uucore::display::Quotable; -#[cfg(unix)] -use uucore::error::set_exit_code; use uucore::error::{FromIo, UResult}; +#[cfg(unix)] +use uucore::error::{USimpleError, set_exit_code}; #[cfg(target_os = "linux")] use uucore::show_if_err; -use uucore::{format_usage, help_about, help_section, help_usage, show_error}; +use uucore::{format_usage, show_error}; -const ABOUT: &str = help_about!("dd.md"); -const AFTER_HELP: &str = help_section!("after help", "dd.md"); -const USAGE: &str = help_usage!("dd.md"); const BUF_INIT_BYTE: u8 = 0xDD; /// Final settings after parsing @@ -87,38 +87,65 @@ struct Settings { /// A timer which triggers on a given interval /// -/// After being constructed with [`Alarm::with_interval`], [`Alarm::is_triggered`] -/// will return true once per the given [`Duration`]. +/// After being constructed with [`Alarm::with_interval`], [`Alarm::get_trigger`] +/// will return [`ALARM_TRIGGER_TIMER`] once per the given [`Duration`]. +/// Alarm can be manually triggered with closure returned by [`Alarm::manual_trigger_fn`]. +/// [`Alarm::get_trigger`] will return [`ALARM_TRIGGER_SIGNAL`] in this case. /// /// Can be cloned, but the trigger status is shared across all instances so only /// the first caller each interval will yield true. /// /// When all instances are dropped the background thread will exit on the next interval. -#[derive(Debug, Clone)] pub struct Alarm { interval: Duration, - trigger: Arc, + trigger: Arc, } +pub const ALARM_TRIGGER_NONE: u8 = 0; +pub const ALARM_TRIGGER_TIMER: u8 = 1; +pub const ALARM_TRIGGER_SIGNAL: u8 = 2; + impl Alarm { + /// use to construct alarm timer with duration pub fn with_interval(interval: Duration) -> Self { - let trigger = Arc::new(AtomicBool::default()); + let trigger = Arc::new(AtomicU8::default()); let weak_trigger = Arc::downgrade(&trigger); thread::spawn(move || { while let Some(trigger) = weak_trigger.upgrade() { thread::sleep(interval); - trigger.store(true, Relaxed); + trigger.store(ALARM_TRIGGER_TIMER, Relaxed); } }); Self { interval, trigger } } - pub fn is_triggered(&self) -> bool { - self.trigger.swap(false, Relaxed) + /// Returns a closure that allows to manually trigger the alarm + /// + /// This is useful for cases where more than one alarm even source exists + /// In case of `dd` there is the SIGUSR1/SIGINFO case where we want to + /// trigger an manual progress report. + pub fn manual_trigger_fn(&self) -> Box { + let weak_trigger = Arc::downgrade(&self.trigger); + Box::new(move || { + if let Some(trigger) = weak_trigger.upgrade() { + trigger.store(ALARM_TRIGGER_SIGNAL, Relaxed); + } + }) + } + + /// Use this function to poll for any pending alarm event + /// + /// Returns `ALARM_TRIGGER_NONE` for no pending event. + /// Returns `ALARM_TRIGGER_TIMER` if the event was triggered by timer + /// Returns `ALARM_TRIGGER_SIGNAL` if the event was triggered manually + /// by the closure returned from `manual_trigger_fn` + pub fn get_trigger(&self) -> u8 { + self.trigger.swap(ALARM_TRIGGER_NONE, Relaxed) } + // Getter function for the configured interval duration pub fn get_interval(&self) -> Duration { self.interval } @@ -196,7 +223,8 @@ impl Source { /// The length of the data source in number of bytes. /// /// If it cannot be determined, then this function returns 0. - fn len(&self) -> std::io::Result { + fn len(&self) -> io::Result { + #[allow(clippy::match_wildcard_for_single_variants)] match self { Self::File(f) => Ok(f.metadata()?.len().try_into().unwrap_or(i64::MAX)), _ => Ok(0), @@ -208,7 +236,10 @@ impl Source { #[cfg(not(unix))] Self::Stdin(stdin) => match io::copy(&mut stdin.take(n), &mut io::sink()) { Ok(m) if m < n => { - show_error!("'standard input': cannot skip to specified offset"); + show_error!( + "{}", + translate!("dd-error-cannot-skip-offset", "file" => "standard input") + ); Ok(m) } Ok(m) => Ok(m), @@ -220,21 +251,27 @@ impl Source { if len < n { // GNU compatibility: // this case prints the stats but sets the exit code to 1 - show_error!("'standard input': cannot skip: Invalid argument"); + show_error!( + "{}", + translate!("dd-error-cannot-skip-invalid", "file" => "standard input") + ); set_exit_code(1); return Ok(len); } } match io::copy(&mut f.take(n), &mut io::sink()) { Ok(m) if m < n => { - show_error!("'standard input': cannot skip to specified offset"); + show_error!( + "{}", + translate!("dd-error-cannot-skip-offset", "file" => "standard input") + ); Ok(m) } Ok(m) => Ok(m), Err(e) => Err(e), } } - Self::File(f) => f.seek(io::SeekFrom::Current(n.try_into().unwrap())), + Self::File(f) => f.seek(SeekFrom::Current(n.try_into().unwrap())), #[cfg(unix)] Self::Fifo(f) => io::copy(&mut f.take(n), &mut io::sink()), } @@ -248,10 +285,11 @@ impl Source { /// then this function returns an error. #[cfg(target_os = "linux")] fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) -> nix::Result<()> { + #[allow(clippy::match_wildcard_for_single_variants)] match self { Self::File(f) => { let advice = PosixFadviseAdvice::POSIX_FADV_DONTNEED; - posix_fadvise(f.as_raw_fd(), offset, len, advice) + posix_fadvise(f.as_fd(), offset, len, advice) } _ => Err(Errno::ESPIPE), // "Illegal seek" } @@ -312,13 +350,13 @@ impl<'a> Input<'a> { let mut src = Source::stdin_as_file(); #[cfg(unix)] if let Source::StdinFile(f) = &src { - // GNU compatibility: - // this will check whether stdin points to a folder or not - if f.metadata()?.is_file() && settings.iflags.directory { - show_error!("standard input: not a directory"); - return Err(1.into()); + if settings.iflags.directory && !f.metadata()?.is_dir() { + return Err(USimpleError::new( + 1, + translate!("dd-error-not-directory", "file" => "standard input"), + )); } - }; + } if settings.skip > 0 { src.skip(settings.skip)?; } @@ -336,8 +374,9 @@ impl<'a> Input<'a> { opts.custom_flags(libc_flags); } - opts.open(filename) - .map_err_context(|| format!("failed to open {}", filename.quote()))? + opts.open(filename).map_err_context( + || translate!("dd-error-failed-to-open", "path" => filename.quote()), + )? }; let mut src = Source::File(src); @@ -391,14 +430,10 @@ fn make_linux_iflags(iflags: &IFlags) -> Option { flag |= libc::O_SYNC; } - if flag == 0 { - None - } else { - Some(flag) - } + if flag == 0 { None } else { Some(flag) } } -impl<'a> Read for Input<'a> { +impl Read for Input<'_> { fn read(&mut self, buf: &mut [u8]) -> io::Result { let mut base_idx = 0; let target_len = buf.len(); @@ -413,7 +448,7 @@ impl<'a> Read for Input<'a> { } } Ok(len) => return Ok(len), - Err(e) if e.kind() == io::ErrorKind::Interrupted => continue, + Err(e) if e.kind() == io::ErrorKind::Interrupted => (), Err(_) if self.settings.iconv.noerror => return Ok(base_idx), Err(e) => return Err(e), } @@ -421,7 +456,7 @@ impl<'a> Read for Input<'a> { } } -impl<'a> Input<'a> { +impl Input<'_> { /// Discard the system file cache for the given portion of the input. /// /// `offset` and `len` specify a contiguous portion of the input. @@ -429,14 +464,15 @@ impl<'a> Input<'a> { /// the input file is no longer needed. If not possible, then this /// function prints an error message to stderr and sets the exit /// status code to 1. - #[allow(unused_variables)] + #[cfg_attr(not(target_os = "linux"), allow(clippy::unused_self, unused_variables))] fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) { #[cfg(target_os = "linux")] { - show_if_err!(self - .src - .discard_cache(offset, len) - .map_err_context(|| "failed to discard cache for: 'standard input'".to_string())); + show_if_err!( + self.src + .discard_cache(offset, len) + .map_err_context(|| translate!("dd-error-failed-discard-cache-input")) + ); } #[cfg(not(target_os = "linux"))] { @@ -448,7 +484,7 @@ impl<'a> Input<'a> { /// Fills a given buffer. /// Reads in increments of 'self.ibs'. /// The start of each ibs-sized read follows the previous one. - fn fill_consecutive(&mut self, buf: &mut Vec) -> std::io::Result { + fn fill_consecutive(&mut self, buf: &mut Vec) -> io::Result { let mut reads_complete = 0; let mut reads_partial = 0; let mut bytes_total = 0; @@ -479,7 +515,7 @@ impl<'a> Input<'a> { /// Fills a given buffer. /// Reads in increments of 'self.ibs'. /// The start of each ibs-sized read is aligned to multiples of ibs; remaining space is filled with the 'pad' byte. - fn fill_blocks(&mut self, buf: &mut Vec, pad: u8) -> std::io::Result { + fn fill_blocks(&mut self, buf: &mut Vec, pad: u8) -> io::Result { let mut reads_complete = 0; let mut reads_partial = 0; let mut base_idx = 0; @@ -585,12 +621,15 @@ impl Dest { if len < n { // GNU compatibility: // this case prints the stats but sets the exit code to 1 - show_error!("'standard output': cannot seek: Invalid argument"); + show_error!( + "{}", + translate!("dd-error-cannot-seek-invalid", "output" => "standard output") + ); set_exit_code(1); return Ok(len); } } - f.seek(io::SeekFrom::Current(n.try_into().unwrap())) + f.seek(SeekFrom::Current(n.try_into().unwrap())) } #[cfg(unix)] Self::Fifo(f) => { @@ -604,6 +643,7 @@ impl Dest { /// Truncate the underlying file to the current stream position, if possible. fn truncate(&mut self) -> io::Result<()> { + #[allow(clippy::match_wildcard_for_single_variants)] match self { Self::File(f, _) => { let pos = f.stream_position()?; @@ -624,7 +664,7 @@ impl Dest { match self { Self::File(f, _) => { let advice = PosixFadviseAdvice::POSIX_FADV_DONTNEED; - posix_fadvise(f.as_raw_fd(), offset, len, advice) + posix_fadvise(f.as_fd(), offset, len, advice) } _ => Err(Errno::ESPIPE), // "Illegal seek" } @@ -633,7 +673,8 @@ impl Dest { /// The length of the data destination in number of bytes. /// /// If it cannot be determined, then this function returns 0. - fn len(&self) -> std::io::Result { + fn len(&self) -> io::Result { + #[allow(clippy::match_wildcard_for_single_variants)] match self { Self::File(f, _) => Ok(f.metadata()?.len().try_into().unwrap_or(i64::MAX)), _ => Ok(0), @@ -654,7 +695,7 @@ impl Write for Dest { .len() .try_into() .expect("Internal dd Error: Seek amount greater than signed 64-bit integer"); - f.seek(io::SeekFrom::Current(seek_amt))?; + f.seek(SeekFrom::Current(seek_amt))?; Ok(buf.len()) } Self::File(f, _) => f.write(buf), @@ -697,7 +738,7 @@ impl<'a> Output<'a> { fn new_stdout(settings: &'a Settings) -> UResult { let mut dst = Dest::Stdout(io::stdout()); dst.seek(settings.seek) - .map_err_context(|| "write error".to_string())?; + .map_err_context(|| translate!("dd-error-write-error"))?; Ok(Self { dst, settings }) } @@ -718,8 +759,9 @@ impl<'a> Output<'a> { opts.open(path) } - let dst = open_dst(filename, &settings.oconv, &settings.oflags) - .map_err_context(|| format!("failed to open {}", filename.quote()))?; + let dst = open_dst(filename, &settings.oconv, &settings.oflags).map_err_context( + || translate!("dd-error-failed-to-open", "path" => filename.quote()), + )?; // Seek to the index in the output file, truncating if requested. // @@ -744,7 +786,7 @@ impl<'a> Output<'a> { }; let mut dst = Dest::File(dst, density); dst.seek(settings.seek) - .map_err_context(|| "failed to seek in output file".to_string())?; + .map_err_context(|| translate!("dd-error-failed-to-seek"))?; Ok(Self { dst, settings }) } @@ -758,7 +800,7 @@ impl<'a> Output<'a> { #[cfg(any(target_os = "linux", target_os = "android"))] if let Some(libc_flags) = make_linux_oflags(&settings.oflags) { nix::fcntl::fcntl( - fx.as_raw().as_raw_fd(), + fx.as_raw().as_fd(), F_SETFL(OFlag::from_bits_retain(libc_flags)), )?; } @@ -802,22 +844,47 @@ impl<'a> Output<'a> { /// the output file is no longer needed. If not possible, then /// this function prints an error message to stderr and sets the /// exit status code to 1. - #[allow(unused_variables)] + #[cfg_attr(not(target_os = "linux"), allow(clippy::unused_self, unused_variables))] fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) { #[cfg(target_os = "linux")] { - show_if_err!(self - .dst - .discard_cache(offset, len) - .map_err_context(|| "failed to discard cache for: 'standard output'".to_string())); + show_if_err!( + self.dst + .discard_cache(offset, len) + .map_err_context(|| { translate!("dd-error-failed-discard-cache-output") }) + ); } - #[cfg(target_os = "linux")] + #[cfg(not(target_os = "linux"))] { // TODO Is there a way to discard filesystem cache on // these other operating systems? } } + /// writes a block of data. optionally retries when first try didn't complete + /// + /// this is needed by gnu-test: tests/dd/stats.s + /// the write can be interrupted by a system signal. + /// e.g. SIGUSR1 which is send to report status + /// without retry, the data might not be fully written to destination. + fn write_block(&mut self, chunk: &[u8]) -> io::Result { + let full_len = chunk.len(); + let mut base_idx = 0; + loop { + match self.dst.write(&chunk[base_idx..]) { + Ok(wlen) => { + base_idx += wlen; + // take iflags.fullblock as oflags shall not have this option + if (base_idx >= full_len) || !self.settings.iflags.fullblock { + return Ok(base_idx); + } + } + Err(e) if e.kind() == io::ErrorKind::Interrupted => (), + Err(e) => return Err(e), + } + } + } + /// Write the given bytes one block at a time. /// /// This may write partial blocks (for example, if the underlying @@ -831,7 +898,7 @@ impl<'a> Output<'a> { let mut bytes_total = 0; for chunk in buf.chunks(self.settings.obs) { - let wlen = self.dst.write(chunk)?; + let wlen = self.write_block(chunk)?; if wlen < self.settings.obs { writes_partial += 1; } else { @@ -848,7 +915,7 @@ impl<'a> Output<'a> { } /// Flush the output to disk, if configured to do so. - fn sync(&mut self) -> std::io::Result<()> { + fn sync(&mut self) -> io::Result<()> { if self.settings.oconv.fsync { self.dst.fsync() } else if self.settings.oconv.fdatasync { @@ -860,7 +927,7 @@ impl<'a> Output<'a> { } /// Truncate the underlying file to the current stream position, if possible. - fn truncate(&mut self) -> std::io::Result<()> { + fn truncate(&mut self) -> io::Result<()> { self.dst.truncate() } } @@ -878,7 +945,7 @@ enum BlockWriter<'a> { Unbuffered(Output<'a>), } -impl<'a> BlockWriter<'a> { +impl BlockWriter<'_> { fn discard_cache(&self, offset: libc::off_t, len: libc::off_t) { match self { Self::Unbuffered(o) => o.discard_cache(offset, len), @@ -914,7 +981,7 @@ impl<'a> BlockWriter<'a> { }; } - fn write_blocks(&mut self, buf: &[u8]) -> std::io::Result { + fn write_blocks(&mut self, buf: &[u8]) -> io::Result { match self { Self::Unbuffered(o) => o.write_blocks(buf), Self::Buffered(o) => o.write_blocks(buf), @@ -922,6 +989,29 @@ impl<'a> BlockWriter<'a> { } } +/// depending on the command line arguments, this function +/// informs the OS to flush/discard the caches for input and/or output file. +fn flush_caches_full_length(i: &Input, o: &Output) -> io::Result<()> { + // TODO Better error handling for overflowing `len`. + if i.settings.iflags.nocache { + let offset = 0; + #[allow(clippy::useless_conversion)] + let len = i.src.len()?.try_into().unwrap(); + i.discard_cache(offset, len); + } + // Similarly, discard the system cache for the output file. + // + // TODO Better error handling for overflowing `len`. + if i.settings.oflags.nocache { + let offset = 0; + #[allow(clippy::useless_conversion)] + let len = o.dst.len()?.try_into().unwrap(); + o.discard_cache(offset, len); + } + + Ok(()) +} + /// Copy the given input data to this output, consuming both. /// /// This method contains the main loop for the `dd` program. Bytes @@ -933,7 +1023,7 @@ impl<'a> BlockWriter<'a> { /// /// If there is a problem reading from the input or writing to /// this output. -fn dd_copy(mut i: Input, o: Output) -> std::io::Result<()> { +fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { // The read and write statistics. // // These objects are counters, initialized to zero. After each @@ -981,22 +1071,7 @@ fn dd_copy(mut i: Input, o: Output) -> std::io::Result<()> { // requests that we inform the system that we no longer // need the contents of the input file in a system cache. // - // TODO Better error handling for overflowing `len`. - if i.settings.iflags.nocache { - let offset = 0; - #[allow(clippy::useless_conversion)] - let len = i.src.len()?.try_into().unwrap(); - i.discard_cache(offset, len); - } - // Similarly, discard the system cache for the output file. - // - // TODO Better error handling for overflowing `len`. - if i.settings.oflags.nocache { - let offset = 0; - #[allow(clippy::useless_conversion)] - let len = o.dst.len()?.try_into().unwrap(); - o.discard_cache(offset, len); - } + flush_caches_full_length(&i, &o)?; return finalize( BlockWriter::Unbuffered(o), rstat, @@ -1006,7 +1081,7 @@ fn dd_copy(mut i: Input, o: Output) -> std::io::Result<()> { output_thread, truncate, ); - }; + } // Create a common buffer with a capacity of the block size. // This is the max size needed. @@ -1018,6 +1093,18 @@ fn dd_copy(mut i: Input, o: Output) -> std::io::Result<()> { // This avoids the need to query the OS monotonic clock for every block. let alarm = Alarm::with_interval(Duration::from_secs(1)); + // The signal handler spawns an own thread that waits for signals. + // When the signal is received, it calls a handler function. + // We inject a handler function that manually triggers the alarm. + #[cfg(target_os = "linux")] + let signal_handler = progress::SignalHandler::install_signal_handler(alarm.manual_trigger_fn()); + #[cfg(target_os = "linux")] + if let Err(e) = &signal_handler { + if Some(StatusLevel::None) != i.settings.status { + eprintln!("{}\n\t{e}", translate!("dd-warning-signal-handler")); + } + } + // Index in the input file where we are reading bytes and in // the output file where we are writing bytes. // @@ -1041,13 +1128,13 @@ fn dd_copy(mut i: Input, o: Output) -> std::io::Result<()> { // blocks to this output. Read/write statistics are updated on // each iteration and cumulative statistics are reported to // the progress reporting thread. - while below_count_limit(&i.settings.count, &rstat) { + while below_count_limit(i.settings.count, &rstat) { // Read a block from the input then write the block to the output. // // As an optimization, make an educated guess about the // best buffer size for reading based on the number of // blocks already read and the number of blocks remaining. - let loop_bsize = calc_loop_bsize(&i.settings.count, &rstat, &wstat, i.settings.ibs, bsize); + let loop_bsize = calc_loop_bsize(i.settings.count, &rstat, &wstat, i.settings.ibs, bsize); let rstat_update = read_helper(&mut i, &mut buf, loop_bsize)?; if rstat_update.is_empty() { break; @@ -1086,11 +1173,20 @@ fn dd_copy(mut i: Input, o: Output) -> std::io::Result<()> { // error. rstat += rstat_update; wstat += wstat_update; - if alarm.is_triggered() { - let prog_update = ProgUpdate::new(rstat, wstat, start.elapsed(), false); - prog_tx.send(prog_update).unwrap_or(()); + match alarm.get_trigger() { + ALARM_TRIGGER_NONE => {} + t @ (ALARM_TRIGGER_TIMER | ALARM_TRIGGER_SIGNAL) => { + let tp = match t { + ALARM_TRIGGER_TIMER => ProgUpdateType::Periodic, + _ => ProgUpdateType::Signal, + }; + let prog_update = ProgUpdate::new(rstat, wstat, start.elapsed(), tp); + prog_tx.send(prog_update).unwrap_or(()); + } + _ => {} } } + finalize(o, rstat, wstat, start, &prog_tx, output_thread, truncate) } @@ -1103,7 +1199,7 @@ fn finalize( prog_tx: &mpsc::Sender, output_thread: thread::JoinHandle, truncate: bool, -) -> std::io::Result<()> { +) -> io::Result<()> { // Flush the output in case a partial write has been buffered but // not yet written. let wstat_update = output.flush()?; @@ -1118,12 +1214,13 @@ fn finalize( // Print the final read/write statistics. let wstat = wstat + wstat_update; - let prog_update = ProgUpdate::new(rstat, wstat, start.elapsed(), true); + let prog_update = ProgUpdate::new(rstat, wstat, start.elapsed(), ProgUpdateType::Final); prog_tx.send(prog_update).unwrap_or(()); // Wait for the output thread to finish output_thread .join() .expect("Failed to join with the output thread."); + Ok(()) } @@ -1161,11 +1258,7 @@ fn make_linux_oflags(oflags: &OFlags) -> Option { flag |= libc::O_SYNC; } - if flag == 0 { - None - } else { - Some(flag) - } + if flag == 0 { None } else { Some(flag) } } /// Read from an input (that is, a source of bytes) into the given buffer. @@ -1174,7 +1267,7 @@ fn make_linux_oflags(oflags: &OFlags) -> Option { /// `conv=swab` or `conv=block` command-line arguments. This function /// mutates the `buf` argument in-place. The returned [`ReadStat`] /// indicates how many blocks were read. -fn read_helper(i: &mut Input, buf: &mut Vec, bsize: usize) -> std::io::Result { +fn read_helper(i: &mut Input, buf: &mut Vec, bsize: usize) -> io::Result { // Local Helper Fns ------------------------------------------------- fn perform_swab(buf: &mut [u8]) { for base in (1..buf.len()).step_by(2) { @@ -1221,10 +1314,10 @@ fn calc_bsize(ibs: usize, obs: usize) -> usize { (ibs / gcd) * obs } -// Calculate the buffer size appropriate for this loop iteration, respecting -// a count=N if present. +/// Calculate the buffer size appropriate for this loop iteration, respecting +/// a `count=N` if present. fn calc_loop_bsize( - count: &Option, + count: Option, rstat: &ReadStat, wstat: &WriteStat, ibs: usize, @@ -1237,7 +1330,7 @@ fn calc_loop_bsize( cmp::min(ideal_bsize as u64, rremain * ibs as u64) as usize } Some(Num::Bytes(bmax)) => { - let bmax: u128 = (*bmax).into(); + let bmax: u128 = bmax.into(); let bremain: u128 = bmax - wstat.bytes_total; cmp::min(ideal_bsize as u128, bremain) as usize } @@ -1245,12 +1338,12 @@ fn calc_loop_bsize( } } -// Decide if the current progress is below a count=N limit or return -// true if no such limit is set. -fn below_count_limit(count: &Option, rstat: &ReadStat) -> bool { +/// Decide if the current progress is below a `count=N` limit or return +/// `true` if no such limit is set. +fn below_count_limit(count: Option, rstat: &ReadStat) -> bool { match count { - Some(Num::Blocks(n)) => rstat.reads_complete + rstat.reads_partial < *n, - Some(Num::Bytes(n)) => rstat.bytes_total < *n, + Some(Num::Blocks(n)) => rstat.reads_complete + rstat.reads_partial < n, + Some(Num::Bytes(n)) => rstat.bytes_total < n, None => true, } } @@ -1323,14 +1416,12 @@ fn is_fifo(filename: &str) -> bool { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let settings: Settings = Parser::new().parse( - &matches + matches .get_many::(options::OPERANDS) - .unwrap_or_default() - .map(|s| s.as_ref()) - .collect::>()[..], + .unwrap_or_default(), )?; let i = match settings.infile { @@ -1346,22 +1437,23 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { None if is_stdout_redirected_to_seekable_file() => Output::new_file_from_stdout(&settings)?, None => Output::new_stdout(&settings)?, }; - dd_copy(i, o).map_err_context(|| "IO error".to_string()) + dd_copy(i, o).map_err_context(|| translate!("dd-error-io-error")) } pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) - .after_help(AFTER_HELP) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) + .about(translate!("dd-about")) + .override_usage(format_usage(&translate!("dd-usage"))) + .after_help(translate!("dd-after-help")) .infer_long_args(true) .arg(Arg::new(options::OPERANDS).num_args(1..)) } #[cfg(test)] mod tests { - use crate::{calc_bsize, Output, Parser}; + use crate::{Output, Parser, calc_bsize}; use std::path::Path; @@ -1369,8 +1461,8 @@ mod tests { fn bsize_test_primes() { let (n, m) = (7901, 7919); let res = calc_bsize(n, m); - assert!(res % n == 0); - assert!(res % m == 0); + assert_eq!(res % n, 0); + assert_eq!(res % m, 0); assert_eq!(res, n * m); } @@ -1379,8 +1471,8 @@ mod tests { fn bsize_test_rel_prime_obs_greater() { let (n, m) = (7 * 5119, 13 * 5119); let res = calc_bsize(n, m); - assert!(res % n == 0); - assert!(res % m == 0); + assert_eq!(res % n, 0); + assert_eq!(res % m, 0); assert_eq!(res, 7 * 13 * 5119); } @@ -1389,8 +1481,8 @@ mod tests { fn bsize_test_rel_prime_ibs_greater() { let (n, m) = (13 * 5119, 7 * 5119); let res = calc_bsize(n, m); - assert!(res % n == 0); - assert!(res % m == 0); + assert_eq!(res % n, 0); + assert_eq!(res % m, 0); assert_eq!(res, 7 * 13 * 5119); } @@ -1399,8 +1491,8 @@ mod tests { fn bsize_test_3fac_rel_prime() { let (n, m) = (11 * 13 * 5119, 7 * 11 * 5119); let res = calc_bsize(n, m); - assert!(res % n == 0); - assert!(res % m == 0); + assert_eq!(res % n, 0); + assert_eq!(res % m, 0); assert_eq!(res, 7 * 11 * 13 * 5119); } @@ -1409,8 +1501,8 @@ mod tests { fn bsize_test_ibs_greater() { let (n, m) = (512 * 1024, 256 * 1024); let res = calc_bsize(n, m); - assert!(res % n == 0); - assert!(res % m == 0); + assert_eq!(res % n, 0); + assert_eq!(res % m, 0); assert_eq!(res, n); } @@ -1419,8 +1511,8 @@ mod tests { fn bsize_test_obs_greater() { let (n, m) = (256 * 1024, 512 * 1024); let res = calc_bsize(n, m); - assert!(res % n == 0); - assert!(res % m == 0); + assert_eq!(res % n, 0); + assert_eq!(res % m, 0); assert_eq!(res, m); } @@ -1429,8 +1521,8 @@ mod tests { fn bsize_test_bs_eq() { let (n, m) = (1024, 1024); let res = calc_bsize(n, m); - assert!(res % n == 0); - assert!(res % m == 0); + assert_eq!(res % n, 0); + assert_eq!(res % m, 0); assert_eq!(res, m); } diff --git a/src/uu/dd/src/numbers.rs b/src/uu/dd/src/numbers.rs index 8a6fa5a7a37..206cd788750 100644 --- a/src/uu/dd/src/numbers.rs +++ b/src/uu/dd/src/numbers.rs @@ -2,7 +2,8 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -/// Functions for formatting a number as a magnitude and a unit suffix. + +//! Functions for formatting a number as a magnitude and a unit suffix. /// The first ten powers of 1024. const IEC_BASES: [u128; 10] = [ @@ -36,7 +37,7 @@ const SI_BASES: [u128; 10] = [ 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. +/// A `SuffixType` determines whether the suffixes are 1000 or 1024 based. #[derive(Clone, Copy)] pub(crate) enum SuffixType { Iec, @@ -82,14 +83,14 @@ pub(crate) fn to_magnitude_and_suffix(n: u128, suffix_type: SuffixType) -> Strin if quotient < 10.0 { format!("{quotient:.1} {suffix}") } else { - format!("{} {}", quotient.round(), suffix) + format!("{} {suffix}", quotient.round()) } } #[cfg(test)] mod tests { - use crate::numbers::{to_magnitude_and_suffix, SuffixType}; + use crate::numbers::{SuffixType, to_magnitude_and_suffix}; #[test] fn test_to_magnitude_and_suffix_powers_of_1024() { diff --git a/src/uu/dd/src/parseargs.rs b/src/uu/dd/src/parseargs.rs index 93d6c63a97d..e76b2c09718 100644 --- a/src/uu/dd/src/parseargs.rs +++ b/src/uu/dd/src/parseargs.rs @@ -9,28 +9,43 @@ mod unit_tests; use super::{ConversionMode, IConvFlags, IFlags, Num, OConvFlags, OFlags, Settings, StatusLevel}; use crate::conversion_tables::ConversionTable; -use std::error::Error; +use thiserror::Error; use uucore::display::Quotable; use uucore::error::UError; -use uucore::parse_size::{ParseSizeError, Parser as SizeParser}; +use uucore::parser::parse_size::{ParseSizeError, Parser as SizeParser}; use uucore::show_warning; +use uucore::translate; /// Parser Errors describe errors with parser input -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Error)] pub enum ParseError { + #[error("{}", translate!("dd-error-unrecognized-operand", "operand" => .0.clone()))] UnrecognizedOperand(String), + #[error("{}", translate!("dd-error-multiple-format-table"))] MultipleFmtTable, + #[error("{}", translate!("dd-error-multiple-case"))] MultipleUCaseLCase, + #[error("{}", translate!("dd-error-multiple-block"))] MultipleBlockUnblock, + #[error("{}", translate!("dd-error-multiple-excl"))] MultipleExclNoCreate, + #[error("{}", translate!("dd-error-invalid-flag", "flag" => .0.clone(), "cmd" => uucore::execution_phrase()))] FlagNoMatch(String), + #[error("{}", translate!("dd-error-conv-flag-no-match", "flag" => .0.clone()))] ConvFlagNoMatch(String), + #[error("{}", translate!("dd-error-multiplier-parse-failure", "input" => .0.clone()))] MultiplierStringParseFailure(String), + #[error("{}", translate!("dd-error-multiplier-overflow", "input" => .0.clone()))] MultiplierStringOverflow(String), + #[error("{}", translate!("dd-error-block-without-cbs"))] BlockUnblockWithoutCBS, + #[error("{}", translate!("dd-error-status-not-recognized", "level" => .0.clone()))] StatusLevelNotRecognized(String), + #[error("{}", translate!("dd-error-unimplemented", "feature" => .0.clone()))] Unimplemented(String), + #[error("{}", translate!("dd-error-bs-out-of-range", "param" => .0.clone()))] BsOutOfRange(String), + #[error("{}", translate!("dd-error-invalid-number", "input" => .0.clone()))] InvalidNumber(String), } @@ -112,13 +127,19 @@ impl Parser { Self::default() } - pub(crate) fn parse(self, operands: &[&str]) -> Result { + pub(crate) fn parse( + self, + operands: impl IntoIterator>, + ) -> Result { self.read(operands)?.validate() } - pub(crate) fn read(mut self, operands: &[&str]) -> Result { + pub(crate) fn read( + mut self, + operands: impl IntoIterator>, + ) -> Result { for operand in operands { - self.parse_operand(operand)?; + self.parse_operand(operand.as_ref())?; } Ok(self) @@ -396,69 +417,6 @@ impl Parser { } } -impl std::fmt::Display for ParseError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::UnrecognizedOperand(arg) => { - write!(f, "Unrecognized operand '{arg}'") - } - Self::MultipleFmtTable => { - write!( - f, - "Only one of conv=ascii conv=ebcdic or conv=ibm may be specified" - ) - } - Self::MultipleUCaseLCase => { - write!(f, "Only one of conv=lcase or conv=ucase may be specified") - } - Self::MultipleBlockUnblock => { - write!(f, "Only one of conv=block or conv=unblock may be specified") - } - Self::MultipleExclNoCreate => { - write!(f, "Only one ov conv=excl or conv=nocreat may be specified") - } - Self::FlagNoMatch(arg) => { - // Additional message about 'dd --help' is displayed only in this situation. - write!( - f, - "invalid input flag: ‘{}’\nTry '{} --help' for more information.", - arg, - uucore::execution_phrase() - ) - } - Self::ConvFlagNoMatch(arg) => { - write!(f, "Unrecognized conv=CONV -> {arg}") - } - Self::MultiplierStringParseFailure(arg) => { - write!(f, "Unrecognized byte multiplier -> {arg}") - } - Self::MultiplierStringOverflow(arg) => { - write!( - f, - "Multiplier string would overflow on current system -> {arg}" - ) - } - Self::BlockUnblockWithoutCBS => { - write!(f, "conv=block or conv=unblock specified without cbs=N") - } - Self::StatusLevelNotRecognized(arg) => { - write!(f, "status=LEVEL not recognized -> {arg}") - } - Self::BsOutOfRange(arg) => { - write!(f, "{arg}=N cannot fit into memory") - } - Self::Unimplemented(arg) => { - write!(f, "feature not implemented on this system -> {arg}") - } - Self::InvalidNumber(arg) => { - write!(f, "invalid number: ‘{arg}’") - } - } - } -} - -impl Error for ParseError {} - impl UError for ParseError { fn code(&self) -> i32 { 1 @@ -467,15 +425,15 @@ impl UError for ParseError { fn show_zero_multiplier_warning() { show_warning!( - "{} is a zero multiplier; use {} if that is intended", - "0x".quote(), - "00x".quote() + "{}", + translate!("dd-warning-zero-multiplier", "zero" => "0x".quote(), "alternative" => "00x".quote()) ); } -/// Parse bytes using str::parse, then map error if needed. -fn parse_bytes_only(s: &str) -> Result { - s.parse() +/// Parse bytes using [`str::parse`], then map error if needed. +fn parse_bytes_only(s: &str, i: usize) -> Result { + s[..i] + .parse() .map_err(|_| ParseError::MultiplierStringParseFailure(s.to_string())) } @@ -490,7 +448,7 @@ fn parse_bytes_only(s: &str) -> Result { /// 512. You can also use standard block size suffixes like `'k'` for /// 1024. /// -/// If the number would be too large, return [`std::u64::MAX`] instead. +/// If the number would be too large, return [`u64::MAX`] instead. /// /// # Errors /// @@ -516,13 +474,11 @@ fn parse_bytes_no_x(full: &str, s: &str) -> Result { (None, None, None) => match parser.parse_u64(s) { Ok(n) => (n, 1), Err(ParseSizeError::SizeTooBig(_)) => (u64::MAX, 1), - Err(ParseSizeError::InvalidSuffix(_) | ParseSizeError::ParseFailure(_)) => { - return Err(ParseError::InvalidNumber(full.to_string())) - } + Err(_) => return Err(ParseError::InvalidNumber(full.to_string())), }, - (Some(i), None, None) => (parse_bytes_only(&s[..i])?, 1), - (None, Some(i), None) => (parse_bytes_only(&s[..i])?, 2), - (None, None, Some(i)) => (parse_bytes_only(&s[..i])?, 512), + (Some(i), None, None) => (parse_bytes_only(s, i)?, 1), + (None, Some(i), None) => (parse_bytes_only(s, i)?, 2), + (None, None, Some(i)) => (parse_bytes_only(s, i)?, 512), _ => return Err(ParseError::MultiplierStringParseFailure(full.to_string())), }; num.checked_mul(multiplier) @@ -530,7 +486,7 @@ fn parse_bytes_no_x(full: &str, s: &str) -> Result { } /// Parse byte and multiplier like 512, 5KiB, or 1G. -/// Uses uucore::parse_size, and adds the 'w' and 'c' suffixes which are mentioned +/// Uses [`uucore::parser::parse_size`], and adds the 'w' and 'c' suffixes which are mentioned /// in dd's info page. pub fn parse_bytes_with_opt_multiplier(s: &str) -> Result { // TODO On my Linux system, there seems to be a maximum block size of 4096 bytes: @@ -631,8 +587,8 @@ fn conversion_mode( #[cfg(test)] mod tests { - use crate::parseargs::{parse_bytes_with_opt_multiplier, Parser}; use crate::Num; + use crate::parseargs::{Parser, parse_bytes_with_opt_multiplier}; use std::matches; const BIG: &str = "9999999999999999999999999999999999999999999999999999999999999"; diff --git a/src/uu/dd/src/parseargs/unit_tests.rs b/src/uu/dd/src/parseargs/unit_tests.rs index baeafdd56ae..cde0ef0cc1f 100644 --- a/src/uu/dd/src/parseargs/unit_tests.rs +++ b/src/uu/dd/src/parseargs/unit_tests.rs @@ -6,11 +6,11 @@ use super::*; +use crate::StatusLevel; use crate::conversion_tables::{ ASCII_TO_EBCDIC_UCASE_TO_LCASE, ASCII_TO_IBM, EBCDIC_TO_ASCII_LCASE_TO_UCASE, }; use crate::parseargs::Parser; -use crate::StatusLevel; #[cfg(not(any(target_os = "linux", target_os = "android")))] #[allow(clippy::useless_vec)] @@ -29,29 +29,20 @@ fn unimplemented_flags_should_error_non_linux() { "noctty", "nofollow", ] { - let args = vec![format!("iflag={}", flag)]; - - if Parser::new() - .parse(&args.iter().map(AsRef::as_ref).collect::>()[..]) - .is_ok() - { - succeeded.push(format!("iflag={}", flag)); + let arg = format!("iflag={flag}"); + if Parser::new().parse([&arg]).is_ok() { + succeeded.push(arg); } - let args = vec![format!("oflag={}", flag)]; - - if Parser::new() - .parse(&args.iter().map(AsRef::as_ref).collect::>()[..]) - .is_ok() - { - succeeded.push(format!("iflag={}", flag)); + let arg = format!("oflag={flag}"); + if Parser::new().parse([&arg]).is_ok() { + succeeded.push(arg); } } assert!( succeeded.is_empty(), - "The following flags did not panic as expected: {:?}", - succeeded + "The following flags did not panic as expected: {succeeded:?}", ); } @@ -62,22 +53,14 @@ fn unimplemented_flags_should_error() { // The following flags are not implemented for flag in ["cio", "nolinks", "text", "binary"] { - let args = vec![format!("iflag={flag}")]; - - if Parser::new() - .parse(&args.iter().map(AsRef::as_ref).collect::>()[..]) - .is_ok() - { - succeeded.push(format!("iflag={flag}")); + let arg = format!("iflag={flag}"); + if Parser::new().parse([&arg]).is_ok() { + succeeded.push(arg); } - let args = vec![format!("oflag={flag}")]; - - if Parser::new() - .parse(&args.iter().map(AsRef::as_ref).collect::>()[..]) - .is_ok() - { - succeeded.push(format!("iflag={flag}")); + let arg = format!("oflag={flag}"); + if Parser::new().parse([&arg]).is_ok() { + succeeded.push(arg); } } @@ -89,14 +72,14 @@ fn unimplemented_flags_should_error() { #[test] fn test_status_level_absent() { - let args = &["if=foo.file", "of=bar.file"]; + let args = ["if=foo.file", "of=bar.file"]; assert_eq!(Parser::new().parse(args).unwrap().status, None); } #[test] fn test_status_level_none() { - let args = &["status=none", "if=foo.file", "of=bar.file"]; + let args = ["status=none", "if=foo.file", "of=bar.file"]; assert_eq!( Parser::new().parse(args).unwrap().status, @@ -107,7 +90,7 @@ fn test_status_level_none() { #[test] #[allow(clippy::cognitive_complexity)] fn test_all_top_level_args_no_leading_dashes() { - let args = &[ + let args = [ "if=foo.file", "of=bar.file", "ibs=10", @@ -157,7 +140,7 @@ fn test_all_top_level_args_no_leading_dashes() { ); // no conv flags apply to output - assert_eq!(settings.oconv, OConvFlags::default(),); + assert_eq!(settings.oconv, OConvFlags::default()); // iconv=count_bytes,skip_bytes assert_eq!( @@ -182,7 +165,7 @@ fn test_all_top_level_args_no_leading_dashes() { #[test] fn test_status_level_progress() { - let args = &["if=foo.file", "of=bar.file", "status=progress"]; + let args = ["if=foo.file", "of=bar.file", "status=progress"]; let settings = Parser::new().parse(args).unwrap(); @@ -191,7 +174,7 @@ fn test_status_level_progress() { #[test] fn test_status_level_noxfer() { - let args = &["if=foo.file", "status=noxfer", "of=bar.file"]; + let args = ["if=foo.file", "status=noxfer", "of=bar.file"]; let settings = Parser::new().parse(args).unwrap(); @@ -200,7 +183,7 @@ fn test_status_level_noxfer() { #[test] fn test_multiple_flags_options() { - let args = &[ + let args = [ "iflag=fullblock,count_bytes", "iflag=skip_bytes", "oflag=append", @@ -247,7 +230,7 @@ fn test_multiple_flags_options() { #[test] fn test_override_multiple_options() { - let args = &[ + let args = [ "if=foo.file", "if=correct.file", "of=bar.file", @@ -289,31 +272,31 @@ fn test_override_multiple_options() { #[test] fn icf_ctable_error() { - let args = &["conv=ascii,ebcdic,ibm"]; + let args = ["conv=ascii,ebcdic,ibm"]; assert!(Parser::new().parse(args).is_err()); } #[test] fn icf_case_error() { - let args = &["conv=ucase,lcase"]; + let args = ["conv=ucase,lcase"]; assert!(Parser::new().parse(args).is_err()); } #[test] fn icf_block_error() { - let args = &["conv=block,unblock"]; + let args = ["conv=block,unblock"]; assert!(Parser::new().parse(args).is_err()); } #[test] fn icf_creat_error() { - let args = &["conv=excl,nocreat"]; + let args = ["conv=excl,nocreat"]; assert!(Parser::new().parse(args).is_err()); } #[test] fn parse_icf_token_ibm() { - let args = &["conv=ibm"]; + let args = ["conv=ibm"]; let settings = Parser::new().parse(args).unwrap(); assert_eq!( @@ -327,7 +310,7 @@ fn parse_icf_token_ibm() { #[test] fn parse_icf_tokens_elu() { - let args = &["conv=ebcdic,lcase"]; + let args = ["conv=ebcdic,lcase"]; let settings = Parser::new().parse(args).unwrap(); assert_eq!( @@ -341,7 +324,9 @@ fn parse_icf_tokens_elu() { #[test] fn parse_icf_tokens_remaining() { - let args = &["conv=ascii,ucase,block,sparse,swab,sync,noerror,excl,nocreat,notrunc,noerror,fdatasync,fsync"]; + let args = [ + "conv=ascii,ucase,block,sparse,swab,sync,noerror,excl,nocreat,notrunc,noerror,fdatasync,fsync", + ]; assert_eq!( Parser::new().read(args), Ok(Parser { @@ -368,7 +353,7 @@ fn parse_icf_tokens_remaining() { #[test] fn parse_iflag_tokens() { - let args = &["iflag=fullblock,count_bytes,skip_bytes"]; + let args = ["iflag=fullblock,count_bytes,skip_bytes"]; assert_eq!( Parser::new().read(args), Ok(Parser { @@ -385,7 +370,7 @@ fn parse_iflag_tokens() { #[test] fn parse_oflag_tokens() { - let args = &["oflag=append,seek_bytes"]; + let args = ["oflag=append,seek_bytes"]; assert_eq!( Parser::new().read(args), Ok(Parser { @@ -402,7 +387,7 @@ fn parse_oflag_tokens() { #[cfg(any(target_os = "linux", target_os = "android"))] #[test] fn parse_iflag_tokens_linux() { - let args = &["iflag=direct,directory,dsync,sync,nonblock,noatime,noctty,nofollow"]; + let args = ["iflag=direct,directory,dsync,sync,nonblock,noatime,noctty,nofollow"]; assert_eq!( Parser::new().read(args), Ok(Parser { @@ -425,7 +410,7 @@ fn parse_iflag_tokens_linux() { #[cfg(any(target_os = "linux", target_os = "android"))] #[test] fn parse_oflag_tokens_linux() { - let args = &["oflag=direct,directory,dsync,sync,nonblock,noatime,noctty,nofollow"]; + let args = ["oflag=direct,directory,dsync,sync,nonblock,noatime,noctty,nofollow"]; assert_eq!( Parser::new().read(args), Ok(Parser { diff --git a/src/uu/dd/src/progress.rs b/src/uu/dd/src/progress.rs index 238b2ab39a2..b8bfe327c68 100644 --- a/src/uu/dd/src/progress.rs +++ b/src/uu/dd/src/progress.rs @@ -11,27 +11,27 @@ //! updater that runs in its own thread. use std::io::Write; use std::sync::mpsc; +#[cfg(target_os = "linux")] +use std::thread::JoinHandle; use std::time::Duration; +#[cfg(target_os = "linux")] +use signal_hook::iterator::Handle; use uucore::{ error::UResult, format::num_format::{FloatVariant, Formatter}, + locale::setup_localization, + translate, }; -use crate::numbers::{to_magnitude_and_suffix, SuffixType}; +use crate::numbers::{SuffixType, to_magnitude_and_suffix}; -// On Linux, we register a signal handler that prints progress updates. -#[cfg(target_os = "linux")] -use signal_hook::consts::signal; -#[cfg(target_os = "linux")] -use std::{ - env, - error::Error, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, - }, -}; +#[derive(PartialEq, Eq)] +pub(crate) enum ProgUpdateType { + Periodic, + Signal, + Final, +} /// Summary statistics for read and write progress of dd for a given duration. pub(crate) struct ProgUpdate { @@ -53,7 +53,7 @@ pub(crate) struct ProgUpdate { /// The status of the write. /// /// True if the write is completed, false if still in-progress. - pub(crate) complete: bool, + pub(crate) update_type: ProgUpdateType, } impl ProgUpdate { @@ -62,13 +62,13 @@ impl ProgUpdate { read_stat: ReadStat, write_stat: WriteStat, duration: Duration, - complete: bool, + update_type: ProgUpdateType, ) -> Self { Self { read_stat, write_stat, duration, - complete, + update_type, } } @@ -104,8 +104,10 @@ impl ProgUpdate { self.write_stat.report(w)?; match self.read_stat.records_truncated { 0 => {} - 1 => writeln!(w, "1 truncated record")?, - n => writeln!(w, "{n} truncated records")?, + count => { + let message = translate!("dd-progress-truncated-record", "count" => count); + writeln!(w, "{message}")?; + } } Ok(()) } @@ -159,31 +161,29 @@ impl ProgUpdate { variant: FloatVariant::Shortest, ..Default::default() } - .fmt(&mut duration_str, duration)?; + .fmt(&mut duration_str, &duration.into())?; // We assume that printf will output valid UTF-8 let duration_str = std::str::from_utf8(&duration_str).unwrap(); // If the number of bytes written is sufficiently large, then // print a more concise representation of the number, like // "1.2 kB" and "1.0 KiB". - match btotal { - 1 => write!( - w, - "{carriage_return}{btotal} byte copied, {duration_str} s, {transfer_rate}/s{newline}", - )?, - 0..=999 => write!( - w, - "{carriage_return}{btotal} bytes copied, {duration_str} s, {transfer_rate}/s{newline}", - )?, - 1000..=1023 => write!( - w, - "{carriage_return}{btotal} bytes ({btotal_metric}) copied, {duration_str} s, {transfer_rate}/s{newline}", - )?, - _ => write!( - w, - "{carriage_return}{btotal} bytes ({btotal_metric}, {btotal_bin}) copied, {duration_str} s, {transfer_rate}/s{newline}", - )?, + let message = match btotal { + 1 => { + translate!("dd-progress-byte-copied", "bytes" => btotal, "duration" => duration_str, "rate" => transfer_rate) + } + 0..=999 => { + translate!("dd-progress-bytes-copied", "bytes" => btotal, "duration" => duration_str, "rate" => transfer_rate) + } + 1000..=1023 => { + translate!("dd-progress-bytes-copied-si", "bytes" => btotal, "si" => btotal_metric, "duration" => duration_str, "rate" => transfer_rate) + } + _ => { + translate!("dd-progress-bytes-copied-si-iec", "bytes" => btotal, "si" => btotal_metric, "iec" => btotal_bin, "duration" => duration_str, "rate" => transfer_rate) + } }; + + write!(w, "{carriage_return}{message}{newline}")?; Ok(()) } @@ -313,11 +313,8 @@ impl ReadStat { /// /// If there is a problem writing to `w`. fn report(&self, w: &mut impl Write) -> std::io::Result<()> { - writeln!( - w, - "{}+{} records in", - self.reads_complete, self.reads_partial - )?; + let message = translate!("dd-progress-records-in", "complete" => self.reads_complete, "partial" => self.reads_partial); + writeln!(w, "{message}")?; Ok(()) } } @@ -370,11 +367,8 @@ impl WriteStat { /// /// If there is a problem writing to `w`. fn report(&self, w: &mut impl Write) -> std::io::Result<()> { - writeln!( - w, - "{}+{} records out", - self.writes_complete, self.writes_partial - ) + let message = translate!("dd-progress-records-out", "complete" => self.writes_complete, "partial" => self.writes_partial); + writeln!(w, "{message}") } } @@ -430,10 +424,13 @@ pub(crate) fn gen_prog_updater( print_level: Option, ) -> impl Fn() { move || { + // As we are in a thread, we need to set up localization independently. + let _ = setup_localization("dd"); + let mut progress_printed = false; while let Ok(update) = rx.recv() { // Print the final read/write statistics. - if update.complete { + if update.update_type == ProgUpdateType::Final { update.print_final_stats(print_level, progress_printed); return; } @@ -445,6 +442,49 @@ pub(crate) fn gen_prog_updater( } } +/// signal handler listens for SIGUSR1 signal and runs provided closure. +#[cfg(target_os = "linux")] +pub(crate) struct SignalHandler { + handle: Handle, + thread: Option>, +} + +#[cfg(target_os = "linux")] +impl SignalHandler { + pub(crate) fn install_signal_handler( + f: Box, + ) -> Result { + use signal_hook::consts::signal::*; + use signal_hook::iterator::Signals; + + let mut signals = Signals::new([SIGUSR1])?; + let handle = signals.handle(); + let thread = std::thread::spawn(move || { + for signal in &mut signals { + match signal { + SIGUSR1 => (*f)(), + _ => unreachable!(), + } + } + }); + + Ok(Self { + handle, + thread: Some(thread), + }) + } +} + +#[cfg(target_os = "linux")] +impl Drop for SignalHandler { + fn drop(&mut self) { + self.handle.close(); + if let Some(thread) = std::mem::take(&mut self.thread) { + thread.join().unwrap(); + } + } +} + /// Return a closure that can be used in its own thread to print progress info. /// /// This function returns a closure that receives [`ProgUpdate`] @@ -459,50 +499,34 @@ pub(crate) fn gen_prog_updater( rx: mpsc::Receiver, print_level: Option, ) -> impl Fn() { - // TODO: SIGINFO: Trigger progress line reprint. BSD-style Linux only. - const SIGUSR1_USIZE: usize = signal::SIGUSR1 as usize; - fn posixly_correct() -> bool { - env::var("POSIXLY_CORRECT").is_ok() - } - fn register_linux_signal_handler(sigval: Arc) -> Result<(), Box> { - if !posixly_correct() { - signal_hook::flag::register_usize(signal::SIGUSR1, sigval, SIGUSR1_USIZE)?; - } - - Ok(()) - } // -------------------------------------------------------------- move || { - let sigval = Arc::new(AtomicUsize::new(0)); - - register_linux_signal_handler(sigval.clone()).unwrap_or_else(|e| { - if Some(StatusLevel::None) != print_level { - eprintln!("Internal dd Warning: Unable to register signal handler \n\t{e}"); - } - }); + // As we are in a thread, we need to set up localization independently. + let _ = setup_localization("dd"); // Holds the state of whether we have printed the current progress. // This is needed so that we know whether or not to print a newline // character before outputting non-progress data. let mut progress_printed = false; while let Ok(update) = rx.recv() { - // Print the final read/write statistics. - if update.complete { - update.print_final_stats(print_level, progress_printed); - return; - } - // (Re)print status line if progress is requested. - if Some(StatusLevel::Progress) == print_level && !update.complete { - update.reprint_prog_line(); - progress_printed = true; - } - // Handle signals and set the signal to un-seen. - // This will print a maximum of 1 time per second, even though it - // should be printing on every SIGUSR1. - if let SIGUSR1_USIZE = sigval.swap(0, Ordering::Relaxed) { - update.print_transfer_stats(progress_printed); - // Reset the progress printed, since print_transfer_stats always prints a newline. - progress_printed = false; + match update.update_type { + ProgUpdateType::Final => { + // Print the final read/write statistics. + update.print_final_stats(print_level, progress_printed); + return; + } + ProgUpdateType::Periodic => { + // (Re)print status line if progress is requested. + if Some(StatusLevel::Progress) == print_level { + update.reprint_prog_line(); + progress_printed = true; + } + } + ProgUpdateType::Signal => { + update.print_transfer_stats(progress_printed); + // Reset the progress printed, since print_transfer_stats always prints a newline. + progress_printed = false; + } } } } @@ -510,11 +534,18 @@ pub(crate) fn gen_prog_updater( #[cfg(test)] mod tests { - + use std::env; use std::io::Cursor; use std::time::Duration; + use uucore::locale::setup_localization; use super::{ProgUpdate, ReadStat, WriteStat}; + fn init() { + unsafe { + env::set_var("LANG", "C"); + } + let _ = setup_localization("dd"); + } fn prog_update_write(n: u128) -> ProgUpdate { ProgUpdate { @@ -524,7 +555,7 @@ mod tests { ..Default::default() }, duration: Duration::new(1, 0), // one second - complete: false, + update_type: super::ProgUpdateType::Periodic, } } @@ -533,54 +564,64 @@ mod tests { read_stat: ReadStat::default(), write_stat: WriteStat::default(), duration, - complete: false, + update_type: super::ProgUpdateType::Periodic, } } #[test] fn test_read_stat_report() { + init(); let read_stat = ReadStat::new(1, 2, 3, 4); let mut cursor = Cursor::new(vec![]); read_stat.report(&mut cursor).unwrap(); - assert_eq!(cursor.get_ref(), b"1+2 records in\n"); + assert_eq!( + std::str::from_utf8(cursor.get_ref()).unwrap(), + "1+2 records in\n" + ); } #[test] fn test_write_stat_report() { + init(); let write_stat = WriteStat::new(1, 2, 3); let mut cursor = Cursor::new(vec![]); write_stat.report(&mut cursor).unwrap(); - assert_eq!(cursor.get_ref(), b"1+2 records out\n"); + assert_eq!( + std::str::from_utf8(cursor.get_ref()).unwrap(), + "1+2 records out\n" + ); } #[test] fn test_prog_update_write_io_lines() { + init(); let read_stat = ReadStat::new(1, 2, 3, 4); let write_stat = WriteStat::new(4, 5, 6); let duration = Duration::new(789, 0); - let complete = false; + let update_type = super::ProgUpdateType::Periodic; let prog_update = ProgUpdate { read_stat, write_stat, duration, - complete, + update_type, }; let mut cursor = Cursor::new(vec![]); prog_update.write_io_lines(&mut cursor).unwrap(); assert_eq!( - cursor.get_ref(), - b"1+2 records in\n4+5 records out\n3 truncated records\n" + std::str::from_utf8(cursor.get_ref()).unwrap(), + "1+2 records in\n4+5 records out\n3 truncated records\n" ); } #[test] fn test_prog_update_write_prog_line() { + init(); let prog_update = ProgUpdate { read_stat: ReadStat::default(), write_stat: WriteStat::default(), duration: Duration::new(1, 0), // one second - complete: false, + update_type: super::ProgUpdateType::Periodic, }; let mut cursor = Cursor::new(vec![]); @@ -593,91 +634,108 @@ mod tests { // 0 bytes copied, 7.9151e-05 s, 0.0 kB/s // // The throughput still does not match GNU dd. - assert_eq!(cursor.get_ref(), b"0 bytes copied, 1 s, 0.0 B/s\n"); + assert_eq!( + std::str::from_utf8(cursor.get_ref()).unwrap(), + "0 bytes copied, 1 s, 0.0 B/s\n" + ); let prog_update = prog_update_write(1); let mut cursor = Cursor::new(vec![]); prog_update.write_prog_line(&mut cursor, rewrite).unwrap(); - assert_eq!(cursor.get_ref(), b"1 byte copied, 1 s, 0.0 B/s\n"); + assert_eq!( + std::str::from_utf8(cursor.get_ref()).unwrap(), + "1 byte copied, 1 s, 0.0 B/s\n" + ); let prog_update = prog_update_write(999); let mut cursor = Cursor::new(vec![]); prog_update.write_prog_line(&mut cursor, rewrite).unwrap(); - assert_eq!(cursor.get_ref(), b"999 bytes copied, 1 s, 0.0 B/s\n"); + assert_eq!( + std::str::from_utf8(cursor.get_ref()).unwrap(), + "999 bytes copied, 1 s, 0.0 B/s\n" + ); let prog_update = prog_update_write(1000); let mut cursor = Cursor::new(vec![]); prog_update.write_prog_line(&mut cursor, rewrite).unwrap(); assert_eq!( - cursor.get_ref(), - b"1000 bytes (1.0 kB) copied, 1 s, 1.0 kB/s\n" + std::str::from_utf8(cursor.get_ref()).unwrap(), + "1000 bytes (1.0 kB) copied, 1 s, 1.0 kB/s\n" ); let prog_update = prog_update_write(1023); let mut cursor = Cursor::new(vec![]); prog_update.write_prog_line(&mut cursor, rewrite).unwrap(); assert_eq!( - cursor.get_ref(), - b"1023 bytes (1.0 kB) copied, 1 s, 1.0 kB/s\n" + std::str::from_utf8(cursor.get_ref()).unwrap(), + "1023 bytes (1.0 kB) copied, 1 s, 1.0 kB/s\n" ); let prog_update = prog_update_write(1024); let mut cursor = Cursor::new(vec![]); prog_update.write_prog_line(&mut cursor, rewrite).unwrap(); assert_eq!( - cursor.get_ref(), - b"1024 bytes (1.0 kB, 1.0 KiB) copied, 1 s, 1.0 kB/s\n" + std::str::from_utf8(cursor.get_ref()).unwrap(), + "1024 bytes (1.0 kB, 1.0 KiB) copied, 1 s, 1.0 kB/s\n" ); } #[test] fn write_transfer_stats() { + init(); let prog_update = ProgUpdate { read_stat: ReadStat::default(), write_stat: WriteStat::default(), duration: Duration::new(1, 0), // one second - complete: false, + update_type: super::ProgUpdateType::Periodic, }; let mut cursor = Cursor::new(vec![]); prog_update .write_transfer_stats(&mut cursor, false) .unwrap(); - let mut iter = cursor.get_ref().split(|v| *v == b'\n'); - assert_eq!(iter.next().unwrap(), b"0+0 records in"); - assert_eq!(iter.next().unwrap(), b"0+0 records out"); - assert_eq!(iter.next().unwrap(), b"0 bytes copied, 1 s, 0.0 B/s"); - assert_eq!(iter.next().unwrap(), b""); + let output_str = std::str::from_utf8(cursor.get_ref()).unwrap(); + let mut iter = output_str.split('\n'); + assert_eq!(iter.next().unwrap(), "0+0 records in"); + assert_eq!(iter.next().unwrap(), "0+0 records out"); + assert_eq!(iter.next().unwrap(), "0 bytes copied, 1 s, 0.0 B/s"); + assert_eq!(iter.next().unwrap(), ""); assert!(iter.next().is_none()); } #[test] fn write_final_transfer_stats() { + init(); // Tests the formatting of the final statistics written after a progress line. let prog_update = ProgUpdate { read_stat: ReadStat::default(), write_stat: WriteStat::default(), duration: Duration::new(1, 0), // one second - complete: false, + update_type: super::ProgUpdateType::Periodic, }; let mut cursor = Cursor::new(vec![]); let rewrite = true; prog_update.write_prog_line(&mut cursor, rewrite).unwrap(); prog_update.write_transfer_stats(&mut cursor, true).unwrap(); - let mut iter = cursor.get_ref().split(|v| *v == b'\n'); - assert_eq!(iter.next().unwrap(), b"\r0 bytes copied, 1 s, 0.0 B/s"); - assert_eq!(iter.next().unwrap(), b"0+0 records in"); - assert_eq!(iter.next().unwrap(), b"0+0 records out"); - assert_eq!(iter.next().unwrap(), b"0 bytes copied, 1 s, 0.0 B/s"); - assert_eq!(iter.next().unwrap(), b""); + let output_str = std::str::from_utf8(cursor.get_ref()).unwrap(); + let mut iter = output_str.split('\n'); + assert_eq!(iter.next().unwrap(), "\r0 bytes copied, 1 s, 0.0 B/s"); + assert_eq!(iter.next().unwrap(), "0+0 records in"); + assert_eq!(iter.next().unwrap(), "0+0 records out"); + assert_eq!(iter.next().unwrap(), "0 bytes copied, 1 s, 0.0 B/s"); + assert_eq!(iter.next().unwrap(), ""); assert!(iter.next().is_none()); } #[test] fn test_duration_precision() { + init(); let prog_update = prog_update_duration(Duration::from_nanos(123)); let mut cursor = Cursor::new(vec![]); let rewrite = false; prog_update.write_prog_line(&mut cursor, rewrite).unwrap(); - assert_eq!(cursor.get_ref(), b"0 bytes copied, 1.23e-07 s, 0.0 B/s\n"); + assert_eq!( + std::str::from_utf8(cursor.get_ref()).unwrap(), + "0 bytes copied, 0.000000123 s, 0.0 B/s\n" + ); } } diff --git a/src/uu/df/Cargo.toml b/src/uu/df/Cargo.toml index 788db50406d..4d837edc617 100644 --- a/src/uu/df/Cargo.toml +++ b/src/uu/df/Cargo.toml @@ -1,26 +1,31 @@ [package] name = "uu_df" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "df ~ (uutils) display file system information" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/df" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/df.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["libc", "fsext"] } +uucore = { workspace = true, features = ["libc", "fsext", "parser"] } unicode-width = { workspace = true } +thiserror = { workspace = true } +fluent = { workspace = true } [dev-dependencies] -tempfile = "3" +tempfile = { workspace = true } [[bin]] name = "df" diff --git a/src/uu/df/df.md b/src/uu/df/df.md deleted file mode 100644 index 1a192f8fd01..00000000000 --- a/src/uu/df/df.md +++ /dev/null @@ -1,18 +0,0 @@ -# df - -``` -df [OPTION]... [FILE]... -``` - -Show information about the file system on which each FILE resides, -or all file systems by default. - -## After Help - -Display values are in units of the first available SIZE from --block-size, -and the DF_BLOCK_SIZE, BLOCK_SIZE and BLOCKSIZE environment variables. -Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set). - -SIZE is an integer and optional unit (example: 10M is 10*1024*1024). -Units are K, M, G, T, P, E, Z, Y (powers of 1024) or KB, MB,... (powers -of 1000). diff --git a/src/uu/df/locales/en-US.ftl b/src/uu/df/locales/en-US.ftl new file mode 100644 index 00000000000..62bff44d88d --- /dev/null +++ b/src/uu/df/locales/en-US.ftl @@ -0,0 +1,60 @@ +df-about = Show information about the file system on which each FILE resides, + or all file systems by default. +df-usage = df [OPTION]... [FILE]... +df-after-help = Display values are in units of the first available SIZE from --block-size, + and the DF_BLOCK_SIZE, BLOCK_SIZE and BLOCKSIZE environment variables. + Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set). + + SIZE is an integer and optional unit (example: 10M is 10*1024*1024). + Units are K, M, G, T, P, E, Z, Y (powers of 1024) or KB, MB,... (powers + of 1000). + +# Help messages +df-help-print-help = Print help information. +df-help-all = include dummy file systems +df-help-block-size = scale sizes by SIZE before printing them; e.g. '-BM' prints sizes in units of 1,048,576 bytes +df-help-total = produce a grand total +df-help-human-readable = print sizes in human readable format (e.g., 1K 234M 2G) +df-help-si = likewise, but use powers of 1000 not 1024 +df-help-inodes = list inode information instead of block usage +df-help-kilo = like --block-size=1K +df-help-local = limit listing to local file systems +df-help-no-sync = do not invoke sync before getting usage info (default) +df-help-output = use output format defined by FIELD_LIST, or print all fields if FIELD_LIST is omitted. +df-help-portability = use the POSIX output format +df-help-sync = invoke sync before getting usage info (non-windows only) +df-help-type = limit listing to file systems of type TYPE +df-help-print-type = print file system type +df-help-exclude-type = limit listing to file systems not of type TYPE + +# Error messages +df-error-block-size-too-large = --block-size argument '{ $size }' too large +df-error-invalid-block-size = invalid --block-size argument { $size } +df-error-invalid-suffix = invalid suffix in --block-size argument { $size } +df-error-field-used-more-than-once = option --output: field { $field } used more than once +df-error-filesystem-type-both-selected-and-excluded = file system type { $type } both selected and excluded +df-error-no-such-file-or-directory = { $path }: No such file or directory +df-error-no-file-systems-processed = no file systems processed +df-error-cannot-access-over-mounted = cannot access { $path }: over-mounted by another device +df-error-cannot-read-table-of-mounted-filesystems = cannot read table of mounted file systems +df-error-inodes-not-supported-windows = { $program }: doesn't support -i option + +# Headers +df-header-filesystem = Filesystem +df-header-size = Size +df-header-used = Used +df-header-avail = Avail +df-header-available = Available +df-header-use-percent = Use% +df-header-capacity = Capacity +df-header-mounted-on = Mounted on +df-header-inodes = Inodes +df-header-iused = IUsed +df-header-iavail = IFree +df-header-iuse-percent = IUse% +df-header-file = File +df-header-type = Type + +# Other +df-total = total +df-blocks-suffix = -blocks diff --git a/src/uu/df/locales/fr-FR.ftl b/src/uu/df/locales/fr-FR.ftl new file mode 100644 index 00000000000..f7c8236da81 --- /dev/null +++ b/src/uu/df/locales/fr-FR.ftl @@ -0,0 +1,60 @@ +df-about = afficher des informations sur le système de fichiers sur lequel chaque FICHIER réside, + ou tous les systèmes de fichiers par défaut. +df-usage = df [OPTION]... [FICHIER]... +df-after-help = Les valeurs affichées sont en unités de la première TAILLE disponible de --block-size, + et des variables d'environnement DF_BLOCK_SIZE, BLOCK_SIZE et BLOCKSIZE. + Sinon, les unités par défaut sont 1024 octets (ou 512 si POSIXLY_CORRECT est défini). + + TAILLE est un entier et une unité optionnelle (exemple : 10M est 10*1024*1024). + Les unités sont K, M, G, T, P, E, Z, Y (puissances de 1024) ou KB, MB,... (puissances + de 1000). + +# Messages d'aide +df-help-print-help = afficher les informations d'aide. +df-help-all = inclure les systèmes de fichiers factices +df-help-block-size = mettre les tailles à l'échelle par TAILLE avant de les afficher ; par ex. '-BM' affiche les tailles en unités de 1 048 576 octets +df-help-total = produire un total général +df-help-human-readable = afficher les tailles dans un format lisible par l'homme (par ex., 1K 234M 2G) +df-help-si = pareillement, mais utiliser les puissances de 1000 pas 1024 +df-help-inodes = lister les informations d'inode au lieu de l'utilisation des blocs +df-help-kilo = comme --block-size=1K +df-help-local = limiter l'affichage aux systèmes de fichiers locaux +df-help-no-sync = ne pas invoquer sync avant d'obtenir les informations d'utilisation (par défaut) +df-help-output = utiliser le format de sortie défini par LISTE_CHAMPS, ou afficher tous les champs si LISTE_CHAMPS est omise. +df-help-portability = utiliser le format de sortie POSIX +df-help-sync = invoquer sync avant d'obtenir les informations d'utilisation (non-windows seulement) +df-help-type = limiter l'affichage aux systèmes de fichiers de type TYPE +df-help-print-type = afficher le type de système de fichiers +df-help-exclude-type = limiter l'affichage aux systèmes de fichiers pas de type TYPE + +# Messages d'erreur +df-error-block-size-too-large = argument --block-size '{ $size }' trop grand +df-error-invalid-block-size = argument --block-size invalide { $size } +df-error-invalid-suffix = suffixe invalide dans l'argument --block-size { $size } +df-error-field-used-more-than-once = option --output : champ { $field } utilisé plus d'une fois +df-error-filesystem-type-both-selected-and-excluded = type de système de fichiers { $type } à la fois sélectionné et exclu +df-error-no-such-file-or-directory = { $path } : aucun fichier ou répertoire de ce type +df-error-no-file-systems-processed = aucun système de fichiers traité +df-error-cannot-access-over-mounted = impossible d'accéder à { $path } : sur-monté par un autre périphérique +df-error-cannot-read-table-of-mounted-filesystems = impossible de lire la table des systèmes de fichiers montés +df-error-inodes-not-supported-windows = { $program } : ne supporte pas l'option -i + +# En-têtes du tableau +df-header-filesystem = Sys. de fichiers +df-header-size = Taille +df-header-used = Utilisé +df-header-avail = Disp. +df-header-available = Disponible +df-header-use-percent = Util% +df-header-capacity = Capacité +df-header-mounted-on = Monté sur +df-header-inodes = Inodes +df-header-iused = IUtil +df-header-iavail = ILibre +df-header-iuse-percent = IUtil% +df-header-file = Fichier +df-header-type = Type + +# Autres messages +df-total = total +df-blocks-suffix = -blocs diff --git a/src/uu/df/src/blocks.rs b/src/uu/df/src/blocks.rs index d7a689d8c86..57301ba5f11 100644 --- a/src/uu/df/src/blocks.rs +++ b/src/uu/df/src/blocks.rs @@ -9,7 +9,7 @@ use std::{env, fmt}; use uucore::{ display::Quotable, - parse_size::{parse_size_u64, ParseSizeError}, + parser::parse_size::{ParseSizeError, parse_size_u64}, }; /// The first ten powers of 1024. @@ -40,8 +40,8 @@ const SI_BASES: [u128; 10] = [ 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. +/// A `SuffixType` determines whether the suffixes are 1000 or 1024 based, and whether they are +/// intended for `HumanReadable` mode or not. #[derive(Clone, Copy)] pub(crate) enum SuffixType { Iec, @@ -98,9 +98,9 @@ pub(crate) fn to_magnitude_and_suffix(n: u128, suffix_type: SuffixType) -> Strin if rem % (bases[i] / 10) == 0 { format!("{quot}.{tenths_place}{suffix}") } else if tenths_place + 1 == 10 || quot >= 10 { - format!("{}{}", quot + 1, suffix) + format!("{}{suffix}", quot + 1) } else { - format!("{}.{}{}", quot, tenths_place + 1, suffix) + format!("{quot}.{}{suffix}", tenths_place + 1) } } } @@ -184,11 +184,7 @@ pub(crate) fn read_block_size(matches: &ArgMatches) -> Result Option { for env_var in ["DF_BLOCK_SIZE", "BLOCK_SIZE", "BLOCKSIZE"] { if let Ok(env_size) = env::var(env_var) { - if let Ok(size) = parse_size_u64(&env_size) { - return Some(size); - } else { - return None; - } + return parse_size_u64(&env_size).ok(); } } @@ -216,7 +212,7 @@ mod tests { use std::env; - use crate::blocks::{to_magnitude_and_suffix, BlockSize, SuffixType}; + use crate::blocks::{BlockSize, SuffixType, to_magnitude_and_suffix}; #[test] fn test_to_magnitude_and_suffix_powers_of_1024() { @@ -294,8 +290,8 @@ mod tests { #[test] fn test_default_block_size() { assert_eq!(BlockSize::Bytes(1024), BlockSize::default()); - env::set_var("POSIXLY_CORRECT", "1"); + unsafe { env::set_var("POSIXLY_CORRECT", "1") }; assert_eq!(BlockSize::Bytes(512), BlockSize::default()); - env::remove_var("POSIXLY_CORRECT"); + unsafe { env::remove_var("POSIXLY_CORRECT") }; } } diff --git a/src/uu/df/src/columns.rs b/src/uu/df/src/columns.rs index 0659d7f7da7..0d2d121a3d1 100644 --- a/src/uu/df/src/columns.rs +++ b/src/uu/df/src/columns.rs @@ -4,7 +4,9 @@ // file that was distributed with this source code. // spell-checker:ignore itotal iused iavail ipcent pcent squashfs use crate::{OPT_INODES, OPT_OUTPUT, OPT_PRINT_TYPE}; -use clap::{parser::ValueSource, ArgMatches}; +use clap::{ArgMatches, parser::ValueSource}; +use thiserror::Error; +use uucore::display::Quotable; /// The columns in the output table produced by `df`. /// @@ -56,9 +58,10 @@ pub(crate) enum Column { } /// An error while defining which columns to display in the output table. -#[derive(Debug)] +#[derive(Debug, Error)] pub(crate) enum ColumnError { /// If a column appears more than once in the `--output` argument. + #[error("{}", .0.quote())] MultipleColumns(String), } @@ -201,6 +204,7 @@ impl Column { // 14 = length of "Filesystem" plus 4 spaces Self::Source => 14, Self::Used => 5, + Self::Size => 5, // the shortest headers have a length of 4 chars so we use that as the minimum width _ => 4, } diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index c21ba98471a..3a7a10f92de 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -11,29 +11,27 @@ mod table; use blocks::HumanReadable; use clap::builder::ValueParser; use table::HeaderMode; +use uucore::LocalizedCommand; use uucore::display::Quotable; -use uucore::error::FromIo; -use uucore::error::{UError, UResult, USimpleError}; -use uucore::fsext::{read_fs_list, MountInfo}; -use uucore::parse_size::ParseSizeError; -use uucore::{format_usage, help_about, help_section, help_usage, show}; +use uucore::error::{UError, UResult, USimpleError, get_exit_code}; +use uucore::fsext::{MountInfo, read_fs_list}; +use uucore::parser::parse_size::ParseSizeError; +use uucore::translate; +use uucore::{format_usage, show}; -use clap::{crate_version, parser::ValueSource, Arg, ArgAction, ArgMatches, Command}; +use clap::{Arg, ArgAction, ArgMatches, Command, parser::ValueSource}; -use std::error::Error; use std::ffi::OsString; -use std::fmt; +use std::io::stdout; use std::path::Path; +use thiserror::Error; -use crate::blocks::{read_block_size, BlockSize}; +use crate::blocks::{BlockSize, read_block_size}; use crate::columns::{Column, ColumnError}; use crate::filesystem::Filesystem; +use crate::filesystem::FsError; use crate::table::Table; -const ABOUT: &str = help_about!("df.md"); -const USAGE: &str = help_usage!("df.md"); -const AFTER_HELP: &str = help_section!("after help", "df.md"); - static OPT_HELP: &str = "help"; static OPT_ALL: &str = "all"; static OPT_BLOCKSIZE: &str = "blocksize"; @@ -114,52 +112,35 @@ impl Default for Options { } } -#[derive(Debug)] +#[derive(Debug, Error)] enum OptionsError { + // TODO This needs to vary based on whether `--block-size` + // or `-B` were provided. + #[error("{}", translate!("df-error-block-size-too-large", "size" => .0.clone()))] BlockSizeTooLarge(String), + // TODO This needs to vary based on whether `--block-size` + // or `-B` were provided., + #[error("{}", translate!("df-error-invalid-block-size", "size" => .0.clone()))] InvalidBlockSize(String), + // TODO This needs to vary based on whether `--block-size` + // or `-B` were provided. + #[error("{}", translate!("df-error-invalid-suffix", "size" => .0.clone()))] InvalidSuffix(String), /// An error getting the columns to display in the output table. + #[error("{}", translate!("df-error-field-used-more-than-once", "field" => format!("{}", .0)))] ColumnError(ColumnError), + #[error( + "{}", + .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()) + )] FilesystemTypeBothSelectedAndExcluded(Vec), } -impl fmt::Display for OptionsError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - // TODO This needs to vary based on whether `--block-size` - // or `-B` were provided. - Self::BlockSizeTooLarge(s) => { - write!(f, "--block-size argument {} too large", s.quote()) - } - // TODO This needs to vary based on whether `--block-size` - // or `-B` were provided. - Self::InvalidBlockSize(s) => write!(f, "invalid --block-size argument {s}"), - // TODO This needs to vary based on whether `--block-size` - // or `-B` were provided. - Self::InvalidSuffix(s) => write!(f, "invalid suffix in --block-size argument {s}"), - Self::ColumnError(ColumnError::MultipleColumns(s)) => write!( - f, - "option --output: field {} used more than once", - s.quote() - ), - #[allow(clippy::print_in_format_impl)] - Self::FilesystemTypeBothSelectedAndExcluded(types) => { - for t in types { - eprintln!( - "{}: file system type {} both selected and excluded", - uucore::util_name(), - t.quote() - ); - } - Ok(()) - } - } - } -} - impl Options { /// Convert command-line arguments into [`Options`]. fn from(matches: &ArgMatches) -> Result { @@ -189,6 +170,7 @@ impl Options { .to_string(), ), ParseSizeError::ParseFailure(s) => OptionsError::InvalidBlockSize(s), + ParseSizeError::PhysicalMem(s) => OptionsError::InvalidBlockSize(s), })?, header_mode: { if matches.get_flag(OPT_HUMAN_READABLE_BINARY) @@ -242,7 +224,10 @@ fn is_included(mi: &MountInfo, opt: &Options) -> bool { } // Don't show pseudo filesystems unless `--all` has been given. - if mi.dummy && !opt.show_all_fs { + // The "lofs" filesystem is a loopback + // filesystem present on Solaris and FreeBSD systems. It + // is similar to a symbolic link. + if (mi.dummy || mi.fs_type == "lofs") && !opt.show_all_fs { return false; } @@ -305,35 +290,11 @@ fn is_best(previous: &[MountInfo], mi: &MountInfo) -> bool { true } -/// Keep only the specified subset of [`MountInfo`] instances. -/// -/// The `opt` argument specifies a variety of ways of excluding -/// [`MountInfo`] instances; see [`Options`] for more information. -/// -/// Finally, if there are duplicate entries, the one with the shorter -/// path is kept. - -fn filter_mount_list(vmi: Vec, opt: &Options) -> Vec { - let mut result = vec![]; - for mi in vmi { - // TODO The running time of the `is_best()` function is linear - // in the length of `result`. That makes the running time of - // this loop quadratic in the length of `vmi`. This could be - // improved by a more efficient implementation of `is_best()`, - // but `vmi` is probably not very long in practice. - if is_included(&mi, opt) && is_best(&result, &mi) { - result.push(mi); - } - } - result -} - /// Get all currently mounted filesystems. /// /// `opt` excludes certain filesystems from consideration and allows for the synchronization of filesystems before running; see /// [`Options`] for more information. - -fn get_all_filesystems(opt: &Options) -> Result, std::io::Error> { +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")))] @@ -345,117 +306,144 @@ fn get_all_filesystems(opt: &Options) -> Result, std::io::Error> } } - // The list of all mounted filesystems. - // - // Filesystems excluded by the command-line options are - // not considered. - let mounts: Vec = filter_mount_list(read_fs_list()?, opt); + let mut mounts = vec![]; + for mi in read_fs_list()? { + // TODO The running time of the `is_best()` function is linear + // in the length of `result`. That makes the running time of + // this loop quadratic in the length of `vmi`. This could be + // improved by a more efficient implementation of `is_best()`, + // but `vmi` is probably not very long in practice. + if is_included(&mi, opt) && is_best(&mounts, &mi) { + mounts.push(mi); + } + } // Convert each `MountInfo` into a `Filesystem`, which contains // both the mount information and usage information. - Ok(mounts - .into_iter() - .filter_map(|m| Filesystem::new(m, None)) - .filter(|fs| opt.show_all_fs || fs.usage.blocks > 0) - .collect()) + #[cfg(not(windows))] + { + let maybe_mount = |m| Filesystem::from_mount(&mounts, &m, None).ok(); + Ok(mounts + .clone() + .into_iter() + .filter_map(maybe_mount) + .filter(|fs| opt.show_all_fs || fs.usage.blocks > 0) + .collect()) + } + #[cfg(windows)] + { + let maybe_mount = |m| Filesystem::from_mount(&m, None).ok(); + Ok(mounts + .into_iter() + .filter_map(maybe_mount) + .filter(|fs| opt.show_all_fs || fs.usage.blocks > 0) + .collect()) + } } /// For each path, get the filesystem that contains that path. -fn get_named_filesystems

(paths: &[P], opt: &Options) -> Result, std::io::Error> +fn get_named_filesystems

(paths: &[P], opt: &Options) -> UResult> where P: AsRef, { // The list of all mounted filesystems. - // - // Filesystems marked as `dummy` or of type "lofs" are not - // considered. The "lofs" filesystem is a loopback - // filesystem present on Solaris and FreeBSD systems. It - // is similar to a symbolic link. - let mounts: Vec = filter_mount_list(read_fs_list()?, opt) - .into_iter() - .filter(|mi| mi.fs_type != "lofs" && !mi.dummy) - .collect(); + let mounts: Vec = read_fs_list()?; let mut result = vec![]; - // this happens if the file system type doesn't exist - if mounts.is_empty() { - show!(USimpleError::new(1, "no file systems processed")); - return Ok(result); - } - // Convert each path into a `Filesystem`, which contains // both the mount information and usage information. for path in paths { match Filesystem::from_path(&mounts, path) { - Some(fs) => result.push(fs), - None => { - // this happens if specified file system type != file system type of the file - if path.as_ref().exists() { - show!(USimpleError::new(1, "no file systems processed")); - } else { - show!(USimpleError::new( - 1, - format!("{}: No such file or directory", path.as_ref().display()) - )); + Ok(fs) => { + if is_included(&fs.mount_info, opt) { + result.push(fs); } } + Err(FsError::InvalidPath) => { + show!(USimpleError::new( + 1, + translate!("df-error-no-such-file-or-directory", "path" => path.as_ref().display()) + )); + } + Err(FsError::MountMissing) => { + show!(USimpleError::new( + 1, + translate!("df-error-no-file-systems-processed") + )); + } + #[cfg(not(windows))] + Err(FsError::OverMounted) => { + show!(USimpleError::new( + 1, + translate!("df-error-cannot-access-over-mounted", "path" => path.as_ref().quote()) + )); + } } } + if get_exit_code() == 0 && result.is_empty() { + show!(USimpleError::new( + 1, + translate!("df-error-no-file-systems-processed") + )); + return Ok(result); + } + Ok(result) } -#[derive(Debug)] +#[derive(Debug, Error)] enum DfError { /// A problem while parsing command-line options. + #[error("{}", .0)] OptionsError(OptionsError), } -impl Error for DfError {} - impl UError for DfError { fn usage(&self) -> bool { matches!(self, Self::OptionsError(OptionsError::ColumnError(_))) } } -impl fmt::Display for DfError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::OptionsError(e) => e.fmt(f), - } - } -} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); #[cfg(windows)] { if matches.get_flag(OPT_INODES) { - println!("{}: doesn't support -i option", uucore::util_name()); + println!( + "{}", + translate!("df-error-inodes-not-supported-windows", "program" => uucore::util_name()) + ); return Ok(()); } } let opt = Options::from(&matches).map_err(DfError::OptionsError)?; // Get the list of filesystems to display in the output table. - let filesystems: Vec = match matches.get_many::(OPT_PATHS) { + let filesystems: Vec = match matches.get_many::(OPT_PATHS) { None => { - let filesystems = get_all_filesystems(&opt) - .map_err_context(|| "cannot read table of mounted file systems".into())?; + let filesystems = get_all_filesystems(&opt).map_err(|e| { + let context = translate!("df-error-cannot-read-table-of-mounted-filesystems"); + USimpleError::new(e.code(), format!("{context}: {e}")) + })?; if filesystems.is_empty() { - return Err(USimpleError::new(1, "no file systems processed")); + return Err(USimpleError::new( + 1, + translate!("df-error-no-file-systems-processed"), + )); } filesystems } Some(paths) => { let paths: Vec<_> = paths.collect(); - let filesystems = get_named_filesystems(&paths, &opt) - .map_err_context(|| "cannot read table of mounted file systems".into())?; + let filesystems = get_named_filesystems(&paths, &opt).map_err(|e| { + let context = translate!("df-error-cannot-read-table-of-mounted-filesystems"); + USimpleError::new(e.code(), format!("{context}: {e}")) + })?; // This can happen if paths are given as command-line arguments // but none of the paths exist. @@ -467,23 +455,24 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } }; - println!("{}", Table::new(&opt, filesystems)); + Table::new(&opt, filesystems).write_to(&mut stdout())?; Ok(()) } pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) - .about(ABOUT) - .override_usage(format_usage(USAGE)) - .after_help(AFTER_HELP) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) + .about(translate!("df-about")) + .override_usage(format_usage(&translate!("df-usage"))) + .after_help(translate!("df-after-help")) .infer_long_args(true) .disable_help_flag(true) .arg( Arg::new(OPT_HELP) .long(OPT_HELP) - .help("Print help information.") + .help(translate!("df-help-print-help")) .action(ArgAction::Help), ) .arg( @@ -491,7 +480,7 @@ pub fn uu_app() -> Command { .short('a') .long("all") .overrides_with(OPT_ALL) - .help("include dummy file systems") + .help(translate!("df-help-all")) .action(ArgAction::SetTrue), ) .arg( @@ -500,16 +489,13 @@ pub fn uu_app() -> Command { .long("block-size") .value_name("SIZE") .overrides_with_all([OPT_KILO, OPT_BLOCKSIZE]) - .help( - "scale sizes by SIZE before printing them; e.g.\ - '-BM' prints sizes in units of 1,048,576 bytes", - ), + .help(translate!("df-help-block-size")), ) .arg( Arg::new(OPT_TOTAL) .long("total") .overrides_with(OPT_TOTAL) - .help("produce a grand total") + .help(translate!("df-help-total")) .action(ArgAction::SetTrue), ) .arg( @@ -517,7 +503,7 @@ pub fn uu_app() -> Command { .short('h') .long("human-readable") .overrides_with_all([OPT_HUMAN_READABLE_DECIMAL, OPT_HUMAN_READABLE_BINARY]) - .help("print sizes in human readable format (e.g., 1K 234M 2G)") + .help(translate!("df-help-human-readable")) .action(ArgAction::SetTrue), ) .arg( @@ -525,7 +511,7 @@ pub fn uu_app() -> Command { .short('H') .long("si") .overrides_with_all([OPT_HUMAN_READABLE_BINARY, OPT_HUMAN_READABLE_DECIMAL]) - .help("likewise, but use powers of 1000 not 1024") + .help(translate!("df-help-si")) .action(ArgAction::SetTrue), ) .arg( @@ -533,13 +519,13 @@ pub fn uu_app() -> Command { .short('i') .long("inodes") .overrides_with(OPT_INODES) - .help("list inode information instead of block usage") + .help(translate!("df-help-inodes")) .action(ArgAction::SetTrue), ) .arg( Arg::new(OPT_KILO) .short('k') - .help("like --block-size=1K") + .help(translate!("df-help-kilo")) .overrides_with_all([OPT_BLOCKSIZE, OPT_KILO]) .action(ArgAction::SetTrue), ) @@ -548,14 +534,14 @@ pub fn uu_app() -> Command { .short('l') .long("local") .overrides_with(OPT_LOCAL) - .help("limit listing to local file systems") + .help(translate!("df-help-local")) .action(ArgAction::SetTrue), ) .arg( Arg::new(OPT_NO_SYNC) .long("no-sync") .overrides_with_all([OPT_SYNC, OPT_NO_SYNC]) - .help("do not invoke sync before getting usage info (default)") + .help(translate!("df-help-no-sync")) .action(ArgAction::SetTrue), ) .arg( @@ -570,24 +556,21 @@ pub fn uu_app() -> Command { .default_missing_values(OUTPUT_FIELD_LIST) .default_values(["source", "size", "used", "avail", "pcent", "target"]) .conflicts_with_all([OPT_INODES, OPT_PORTABILITY, OPT_PRINT_TYPE]) - .help( - "use the output format defined by FIELD_LIST, \ - or print all fields if FIELD_LIST is omitted.", - ), + .help(translate!("df-help-output")), ) .arg( Arg::new(OPT_PORTABILITY) .short('P') .long("portability") .overrides_with(OPT_PORTABILITY) - .help("use the POSIX output format") + .help(translate!("df-help-portability")) .action(ArgAction::SetTrue), ) .arg( Arg::new(OPT_SYNC) .long("sync") .overrides_with_all([OPT_NO_SYNC, OPT_SYNC]) - .help("invoke sync before getting usage info (non-windows only)") + .help(translate!("df-help-sync")) .action(ArgAction::SetTrue), ) .arg( @@ -597,14 +580,14 @@ pub fn uu_app() -> Command { .value_parser(ValueParser::os_string()) .value_name("TYPE") .action(ArgAction::Append) - .help("limit listing to file systems of type TYPE"), + .help(translate!("df-help-type")), ) .arg( Arg::new(OPT_PRINT_TYPE) .short('T') .long("print-type") .overrides_with(OPT_PRINT_TYPE) - .help("print file system type") + .help(translate!("df-help-print-type")) .action(ArgAction::SetTrue), ) .arg( @@ -615,11 +598,12 @@ pub fn uu_app() -> Command { .value_parser(ValueParser::os_string()) .value_name("TYPE") .use_value_delimiter(true) - .help("limit listing to file systems not of type TYPE"), + .help(translate!("df-help-exclude-type")), ) .arg( Arg::new(OPT_PATHS) .action(ArgAction::Append) + .value_parser(ValueParser::os_string()) .value_hint(clap::ValueHint::AnyPath), ) } @@ -638,9 +622,9 @@ mod tests { dev_id: String::new(), dev_name: String::from(dev_name), fs_type: String::new(), - mount_dir: String::from(mount_dir), + mount_dir: mount_dir.into(), mount_option: String::new(), - mount_root: String::from(mount_root), + mount_root: mount_root.into(), remote: false, dummy: false, } @@ -688,9 +672,9 @@ mod tests { dev_id: String::from(dev_id), dev_name: String::new(), fs_type: String::new(), - mount_dir: String::from(mount_dir), + mount_dir: mount_dir.into(), mount_option: String::new(), - mount_root: String::new(), + mount_root: "/".into(), remote: false, dummy: false, } @@ -706,7 +690,7 @@ mod tests { fn test_different_dev_id() { let m1 = mount_info("0", "/mnt/bar"); let m2 = mount_info("1", "/mnt/bar"); - assert!(is_best(&[m1.clone()], &m2)); + assert!(is_best(std::slice::from_ref(&m1), &m2)); assert!(is_best(&[m2], &m1)); } @@ -717,14 +701,14 @@ mod tests { // one condition in this test. let m1 = mount_info("0", "/mnt/bar"); let m2 = mount_info("0", "/mnt/bar/baz"); - assert!(!is_best(&[m1.clone()], &m2)); + assert!(!is_best(std::slice::from_ref(&m1), &m2)); assert!(is_best(&[m2], &m1)); } } mod is_included { - use crate::{is_included, Options}; + use crate::{Options, is_included}; use uucore::fsext::MountInfo; /// Instantiate a [`MountInfo`] with the given fields. @@ -733,9 +717,9 @@ mod tests { dev_id: String::new(), dev_name: String::new(), fs_type: String::from(fs_type), - mount_dir: String::from(mount_dir), + mount_dir: mount_dir.into(), mount_option: String::new(), - mount_root: String::new(), + mount_root: "/".into(), remote, dummy, } @@ -858,16 +842,4 @@ mod tests { assert!(is_included(&m, &opt)); } } - - mod filter_mount_list { - - use crate::{filter_mount_list, Options}; - - #[test] - fn test_empty() { - let opt = Options::default(); - let mount_infos = vec![]; - assert!(filter_mount_list(mount_infos, &opt).is_empty()); - } - } } diff --git a/src/uu/df/src/filesystem.rs b/src/uu/df/src/filesystem.rs index ef5107958a2..28e6bb6c03a 100644 --- a/src/uu/df/src/filesystem.rs +++ b/src/uu/df/src/filesystem.rs @@ -8,7 +8,7 @@ //! filesystem mounted at a particular directory. It also includes //! information on amount of space available and amount of space used. // spell-checker:ignore canonicalized -use std::path::Path; +use std::{ffi::OsString, path::Path}; #[cfg(unix)] use uucore::fsext::statfs; @@ -28,7 +28,7 @@ pub(crate) struct Filesystem { /// When invoking `df` with a positional argument, it displays /// usage information for the filesystem that contains the given /// file. If given, this field contains that filename. - pub file: Option, + pub file: Option, /// Information about the mounted device, mount directory, and related options. pub mount_info: MountInfo, @@ -37,6 +37,33 @@ pub(crate) struct Filesystem { pub usage: FsUsage, } +#[derive(Debug, PartialEq)] +pub(crate) enum FsError { + #[cfg(not(windows))] + OverMounted, + InvalidPath, + MountMissing, +} + +/// Check whether `mount` has been over-mounted. +/// +/// `mount` is considered over-mounted if it there is an element in +/// `mounts` after mount that has the same `mount_dir`. +#[cfg(not(windows))] +fn is_over_mounted(mounts: &[MountInfo], mount: &MountInfo) -> bool { + let last_mount_for_dir = mounts + .iter() + .filter(|m| m.mount_dir == mount.mount_dir) + .next_back(); + + if let Some(lmi) = last_mount_for_dir { + lmi.dev_name != mount.dev_name + } else { + // Should be unreachable if `mount` is in `mounts` + false + } +} + /// Find the mount info that best matches a given filesystem path. /// /// This function returns the element of `mounts` on which `path` is @@ -56,14 +83,16 @@ fn mount_info_from_path

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

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

(mounts: &[MountInfo], path: P) -> Result where P: AsRef, { - let file = path.as_ref().display().to_string(); + let file = path.as_ref().as_os_str().to_owned(); let canonicalize = true; - let mount_info = mount_info_from_path(mounts, path, canonicalize)?; - // TODO Make it so that we do not need to clone the `mount_info`. - let mount_info = (*mount_info).clone(); - Self::new(mount_info, Some(file)) + + let result = mount_info_from_path(mounts, path, canonicalize); + #[cfg(windows)] + return result.and_then(|mount_info| Self::from_mount(mount_info, Some(file))); + #[cfg(not(windows))] + return result.and_then(|mount_info| Self::from_mount(mounts, mount_info, Some(file))); } } @@ -151,25 +205,27 @@ mod tests { mod mount_info_from_path { + use std::ffi::OsString; + use uucore::fsext::MountInfo; - use crate::filesystem::mount_info_from_path; + use crate::filesystem::{FsError, mount_info_from_path}; - // Create a fake `MountInfo` with the given directory name. + /// Create a fake `MountInfo` with the given directory name. fn mount_info(mount_dir: &str) -> MountInfo { MountInfo { dev_id: String::default(), dev_name: String::default(), fs_type: String::default(), - mount_dir: String::from(mount_dir), + mount_dir: OsString::from(mount_dir), mount_option: String::default(), - mount_root: String::default(), + mount_root: OsString::default(), remote: Default::default(), dummy: Default::default(), } } - // Check whether two `MountInfo` instances are equal. + /// Check whether two `MountInfo` instances are equal. fn mount_info_eq(m1: &MountInfo, m2: &MountInfo) -> bool { m1.dev_id == m2.dev_id && m1.dev_name == m2.dev_name @@ -183,7 +239,19 @@ mod tests { #[test] fn test_empty_mounts() { - assert!(mount_info_from_path(&[], "/", false).is_none()); + assert_eq!( + mount_info_from_path(&[], "/", false).unwrap_err(), + FsError::MountMissing + ); + } + + #[test] + fn test_bad_path() { + assert_eq!( + // This path better not exist.... + mount_info_from_path(&[], "/non-existent-path", true).unwrap_err(), + FsError::InvalidPath + ); } #[test] @@ -210,16 +278,25 @@ mod tests { #[test] fn test_no_match() { let mounts = [mount_info("/foo")]; - assert!(mount_info_from_path(&mounts, "/bar", false).is_none()); + assert_eq!( + mount_info_from_path(&mounts, "/bar", false).unwrap_err(), + FsError::MountMissing + ); } #[test] fn test_partial_match() { let mounts = [mount_info("/foo/bar")]; - assert!(mount_info_from_path(&mounts, "/foo/baz", false).is_none()); + assert_eq!( + mount_info_from_path(&mounts, "/foo/baz", false).unwrap_err(), + FsError::MountMissing + ); } #[test] + // clippy::assigning_clones added with Rust 1.78 + // Rust version = 1.76 on OpenBSD stable/7.5 + #[cfg_attr(not(target_os = "openbsd"), allow(clippy::assigning_clones))] fn test_dev_name_match() { let tmp = tempfile::TempDir::new().expect("Failed to create temp dir"); let dev_name = std::fs::canonicalize(tmp.path()) @@ -234,4 +311,54 @@ mod tests { assert!(mount_info_eq(actual, &mounts[0])); } } + + #[cfg(not(windows))] + mod over_mount { + use std::ffi::OsString; + + use crate::filesystem::{Filesystem, FsError, is_over_mounted}; + use uucore::fsext::MountInfo; + + fn mount_info_with_dev_name(mount_dir: &str, dev_name: Option<&str>) -> MountInfo { + MountInfo { + dev_id: String::default(), + dev_name: dev_name.map(String::from).unwrap_or_default(), + fs_type: String::default(), + mount_dir: OsString::from(mount_dir), + mount_option: String::default(), + mount_root: OsString::default(), + remote: Default::default(), + dummy: Default::default(), + } + } + + #[test] + fn test_over_mount() { + let mount_info1 = mount_info_with_dev_name("/foo", Some("dev_name_1")); + let mount_info2 = mount_info_with_dev_name("/foo", Some("dev_name_2")); + let mounts = [mount_info1, mount_info2]; + assert!(is_over_mounted(&mounts, &mounts[0])); + } + + #[test] + fn test_over_mount_not_over_mounted() { + let mount_info1 = mount_info_with_dev_name("/foo", Some("dev_name_1")); + let mount_info2 = mount_info_with_dev_name("/foo", Some("dev_name_2")); + let mounts = [mount_info1, mount_info2]; + assert!(!is_over_mounted(&mounts, &mounts[1])); + } + + #[test] + fn test_from_mount_over_mounted() { + let mount_info1 = mount_info_with_dev_name("/foo", Some("dev_name_1")); + let mount_info2 = mount_info_with_dev_name("/foo", Some("dev_name_2")); + + let mounts = [mount_info1, mount_info2]; + + assert_eq!( + Filesystem::from_mount(&mounts, &mounts[0], None).unwrap_err(), + FsError::OverMounted + ); + } + } } diff --git a/src/uu/df/src/table.rs b/src/uu/df/src/table.rs index daa30d1eef5..6df95f7be63 100644 --- a/src/uu/df/src/table.rs +++ b/src/uu/df/src/table.rs @@ -9,13 +9,15 @@ //! collection of data rows ([`Row`]), one per filesystem. use unicode_width::UnicodeWidthStr; -use crate::blocks::{to_magnitude_and_suffix, SuffixType}; +use crate::blocks::{SuffixType, to_magnitude_and_suffix}; use crate::columns::{Alignment, Column}; use crate::filesystem::Filesystem; use crate::{BlockSize, Options}; use uucore::fsext::{FsUsage, MountInfo}; +use uucore::translate; -use std::fmt; +use std::ffi::OsString; +use std::iter; use std::ops::AddAssign; /// A row in the filesystem usage data table. @@ -24,7 +26,7 @@ use std::ops::AddAssign; /// filesystem device, the mountpoint, the number of bytes used, etc. pub(crate) struct Row { /// The filename given on the command-line, if given. - file: Option, + file: Option, /// Name of the device on which the filesystem lives. fs_device: String, @@ -33,7 +35,7 @@ pub(crate) struct Row { fs_type: String, /// Path at which the filesystem is mounted. - fs_mount: String, + fs_mount: OsString, /// Total number of bytes in the filesystem regardless of whether they are used. bytes: u64, @@ -106,7 +108,7 @@ impl AddAssign for Row { let inodes_used = self.inodes_used + rhs.inodes_used; *self = Self { file: None, - fs_device: "total".into(), + fs_device: translate!("df-total"), fs_type: "-".into(), fs_mount: "-".into(), bytes, @@ -190,6 +192,43 @@ impl From for Row { } } +/// A `Cell` in the table. We store raw `bytes` as the data (e.g. directory name +/// may be non-Unicode). We also record the printed `width` for alignment purpose, +/// as it is easier to compute on the original string. +struct Cell { + bytes: Vec, + width: usize, +} + +impl Cell { + /// Create a cell, knowing that s contains only 1-length chars + fn from_ascii_string>(s: T) -> Cell { + let s = s.as_ref(); + Cell { + bytes: s.as_bytes().into(), + width: s.len(), + } + } + + /// Create a cell from an unknown origin string that may contain + /// wide characters. + fn from_string>(s: T) -> Cell { + let s = s.as_ref(); + Cell { + bytes: s.as_bytes().into(), + width: UnicodeWidthStr::width(s), + } + } + + /// Create a cell from an `OsString` + fn from_os_string(os: &OsString) -> Cell { + Cell { + bytes: uucore::os_str_as_bytes(os).unwrap().to_vec(), + width: UnicodeWidthStr::width(os.to_string_lossy().as_ref()), + } + } +} + /// A formatter for [`Row`]. /// /// The `options` control how the information in the row gets formatted. @@ -223,47 +262,50 @@ impl<'a> RowFormatter<'a> { /// Get a string giving the scaled version of the input number. /// /// The scaling factor is defined in the `options` field. - fn scaled_bytes(&self, size: u64) -> String { - if let Some(h) = self.options.human_readable { + fn scaled_bytes(&self, size: u64) -> Cell { + let s = if let Some(h) = self.options.human_readable { to_magnitude_and_suffix(size.into(), SuffixType::HumanReadable(h)) } else { let BlockSize::Bytes(d) = self.options.block_size; (size as f64 / d as f64).ceil().to_string() - } + }; + Cell::from_ascii_string(s) } /// Get a string giving the scaled version of the input number. /// /// The scaling factor is defined in the `options` field. - fn scaled_inodes(&self, size: u128) -> String { - if let Some(h) = self.options.human_readable { + fn scaled_inodes(&self, size: u128) -> Cell { + let s = if let Some(h) = self.options.human_readable { to_magnitude_and_suffix(size, SuffixType::HumanReadable(h)) } else { size.to_string() - } + }; + Cell::from_ascii_string(s) } /// Convert a float between 0 and 1 into a percentage string. /// /// If `None`, return the string `"-"` instead. - fn percentage(fraction: Option) -> String { - match fraction { + fn percentage(fraction: Option) -> Cell { + let s = match fraction { None => "-".to_string(), Some(x) => format!("{:.0}%", (100.0 * x).ceil()), - } + }; + Cell::from_ascii_string(s) } /// Returns formatted row data. - fn get_values(&self) -> Vec { - let mut strings = Vec::new(); + fn get_cells(&self) -> Vec { + let mut cells = Vec::new(); for column in &self.options.columns { - let string = match column { + let cell = match column { Column::Source => { if self.is_total_row { - "total".to_string() + Cell::from_string(translate!("df-total")) } else { - self.row.fs_device.to_string() + Cell::from_string(&self.row.fs_device) } } Column::Size => self.scaled_bytes(self.row.bytes), @@ -273,30 +315,34 @@ impl<'a> RowFormatter<'a> { Column::Target => { if self.is_total_row && !self.options.columns.contains(&Column::Source) { - "total".to_string() + Cell::from_string(translate!("df-total")) } else { - self.row.fs_mount.to_string() + Cell::from_os_string(&self.row.fs_mount) } } Column::Itotal => self.scaled_inodes(self.row.inodes), Column::Iused => self.scaled_inodes(self.row.inodes_used), Column::Iavail => self.scaled_inodes(self.row.inodes_free), Column::Ipcent => Self::percentage(self.row.inodes_usage), - Column::File => self.row.file.as_ref().unwrap_or(&"-".into()).to_string(), + Column::File => self + .row + .file + .as_ref() + .map_or(Cell::from_ascii_string("-"), Cell::from_os_string), - Column::Fstype => self.row.fs_type.to_string(), + Column::Fstype => Cell::from_string(&self.row.fs_type), #[cfg(target_os = "macos")] Column::Capacity => Self::percentage(self.row.bytes_capacity), }; - strings.push(string); + cells.push(cell); } - strings + cells } } -/// A HeaderMode defines what header labels should be shown. +/// A `HeaderMode` defines what header labels should be shown. pub(crate) enum HeaderMode { Default, // the user used -h or -H @@ -325,32 +371,38 @@ impl Header { for column in &options.columns { let header = match column { - Column::Source => String::from("Filesystem"), + Column::Source => translate!("df-header-filesystem"), Column::Size => match options.header_mode { - HeaderMode::HumanReadable => String::from("Size"), + HeaderMode::HumanReadable => translate!("df-header-size"), HeaderMode::PosixPortability => { - format!("{}-blocks", options.block_size.as_u64()) + format!( + "{}{}", + options.block_size.as_u64(), + translate!("df-blocks-suffix") + ) } - _ => format!("{}-blocks", options.block_size), + _ => format!("{}{}", options.block_size, translate!("df-blocks-suffix")), }, - Column::Used => String::from("Used"), + Column::Used => translate!("df-header-used"), Column::Avail => match options.header_mode { - HeaderMode::HumanReadable | HeaderMode::Output => String::from("Avail"), - _ => String::from("Available"), + HeaderMode::HumanReadable | HeaderMode::Output => { + translate!("df-header-avail") + } + _ => translate!("df-header-available"), }, Column::Pcent => match options.header_mode { - HeaderMode::PosixPortability => String::from("Capacity"), - _ => String::from("Use%"), + HeaderMode::PosixPortability => translate!("df-header-capacity"), + _ => translate!("df-header-use-percent"), }, - Column::Target => String::from("Mounted on"), - Column::Itotal => String::from("Inodes"), - Column::Iused => String::from("IUsed"), - Column::Iavail => String::from("IFree"), - Column::Ipcent => String::from("IUse%"), - Column::File => String::from("File"), - Column::Fstype => String::from("Type"), + Column::Target => translate!("df-header-mounted-on"), + Column::Itotal => translate!("df-header-inodes"), + Column::Iused => translate!("df-header-iused"), + Column::Iavail => translate!("df-header-iavail"), + Column::Ipcent => translate!("df-header-iuse-percent"), + Column::File => translate!("df-header-file"), + Column::Fstype => translate!("df-header-type"), #[cfg(target_os = "macos")] - Column::Capacity => String::from("Capacity"), + Column::Capacity => translate!("df-header-capacity"), }; headers.push(header); @@ -363,7 +415,7 @@ impl Header { /// The output table. pub(crate) struct Table { alignments: Vec, - rows: Vec>, + rows: Vec>, widths: Vec, } @@ -377,13 +429,13 @@ impl Table { .map(|(i, col)| Column::min_width(col).max(headers[i].len())) .collect(); - let mut rows = vec![headers]; + let mut rows = vec![headers.iter().map(Cell::from_string).collect()]; // The running total of filesystem sizes and usage. // // This accumulator is computed in case we need to display the // total counts in the last row of the table. - let mut total = Row::new("total"); + let mut total = Row::new(&translate!("df-total")); for filesystem in filesystems { // If the filesystem is not empty, or if the options require @@ -392,7 +444,7 @@ impl Table { if options.show_all_fs || filesystem.usage.blocks > 0 { let row = Row::from(filesystem); let fmt = RowFormatter::new(&row, options, false); - let values = fmt.get_values(); + let values = fmt.get_cells(); total += row; rows.push(values); @@ -401,15 +453,15 @@ impl Table { if options.show_total { let total_row = RowFormatter::new(&total, options, true); - rows.push(total_row.get_values()); + rows.push(total_row.get_cells()); } // extend the column widths (in chars) for long values in rows // do it here, after total row was added to the list of rows for row in &rows { for (i, value) in row.iter().enumerate() { - if UnicodeWidthStr::width(value.as_str()) > widths[i] { - widths[i] = UnicodeWidthStr::width(value.as_str()); + if value.width > widths[i] { + widths[i] = value.width; } } } @@ -430,37 +482,37 @@ impl Table { alignments } -} -impl fmt::Display for Table { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut row_iter = self.rows.iter().peekable(); - while let Some(row) = row_iter.next() { + pub(crate) fn write_to(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> { + for row in &self.rows { let mut col_iter = row.iter().enumerate().peekable(); while let Some((i, elem)) = col_iter.next() { let is_last_col = col_iter.peek().is_none(); - match self.alignments[i] { - Alignment::Left => { - if is_last_col { - // no trailing spaces in last column - write!(f, "{elem}")?; - } else { - write!(f, "{: { + writer.write_all(&elem.bytes)?; + // no trailing spaces in last column + if !is_last_col { + writer + .write_all(&iter::repeat_n(b' ', pad_width).collect::>())?; } } - Alignment::Right => write!(f, "{:>width$}", elem, width = self.widths[i])?, + Some(Alignment::Right) => { + writer.write_all(&iter::repeat_n(b' ', pad_width).collect::>())?; + writer.write_all(&elem.bytes)?; + } + None => break, } if !is_last_col { // column separator - write!(f, " ")?; + writer.write_all(b" ")?; } } - if row_iter.peek().is_some() { - writeln!(f)?; - } + writeln!(writer)?; } Ok(()) @@ -471,12 +523,20 @@ impl fmt::Display for Table { mod tests { use std::vec; + use uucore::locale::setup_localization; use crate::blocks::HumanReadable; use crate::columns::Column; - use crate::table::{Header, HeaderMode, Row, RowFormatter, Table}; + use crate::table::{Cell, Header, HeaderMode, Row, RowFormatter, Table}; use crate::{BlockSize, Options}; + fn init() { + unsafe { + std::env::set_var("LANG", "C"); + } + let _ = setup_localization("df"); + } + const COLUMNS_WITH_FS_TYPE: [Column; 7] = [ Column::Source, Column::Fstype, @@ -498,10 +558,10 @@ mod tests { impl Default for Row { fn default() -> Self { Self { - file: Some("/path/to/file".to_string()), + file: Some("/path/to/file".into()), fs_device: "my_device".to_string(), fs_type: "my_type".to_string(), - fs_mount: "my_mount".to_string(), + fs_mount: "my_mount".into(), bytes: 100, bytes_used: 25, @@ -521,7 +581,9 @@ mod tests { #[test] fn test_default_header() { + init(); let options = Options::default(); + assert_eq!( Header::get_headers(&options), vec!( @@ -537,6 +599,7 @@ mod tests { #[test] fn test_header_with_fs_type() { + init(); let options = Options { columns: COLUMNS_WITH_FS_TYPE.to_vec(), ..Default::default() @@ -557,6 +620,7 @@ mod tests { #[test] fn test_header_with_inodes() { + init(); let options = Options { columns: COLUMNS_WITH_INODES.to_vec(), ..Default::default() @@ -576,6 +640,7 @@ mod tests { #[test] fn test_header_with_block_size_1024() { + init(); let options = Options { block_size: BlockSize::Bytes(3 * 1024), ..Default::default() @@ -595,6 +660,7 @@ mod tests { #[test] fn test_human_readable_header() { + init(); let options = Options { header_mode: HeaderMode::HumanReadable, ..Default::default() @@ -607,6 +673,7 @@ mod tests { #[test] fn test_posix_portability_header() { + init(); let options = Options { header_mode: HeaderMode::PosixPortability, ..Default::default() @@ -626,6 +693,7 @@ mod tests { #[test] fn test_output_header() { + init(); let options = Options { header_mode: HeaderMode::Output, ..Default::default() @@ -643,15 +711,23 @@ mod tests { ); } + fn compare_cell_content(cells: Vec, expected: Vec<&str>) -> bool { + cells + .into_iter() + .zip(expected) + .all(|(c, s)| c.bytes == s.as_bytes()) + } + #[test] fn test_row_formatter() { + init(); let options = Options { block_size: BlockSize::Bytes(1), ..Default::default() }; let row = Row { fs_device: "my_device".to_string(), - fs_mount: "my_mount".to_string(), + fs_mount: "my_mount".into(), bytes: 100, bytes_used: 25, @@ -661,14 +737,15 @@ mod tests { ..Default::default() }; let fmt = RowFormatter::new(&row, &options, false); - assert_eq!( - fmt.get_values(), + assert!(compare_cell_content( + fmt.get_cells(), vec!("my_device", "100", "25", "75", "25%", "my_mount") - ); + )); } #[test] fn test_row_formatter_with_fs_type() { + init(); let options = Options { columns: COLUMNS_WITH_FS_TYPE.to_vec(), block_size: BlockSize::Bytes(1), @@ -677,7 +754,7 @@ mod tests { let row = Row { fs_device: "my_device".to_string(), fs_type: "my_type".to_string(), - fs_mount: "my_mount".to_string(), + fs_mount: "my_mount".into(), bytes: 100, bytes_used: 25, @@ -687,14 +764,15 @@ mod tests { ..Default::default() }; let fmt = RowFormatter::new(&row, &options, false); - assert_eq!( - fmt.get_values(), + assert!(compare_cell_content( + fmt.get_cells(), vec!("my_device", "my_type", "100", "25", "75", "25%", "my_mount") - ); + )); } #[test] fn test_row_formatter_with_inodes() { + init(); let options = Options { columns: COLUMNS_WITH_INODES.to_vec(), block_size: BlockSize::Bytes(1), @@ -702,7 +780,7 @@ mod tests { }; let row = Row { fs_device: "my_device".to_string(), - fs_mount: "my_mount".to_string(), + fs_mount: "my_mount".into(), inodes: 10, inodes_used: 2, @@ -712,14 +790,15 @@ mod tests { ..Default::default() }; let fmt = RowFormatter::new(&row, &options, false); - assert_eq!( - fmt.get_values(), + assert!(compare_cell_content( + fmt.get_cells(), vec!("my_device", "10", "2", "8", "20%", "my_mount") - ); + )); } #[test] fn test_row_formatter_with_bytes_and_inodes() { + init(); let options = Options { columns: vec![Column::Size, Column::Itotal], block_size: BlockSize::Bytes(100), @@ -731,11 +810,12 @@ mod tests { ..Default::default() }; let fmt = RowFormatter::new(&row, &options, false); - assert_eq!(fmt.get_values(), vec!("1", "10")); + assert!(compare_cell_content(fmt.get_cells(), vec!("1", "10"))); } #[test] fn test_row_formatter_with_human_readable_si() { + init(); let options = Options { human_readable: Some(HumanReadable::Decimal), columns: COLUMNS_WITH_FS_TYPE.to_vec(), @@ -744,7 +824,7 @@ mod tests { let row = Row { fs_device: "my_device".to_string(), fs_type: "my_type".to_string(), - fs_mount: "my_mount".to_string(), + fs_mount: "my_mount".into(), bytes: 4000, bytes_used: 1000, @@ -754,14 +834,15 @@ mod tests { ..Default::default() }; let fmt = RowFormatter::new(&row, &options, false); - assert_eq!( - fmt.get_values(), + assert!(compare_cell_content( + fmt.get_cells(), vec!("my_device", "my_type", "4k", "1k", "3k", "25%", "my_mount") - ); + )); } #[test] fn test_row_formatter_with_human_readable_binary() { + init(); let options = Options { human_readable: Some(HumanReadable::Binary), columns: COLUMNS_WITH_FS_TYPE.to_vec(), @@ -770,7 +851,7 @@ mod tests { let row = Row { fs_device: "my_device".to_string(), fs_type: "my_type".to_string(), - fs_mount: "my_mount".to_string(), + fs_mount: "my_mount".into(), bytes: 4096, bytes_used: 1024, @@ -780,14 +861,15 @@ mod tests { ..Default::default() }; let fmt = RowFormatter::new(&row, &options, false); - assert_eq!( - fmt.get_values(), + assert!(compare_cell_content( + fmt.get_cells(), vec!("my_device", "my_type", "4K", "1K", "3K", "25%", "my_mount") - ); + )); } #[test] fn test_row_formatter_with_round_up_usage() { + init(); let options = Options { columns: vec![Column::Pcent], ..Default::default() @@ -797,12 +879,13 @@ mod tests { ..Default::default() }; let fmt = RowFormatter::new(&row, &options, false); - assert_eq!(fmt.get_values(), vec!("26%")); + assert!(compare_cell_content(fmt.get_cells(), vec!("26%"))); } #[test] fn test_row_formatter_with_round_up_byte_values() { - fn get_formatted_values(bytes: u64, bytes_used: u64, bytes_avail: u64) -> Vec { + init(); + fn get_formatted_values(bytes: u64, bytes_used: u64, bytes_avail: u64) -> Vec { let options = Options { block_size: BlockSize::Bytes(1000), columns: vec![Column::Size, Column::Used, Column::Avail], @@ -815,17 +898,30 @@ mod tests { bytes_avail, ..Default::default() }; - RowFormatter::new(&row, &options, false).get_values() + RowFormatter::new(&row, &options, false).get_cells() } - assert_eq!(get_formatted_values(100, 100, 0), vec!("1", "1", "0")); - assert_eq!(get_formatted_values(100, 99, 1), vec!("1", "1", "1")); - assert_eq!(get_formatted_values(1000, 1000, 0), vec!("1", "1", "0")); - assert_eq!(get_formatted_values(1001, 1000, 1), vec!("2", "1", "1")); + assert!(compare_cell_content( + get_formatted_values(100, 100, 0), + vec!("1", "1", "0") + )); + assert!(compare_cell_content( + get_formatted_values(100, 99, 1), + vec!("1", "1", "1") + )); + assert!(compare_cell_content( + get_formatted_values(1000, 1000, 0), + vec!("1", "1", "0") + )); + assert!(compare_cell_content( + get_formatted_values(1001, 1000, 1), + vec!("2", "1", "1") + )); } #[test] fn test_row_converter_with_invalid_numbers() { + init(); // copy from wsl linux let d = crate::Filesystem { file: None, @@ -833,20 +929,20 @@ mod tests { dev_id: "28".to_string(), dev_name: "none".to_string(), fs_type: "9p".to_string(), - mount_dir: "/usr/lib/wsl/drivers".to_string(), + mount_dir: "/usr/lib/wsl/drivers".into(), mount_option: "ro,nosuid,nodev,noatime".to_string(), - mount_root: "/".to_string(), + mount_root: "/".into(), remote: false, dummy: false, }, usage: crate::table::FsUsage { blocksize: 4096, - blocks: 244029695, - bfree: 125085030, - bavail: 125085030, + blocks: 244_029_695, + bfree: 125_085_030, + bavail: 125_085_030, bavail_top_bit_set: false, files: 999, - ffree: 1000000, + ffree: 1_000_000, }, }; @@ -857,26 +953,27 @@ mod tests { #[test] fn test_table_column_width_computation_include_total_row() { + init(); let d1 = crate::Filesystem { file: None, mount_info: crate::MountInfo { dev_id: "28".to_string(), dev_name: "none".to_string(), fs_type: "9p".to_string(), - mount_dir: "/usr/lib/wsl/drivers".to_string(), + mount_dir: "/usr/lib/wsl/drivers".into(), mount_option: "ro,nosuid,nodev,noatime".to_string(), - mount_root: "/".to_string(), + mount_root: "/".into(), remote: false, dummy: false, }, usage: crate::table::FsUsage { blocksize: 4096, - blocks: 244029695, - bfree: 125085030, - bavail: 125085030, + blocks: 244_029_695, + bfree: 125_085_030, + bavail: 125_085_030, bavail_top_bit_set: false, - files: 99999999999, - ffree: 999999, + files: 99_999_999_999, + ffree: 999_999, }, }; @@ -894,27 +991,86 @@ mod tests { }; let table_w_total = Table::new(&options, filesystems.clone()); + let mut data_w_total: Vec = vec![]; + table_w_total + .write_to(&mut data_w_total) + .expect("Write error."); assert_eq!( - table_w_total.to_string(), + String::from_utf8_lossy(&data_w_total), "Filesystem Inodes IUsed IFree\n\ none 99999999999 99999000000 999999\n\ none 99999999999 99999000000 999999\n\ - total 199999999998 199998000000 1999998" + total 199999999998 199998000000 1999998\n" ); options.show_total = false; let table_w_o_total = Table::new(&options, filesystems); + let mut data_w_o_total: Vec = vec![]; + table_w_o_total + .write_to(&mut data_w_o_total) + .expect("Write error."); assert_eq!( - table_w_o_total.to_string(), + String::from_utf8_lossy(&data_w_o_total), "Filesystem Inodes IUsed IFree\n\ none 99999999999 99999000000 999999\n\ - none 99999999999 99999000000 999999" + none 99999999999 99999000000 999999\n" + ); + } + + #[cfg(unix)] + #[test] + fn test_table_column_width_non_unicode() { + init(); + let bad_unicode_os_str = uucore::os_str_from_bytes(b"/usr/lib/w\xf3l/drivers") + .expect("Only unix platforms can test non-unicode names") + .to_os_string(); + let d1 = crate::Filesystem { + file: None, + mount_info: crate::MountInfo { + dev_id: "28".to_string(), + dev_name: "none".to_string(), + fs_type: "9p".to_string(), + mount_dir: bad_unicode_os_str, + mount_option: "ro,nosuid,nodev,noatime".to_string(), + mount_root: "/".into(), + remote: false, + dummy: false, + }, + usage: crate::table::FsUsage { + blocksize: 4096, + blocks: 244_029_695, + bfree: 125_085_030, + bavail: 125_085_030, + bavail_top_bit_set: false, + files: 99_999_999_999, + ffree: 999_999, + }, + }; + + let filesystems = vec![d1]; + + let options = Options { + show_total: false, + columns: vec![Column::Source, Column::Target, Column::Itotal], + ..Default::default() + }; + + let table = Table::new(&options, filesystems.clone()); + let mut data: Vec = vec![]; + table.write_to(&mut data).expect("Write error."); + assert_eq!( + data, + b"Filesystem Mounted on Inodes\n\ + none /usr/lib/w\xf3l/drivers 99999999999\n", + "Comparison failed, lossy data for reference:\n{}\n", + String::from_utf8_lossy(&data) ); } #[test] fn test_row_accumulation_u64_overflow() { + init(); let total = u64::MAX as u128; let used1 = 3000u128; let used2 = 50000u128; diff --git a/src/uu/dir/Cargo.toml b/src/uu/dir/Cargo.toml index 24da984af03..9bbec793b40 100644 --- a/src/uu/dir/Cargo.toml +++ b/src/uu/dir/Cargo.toml @@ -1,15 +1,18 @@ [package] name = "uu_dir" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "shortcut to ls -C -b" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/ls" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/dir.rs" diff --git a/src/uu/dir/src/dir.rs b/src/uu/dir/src/dir.rs index e255295119a..83cff412d56 100644 --- a/src/uu/dir/src/dir.rs +++ b/src/uu/dir/src/dir.rs @@ -6,9 +6,9 @@ use clap::Command; use std::ffi::OsString; use std::path::Path; -use uu_ls::{options, Config, Format}; +use uu_ls::{Config, Format, options}; use uucore::error::UResult; -use uucore::quoting_style::{Quotes, QuotingStyle}; +use uucore::quoting_style::QuotingStyle; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { @@ -45,9 +45,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut config = Config::from(&matches)?; if default_quoting_style { - config.quoting_style = QuotingStyle::C { - quotes: Quotes::None, - }; + config.quoting_style = QuotingStyle::C_NO_QUOTES; } if default_format_style { config.format = Format::Columns; @@ -55,8 +53,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let locs = matches .get_many::(options::PATHS) - .map(|v| v.map(Path::new).collect()) - .unwrap_or_else(|| vec![Path::new(".")]); + .map_or_else(|| vec![Path::new(".")], |v| v.map(Path::new).collect()); uu_ls::list(locs, &config) } diff --git a/src/uu/dircolors/Cargo.toml b/src/uu/dircolors/Cargo.toml index 15f943a4637..077d4e59e21 100644 --- a/src/uu/dircolors/Cargo.toml +++ b/src/uu/dircolors/Cargo.toml @@ -1,22 +1,26 @@ [package] name = "uu_dircolors" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "dircolors ~ (uutils) display commands to set LS_COLORS" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/dircolors" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/dircolors.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["colors"] } +uucore = { workspace = true, features = ["colors", "parser"] } +fluent = { workspace = true } [[bin]] name = "dircolors" diff --git a/src/uu/dircolors/README.md b/src/uu/dircolors/README.md index ce8aa965f93..62944d4907b 100644 --- a/src/uu/dircolors/README.md +++ b/src/uu/dircolors/README.md @@ -15,4 +15,4 @@ Run the tests: cargo test --features "dircolors" --no-default-features ``` -Edit `/PATH_TO_COREUTILS/src/uu/dircolors/src/colors.rs` until the tests pass. +Edit `/PATH_TO_COREUTILS/src/uu/dircolors/src/dircolors.rs` until the tests pass. diff --git a/src/uu/dircolors/dircolors.md b/src/uu/dircolors/dircolors.md deleted file mode 100644 index 959b8fd19bc..00000000000 --- a/src/uu/dircolors/dircolors.md +++ /dev/null @@ -1,13 +0,0 @@ -# dircolors - -``` -dircolors [OPTION]... [FILE] -``` - -Output commands to set the LS_COLORS environment variable. - -## After Help - -If FILE is specified, read it to determine which colors to use for which -file types and extensions. Otherwise, a precompiled database is used. -For details on the format of these files, run 'dircolors --print-database' diff --git a/src/uu/dircolors/locales/en-US.ftl b/src/uu/dircolors/locales/en-US.ftl new file mode 100644 index 00000000000..db877f6b587 --- /dev/null +++ b/src/uu/dircolors/locales/en-US.ftl @@ -0,0 +1,23 @@ +dircolors-about = Output commands to set the LS_COLORS environment variable. +dircolors-usage = dircolors [OPTION]... [FILE] +dircolors-after-help = If FILE is specified, read it to determine which colors to use for which + file types and extensions. Otherwise, a precompiled database is used. + For details on the format of these files, run 'dircolors --print-database' + +# Help messages +dircolors-help-bourne-shell = output Bourne shell code to set LS_COLORS +dircolors-help-c-shell = output C shell code to set LS_COLORS +dircolors-help-print-database = print the byte counts +dircolors-help-print-ls-colors = output fully escaped colors for display + +# Error messages +dircolors-error-shell-and-output-exclusive = the options to output non shell syntax, + and to select a shell syntax are mutually exclusive +dircolors-error-print-database-and-ls-colors-exclusive = options --print-database and --print-ls-colors are mutually exclusive +dircolors-error-extra-operand-print-database = extra operand { $operand } + file operands cannot be combined with --print-database (-p) +dircolors-error-no-shell-environment = no SHELL environment variable, and no shell type option given +dircolors-error-extra-operand = extra operand { $operand } +dircolors-error-expected-file-got-directory = expected file, got directory { $path } +dircolors-error-invalid-line-missing-token = { $file }:{ $line }: invalid line; missing second token +dircolors-error-unrecognized-keyword = unrecognized keyword { $keyword } diff --git a/src/uu/dircolors/locales/fr-FR.ftl b/src/uu/dircolors/locales/fr-FR.ftl new file mode 100644 index 00000000000..7f7fcaba0f5 --- /dev/null +++ b/src/uu/dircolors/locales/fr-FR.ftl @@ -0,0 +1,23 @@ +dircolors-about = Afficher les commandes pour définir la variable d'environnement LS_COLORS. +dircolors-usage = dircolors [OPTION]... [FICHIER] +dircolors-after-help = Si FICHIER est spécifié, le lire pour déterminer quelles couleurs utiliser pour quels + types de fichiers et extensions. Sinon, une base de données précompilée est utilisée. + Pour les détails sur le format de ces fichiers, exécutez 'dircolors --print-database' + +# Messages d'aide +dircolors-help-bourne-shell = afficher le code Bourne shell pour définir LS_COLORS +dircolors-help-c-shell = afficher le code C shell pour définir LS_COLORS +dircolors-help-print-database = afficher la base de données de configuration +dircolors-help-print-ls-colors = afficher les couleurs entièrement échappées pour l'affichage + +# Messages d'erreur +dircolors-error-shell-and-output-exclusive = les options pour afficher une syntaxe non-shell + et pour sélectionner une syntaxe shell sont mutuellement exclusives +dircolors-error-print-database-and-ls-colors-exclusive = les options --print-database et --print-ls-colors sont mutuellement exclusives +dircolors-error-extra-operand-print-database = opérande supplémentaire { $operand } + les opérandes de fichier ne peuvent pas être combinées avec --print-database (-p) +dircolors-error-no-shell-environment = aucune variable d'environnement SHELL, et aucune option de type de shell donnée +dircolors-error-extra-operand = opérande supplémentaire { $operand } +dircolors-error-expected-file-got-directory = fichier attendu, répertoire obtenu { $path } +dircolors-error-invalid-line-missing-token = { $file }:{ $line } : ligne invalide ; jeton manquant +dircolors-error-unrecognized-keyword = mot-clé non reconnu { $keyword } diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index 531c3ee474c..c00f5d210f0 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -3,20 +3,24 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) clrtoeol dircolors eightbit endcode fnmatch leftcode multihardlink rightcode setenv sgid suid colorterm +// spell-checker:ignore (ToDO) clrtoeol dircolors eightbit endcode fnmatch leftcode multihardlink rightcode setenv sgid suid colorterm disp use std::borrow::Borrow; use std::env; +use std::ffi::OsString; +use std::fmt::Write as _; use std::fs::File; -//use std::io::IsTerminal; use std::io::{BufRead, BufReader}; use std::path::Path; -use clap::{crate_version, Arg, ArgAction, Command}; +use clap::{Arg, ArgAction, Command}; use uucore::colors::{FILE_ATTRIBUTE_CODES, FILE_COLORS, FILE_TYPES, TERMS}; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError}; -use uucore::{help_about, help_section, help_usage}; +use uucore::translate; + +use uucore::LocalizedCommand; +use uucore::{format_usage, parser::parse_glob}; mod options { pub const BOURNE_SHELL: &str = "bourne-shell"; @@ -26,10 +30,6 @@ mod options { pub const FILE: &str = "FILE"; } -const USAGE: &str = help_usage!("dircolors.md"); -const ABOUT: &str = help_about!("dircolors.md"); -const AFTER_HELP: &str = help_section!("after help", "dircolors.md"); - #[derive(PartialEq, Eq, Debug)] pub enum OutputFmt { Shell, @@ -78,14 +78,14 @@ pub fn generate_type_output(fmt: &OutputFmt) -> String { match fmt { OutputFmt::Display => FILE_TYPES .iter() - .map(|&(_, key, val)| format!("\x1b[{}m{}\t{}\x1b[0m", val, key, val)) + .map(|&(_, key, val)| format!("\x1b[{val}m{key}\t{val}\x1b[0m")) .collect::>() .join("\n"), _ => { // Existing logic for other formats FILE_TYPES .iter() - .map(|&(_, v1, v2)| format!("{}={}", v1, v2)) + .map(|&(_, v1, v2)| format!("{v1}={v2}")) .collect::>() .join(":") } @@ -100,8 +100,7 @@ fn generate_ls_colors(fmt: &OutputFmt, sep: &str) -> String { display_parts.push(type_output); for &(extension, code) in FILE_COLORS { let prefix = if extension.starts_with('*') { "" } else { "*" }; - let formatted_extension = - format!("\x1b[{}m{}{}\t{}\x1b[0m", code, prefix, extension, code); + let formatted_extension = format!("\x1b[{code}m{prefix}{extension}\t{code}\x1b[0m"); display_parts.push(formatted_extension); } display_parts.join("\n") @@ -111,28 +110,22 @@ fn generate_ls_colors(fmt: &OutputFmt, sep: &str) -> String { let mut parts = vec![]; for &(extension, code) in FILE_COLORS { let prefix = if extension.starts_with('*') { "" } else { "*" }; - let formatted_extension = format!("{}{}", prefix, extension); - parts.push(format!("{}={}", formatted_extension, code)); + let formatted_extension = format!("{prefix}{extension}"); + parts.push(format!("{formatted_extension}={code}")); } let (prefix, suffix) = get_colors_format_strings(fmt); let ls_colors = parts.join(sep); - format!( - "{}{}:{}:{}", - prefix, - generate_type_output(fmt), - ls_colors, - suffix - ) + format!("{prefix}{}:{ls_colors}:{suffix}", generate_type_output(fmt),) } } } #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = uu_app().get_matches_from_localized(args); let files = matches - .get_many::(options::FILE) + .get_many::(options::FILE) .map_or(vec![], |file_values| file_values.collect()); // clap provides .conflicts_with / .conflicts_with_all, but we want to @@ -142,15 +135,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { { return Err(UUsageError::new( 1, - "the options to output non shell syntax,\n\ - and to select a shell syntax are mutually exclusive", + translate!("dircolors-error-shell-and-output-exclusive"), )); } if matches.get_flag(options::PRINT_DATABASE) && matches.get_flag(options::PRINT_LS_COLORS) { return Err(UUsageError::new( 1, - "options --print-database and --print-ls-colors are mutually exclusive", + translate!("dircolors-error-print-database-and-ls-colors-exclusive"), )); } @@ -158,11 +150,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if !files.is_empty() { return Err(UUsageError::new( 1, - format!( - "extra operand {}\nfile operands cannot be combined with \ - --print-database (-p)", - files[0].quote() - ), + translate!("dircolors-error-extra-operand-print-database", "operand" => files[0].to_string_lossy().quote()), )); } @@ -185,7 +173,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { OutputFmt::Unknown => { return Err(USimpleError::new( 1, - "no SHELL environment variable, and no shell type option given", + translate!("dircolors-error-no-shell-environment"), )); } fmt => out_format = fmt, @@ -211,18 +199,22 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else if files.len() > 1 { return Err(UUsageError::new( 1, - format!("extra operand {}", files[1].quote()), + translate!("dircolors-error-extra-operand", "operand" => files[1].to_string_lossy().quote()), )); - } else if files[0].eq("-") { + } else if files[0] == "-" { let fin = BufReader::new(std::io::stdin()); // For example, for echo "owt 40;33"|dircolors -b - - result = parse(fin.lines().map_while(Result::ok), &out_format, files[0]); + result = parse( + fin.lines().map_while(Result::ok), + &out_format, + &files[0].to_string_lossy(), + ); } else { - let path = Path::new(files[0]); + let path = Path::new(&files[0]); if path.is_dir() { return Err(USimpleError::new( 2, - format!("expected file, got directory {}", path.quote()), + translate!("dircolors-error-expected-file-got-directory", "path" => path.quote()), )); } match File::open(path) { @@ -235,10 +227,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { ); } Err(e) => { - return Err(USimpleError::new( - 1, - format!("{}: {}", path.maybe_quote(), e), - )); + return Err(USimpleError::new(1, format!("{}: {e}", path.maybe_quote()))); } } } @@ -254,10 +243,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .version(crate_version!()) - .about(ABOUT) - .after_help(AFTER_HELP) - .override_usage(format_usage(USAGE)) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) + .about(translate!("dircolors-about")) + .after_help(translate!("dircolors-after-help")) + .override_usage(format_usage(&translate!("dircolors-usage"))) + .args_override_self(true) .infer_long_args(true) .arg( Arg::new(options::BOURNE_SHELL) @@ -265,7 +256,7 @@ pub fn uu_app() -> Command { .short('b') .visible_alias("bourne-shell") .overrides_with(options::C_SHELL) - .help("output Bourne shell code to set LS_COLORS") + .help(translate!("dircolors-help-bourne-shell")) .action(ArgAction::SetTrue), ) .arg( @@ -274,26 +265,27 @@ pub fn uu_app() -> Command { .short('c') .visible_alias("c-shell") .overrides_with(options::BOURNE_SHELL) - .help("output C shell code to set LS_COLORS") + .help(translate!("dircolors-help-c-shell")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::PRINT_DATABASE) .long("print-database") .short('p') - .help("print the byte counts") + .help(translate!("dircolors-help-print-database")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::PRINT_LS_COLORS) .long("print-ls-colors") - .help("output fully escaped colors for display") + .help(translate!("dircolors-help-print-ls-colors")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::FILE) .hide(true) .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)) .action(ArgAction::Append), ) } @@ -301,7 +293,7 @@ pub fn uu_app() -> Command { pub trait StrUtils { /// Remove comments and trim whitespace fn purify(&self) -> &Self; - /// Like split_whitespace() but only produce 2 components + /// Like `split_whitespace()` but only produce 2 parts fn split_two(&self) -> (&str, &str); fn fnmatch(&self, pattern: &str) -> bool; } @@ -359,9 +351,6 @@ enum ParseState { Pass, } -use uucore::{format_usage, parse_glob}; - -#[allow(clippy::cognitive_complexity)] fn parse(user_input: T, fmt: &OutputFmt, fp: &str) -> Result where T: IntoIterator, @@ -372,10 +361,12 @@ where result.push_str(&prefix); + // Get environment variables once at the start let term = env::var("TERM").unwrap_or_else(|_| "none".to_owned()); - let term = term.as_str(); + let colorterm = env::var("COLORTERM").unwrap_or_default(); let mut state = ParseState::Global; + let mut saw_colorterm_match = false; for (num, line) in user_input.into_iter().enumerate() { let num = num + 1; @@ -388,59 +379,42 @@ where let (key, val) = line.split_two(); if val.is_empty() { - return Err(format!( - // The double space is what GNU is doing - "{}:{}: invalid line; missing second token", - fp.maybe_quote(), - num - )); + return Err( + translate!("dircolors-error-invalid-line-missing-token", "file" => fp.maybe_quote(), "line" => num), + ); } + let lower = key.to_lowercase(); - if lower == "term" || lower == "colorterm" { - if term.fnmatch(val) { - state = ParseState::Matched; - } else if state != ParseState::Matched { - state = ParseState::Pass; - } - } else { - if state == ParseState::Matched { - // prevent subsequent mismatched TERM from - // cancelling the input - state = ParseState::Continue; + match lower.as_str() { + "term" => { + if term.fnmatch(val) { + state = ParseState::Matched; + } else if state == ParseState::Global { + state = ParseState::Pass; + } } - if state != ParseState::Pass { - let search_key = lower.as_str(); - - if key.starts_with('.') { - if *fmt == OutputFmt::Display { - result.push_str(format!("\x1b[{val}m*{key}\t{val}\x1b[0m\n").as_str()); - } else { - result.push_str(format!("*{key}={val}:").as_str()); - } - } else if key.starts_with('*') { - if *fmt == OutputFmt::Display { - result.push_str(format!("\x1b[{val}m{key}\t{val}\x1b[0m\n").as_str()); - } else { - result.push_str(format!("{key}={val}:").as_str()); - } - } else if lower == "options" || lower == "color" || lower == "eightbit" { - // Slackware only. Ignore - } else if let Some((_, s)) = FILE_ATTRIBUTE_CODES - .iter() - .find(|&&(key, _)| key == search_key) - { - if *fmt == OutputFmt::Display { - result.push_str(format!("\x1b[{val}m{s}\t{val}\x1b[0m\n").as_str()); - } else { - result.push_str(format!("{s}={val}:").as_str()); - } + "colorterm" => { + // For COLORTERM ?*, only match if COLORTERM is non-empty + let matches = if val == "?*" { + !colorterm.is_empty() } else { - return Err(format!( - "{}:{}: unrecognized keyword {}", - fp.maybe_quote(), - num, - key - )); + colorterm.fnmatch(val) + }; + if matches { + state = ParseState::Matched; + saw_colorterm_match = true; + } else if !saw_colorterm_match && state == ParseState::Global { + state = ParseState::Pass; + } + } + _ => { + if state == ParseState::Matched { + // prevent subsequent mismatched TERM from + // cancelling the input + state = ParseState::Continue; + } + if state != ParseState::Pass { + append_entry(&mut result, fmt, key, &lower, val)?; } } } @@ -455,6 +429,46 @@ where Ok(result) } +fn append_entry( + result: &mut String, + fmt: &OutputFmt, + key: &str, + lower: &str, + val: &str, +) -> Result<(), String> { + if key.starts_with(['.', '*']) { + let entry = if key.starts_with('.') { + format!("*{key}") + } else { + key.to_string() + }; + let disp = if *fmt == OutputFmt::Display { + format!("\x1b[{val}m{entry}\t{val}\x1b[0m\n") + } else { + format!("{entry}={val}:") + }; + result.push_str(&disp); + return Ok(()); + } + + match lower { + "options" | "color" | "eightbit" => Ok(()), // Slackware only, ignore + _ => { + if let Some((_, s)) = FILE_ATTRIBUTE_CODES.iter().find(|&&(key, _)| key == lower) { + let disp = if *fmt == OutputFmt::Display { + format!("\x1b[{val}m{s}\t{val}\x1b[0m\n") + } else { + format!("{s}={val}:") + }; + result.push_str(&disp); + Ok(()) + } else { + Err(translate!("dircolors-error-unrecognized-keyword", "keyword" => key)) + } + } + } +} + /// Escape single quotes because they are not allowed between single quotes in shell code, and code /// enclosed by single quotes is what is returned by `parse()`. /// @@ -468,7 +482,7 @@ fn escape(s: &str) -> String { match c { '\'' => result.push_str("'\\''"), ':' if previous != '\\' => result.push_str("\\:"), - _ => result.push_str(&c.to_string()), + _ => result.push(c), } previous = c; } @@ -492,7 +506,7 @@ pub fn generate_dircolors_config() -> String { ); config.push_str("COLORTERM ?*\n"); for term in TERMS { - config.push_str(&format!("TERM {}\n", term)); + let _ = writeln!(config, "TERM {term}"); } config.push_str( @@ -513,14 +527,14 @@ pub fn generate_dircolors_config() -> String { ); for (name, _, code) in FILE_TYPES { - config.push_str(&format!("{} {}\n", name, code)); + let _ = writeln!(config, "{name} {code}"); } config.push_str("# List any file extensions like '.gz' or '.tar' that you would like ls\n"); config.push_str("# to color below. Put the extension, a space, and the color init string.\n"); for (ext, color) in FILE_COLORS { - config.push_str(&format!("{} {}\n", ext, color)); + let _ = writeln!(config, "{ext} {color}"); } config.push_str("# Subsequent TERM or COLORTERM entries, can be used to add / override\n"); config.push_str("# config specific to those matching environment variables."); diff --git a/src/uu/dirname/Cargo.toml b/src/uu/dirname/Cargo.toml index 49ebcb653c3..85fc99a1c1f 100644 --- a/src/uu/dirname/Cargo.toml +++ b/src/uu/dirname/Cargo.toml @@ -1,15 +1,18 @@ [package] name = "uu_dirname" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "dirname ~ (uutils) display parent directory of PATHNAME" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/dirname" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/dirname.rs" @@ -17,6 +20,7 @@ path = "src/dirname.rs" [dependencies] clap = { workspace = true } uucore = { workspace = true } +fluent = { workspace = true } [[bin]] name = "dirname" diff --git a/src/uu/dirname/dirname.md b/src/uu/dirname/dirname.md deleted file mode 100644 index f08c1c4c9ad..00000000000 --- a/src/uu/dirname/dirname.md +++ /dev/null @@ -1,12 +0,0 @@ -# dirname - -``` -dirname [OPTION] NAME... -``` - -Strip last component from file name - -## After Help - -Output each NAME with its last non-slash component and trailing slashes -removed; if NAME contains no /'s, output '.' (meaning the current directory). diff --git a/src/uu/dirname/locales/en-US.ftl b/src/uu/dirname/locales/en-US.ftl new file mode 100644 index 00000000000..1bd4b1e6a4f --- /dev/null +++ b/src/uu/dirname/locales/en-US.ftl @@ -0,0 +1,6 @@ +dirname-about = Strip last component from file name +dirname-usage = dirname [OPTION] NAME... +dirname-after-help = Output each NAME with its last non-slash component and trailing slashes + removed; if NAME contains no /'s, output '.' (meaning the current directory). +dirname-missing-operand = missing operand +dirname-zero-help = separate output with NUL rather than newline diff --git a/src/uu/dirname/locales/fr-FR.ftl b/src/uu/dirname/locales/fr-FR.ftl new file mode 100644 index 00000000000..0cc0c1b4f47 --- /dev/null +++ b/src/uu/dirname/locales/fr-FR.ftl @@ -0,0 +1,7 @@ +dirname-about = Supprimer le dernier composant du nom de fichier +dirname-usage = dirname [OPTION] NOM... +dirname-after-help = Afficher chaque NOM avec son dernier composant non-slash et les slashes finaux + supprimés ; si NOM ne contient pas de '/', afficher '.' (signifiant le répertoire courant). +dirname-missing-operand = opérande manquant +dirname-zero-help = séparer la sortie avec NUL plutôt qu'avec un saut de ligne + diff --git a/src/uu/dirname/src/dirname.rs b/src/uu/dirname/src/dirname.rs index a645b05fd72..2782c7fb3a9 100644 --- a/src/uu/dirname/src/dirname.rs +++ b/src/uu/dirname/src/dirname.rs @@ -3,16 +3,16 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use clap::{crate_version, Arg, ArgAction, Command}; +use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; use std::path::Path; +use uucore::LocalizedCommand; use uucore::display::print_verbatim; use uucore::error::{UResult, UUsageError}; +use uucore::format_usage; use uucore::line_ending::LineEnding; -use uucore::{format_usage, help_about, help_section, help_usage}; -const ABOUT: &str = help_about!("dirname.md"); -const USAGE: &str = help_usage!("dirname.md"); -const AFTER_HELP: &str = help_section!("after help", "dirname.md"); +use uucore::translate; mod options { pub const ZERO: &str = "zero"; @@ -21,39 +21,41 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().after_help(AFTER_HELP).try_get_matches_from(args)?; + let matches = uu_app() + .after_help(translate!("dirname-after-help")) + .get_matches_from_localized(args); let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); - let dirnames: Vec = matches - .get_many::(options::DIR) + let dirnames: Vec = matches + .get_many::(options::DIR) .unwrap_or_default() .cloned() .collect(); if dirnames.is_empty() { - return Err(UUsageError::new(1, "missing operand")); - } else { - for path in &dirnames { - let p = Path::new(path); - match p.parent() { - Some(d) => { - if d.components().next().is_none() { - print!("."); - } else { - print_verbatim(d).unwrap(); - } + return Err(UUsageError::new(1, translate!("dirname-missing-operand"))); + } + + for path in &dirnames { + let p = Path::new(path); + match p.parent() { + Some(d) => { + if d.components().next().is_none() { + print!("."); + } else { + print_verbatim(d).unwrap(); } - None => { - if p.is_absolute() || path == "/" { - print!("/"); - } else { - print!("."); - } + } + None => { + if p.is_absolute() || path.as_os_str() == "/" { + print!("/"); + } else { + print!("."); } } - print!("{line_ending}"); } + print!("{line_ending}"); } Ok(()) @@ -61,21 +63,24 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .about(ABOUT) - .version(crate_version!()) - .override_usage(format_usage(USAGE)) + .about(translate!("dirname-about")) + .version(uucore::crate_version!()) + .help_template(uucore::localized_help_template(uucore::util_name())) + .override_usage(format_usage(&translate!("dirname-usage"))) + .args_override_self(true) .infer_long_args(true) .arg( Arg::new(options::ZERO) .long(options::ZERO) .short('z') - .help("separate output with NUL rather than newline") + .help(translate!("dirname-zero-help")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::DIR) .hide(true) .action(ArgAction::Append) - .value_hint(clap::ValueHint::AnyPath), + .value_hint(clap::ValueHint::AnyPath) + .value_parser(clap::value_parser!(OsString)), ) } diff --git a/src/uu/du/Cargo.toml b/src/uu/du/Cargo.toml index 03c5e1f8f20..7a396403e81 100644 --- a/src/uu/du/Cargo.toml +++ b/src/uu/du/Cargo.toml @@ -1,25 +1,29 @@ [package] name = "uu_du" -version = "0.0.25" -authors = ["uutils developers"] -license = "MIT" description = "du ~ (uutils) display disk usage" - -homepage = "https://github.com/uutils/coreutils" repository = "https://github.com/uutils/coreutils/tree/main/src/uu/du" -keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] -categories = ["command-line-utilities"] -edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +edition.workspace = true +readme.workspace = true + +[lints] +workspace = true [lib] path = "src/du.rs" [dependencies] -chrono = { workspace = true } # For the --exclude & --exclude-from options glob = { workspace = true } clap = { workspace = true } -uucore = { workspace = true } +uucore = { workspace = true, features = ["format", "fsext", "parser", "time"] } +thiserror = { workspace = true } +fluent = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] windows-sys = { workspace = true, features = [ diff --git a/src/uu/du/du.md b/src/uu/du/du.md deleted file mode 100644 index ae3319ad6b6..00000000000 --- a/src/uu/du/du.md +++ /dev/null @@ -1,24 +0,0 @@ -# du - -``` -du [OPTION]... [FILE]... -du [OPTION]... --files0-from=F -``` - -Estimate file space usage - -## After Help - -Display values are in units of the first available SIZE from --block-size, -and the DU_BLOCK_SIZE, BLOCK_SIZE and BLOCKSIZE environment variables. -Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set). - -SIZE is an integer and optional unit (example: 10M is 10*1024*1024). -Units are K, M, G, T, P, E, Z, Y (powers of 1024) or KB, MB,... (powers -of 1000). - -PATTERN allows some advanced exclusions. For example, the following syntaxes -are supported: -`?` will match only one character -`*` will match zero or more characters -`{a,b}` will match a or b diff --git a/src/uu/du/locales/en-US.ftl b/src/uu/du/locales/en-US.ftl new file mode 100644 index 00000000000..b503d8d539f --- /dev/null +++ b/src/uu/du/locales/en-US.ftl @@ -0,0 +1,77 @@ +du-about = Estimate file space usage +du-usage = du [OPTION]... [FILE]... + du [OPTION]... --files0-from=F +du-after-help = Display values are in units of the first available SIZE from --block-size, + and the DU_BLOCK_SIZE, BLOCK_SIZE and BLOCKSIZE environment variables. + Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set). + + SIZE is an integer and optional unit (example: 10M is 10*1024*1024). + Units are K, M, G, T, P, E, Z, Y (powers of 1024) or KB, MB,... (powers + of 1000). + + PATTERN allows some advanced exclusions. For example, the following syntaxes + are supported: + ? will match only one character + { "*" } will match zero or more characters + {"{"}a,b{"}"} will match a or b + +# Help messages +du-help-print-help = Print help information. +du-help-all = write counts for all files, not just directories +du-help-apparent-size = print apparent sizes, rather than disk usage although the apparent size is usually smaller, it may be larger due to holes in ('sparse') files, internal fragmentation, indirect blocks, and the like +du-help-block-size = scale sizes by SIZE before printing them. E.g., '-BM' prints sizes in units of 1,048,576 bytes. See SIZE format below. +du-help-bytes = equivalent to '--apparent-size --block-size=1' +du-help-total = produce a grand total +du-help-max-depth = print the total for a directory (or file, with --all) only if it is N or fewer levels below the command line argument; --max-depth=0 is the same as --summarize +du-help-human-readable = print sizes in human readable format (e.g., 1K 234M 2G) +du-help-inodes = list inode usage information instead of block usage like --block-size=1K +du-help-block-size-1k = like --block-size=1K +du-help-count-links = count sizes many times if hard linked +du-help-dereference = follow all symbolic links +du-help-dereference-args = follow only symlinks that are listed on the command line +du-help-no-dereference = don't follow any symbolic links (this is the default) +du-help-block-size-1m = like --block-size=1M +du-help-null = end each output line with 0 byte rather than newline +du-help-separate-dirs = do not include size of subdirectories +du-help-summarize = display only a total for each argument +du-help-si = like -h, but use powers of 1000 not 1024 +du-help-one-file-system = skip directories on different file systems +du-help-threshold = exclude entries smaller than SIZE if positive, or entries greater than SIZE if negative +du-help-verbose = verbose mode (option not present in GNU/Coreutils) +du-help-exclude = exclude files that match PATTERN +du-help-exclude-from = exclude files that match any pattern in FILE +du-help-files0-from = summarize device usage of the NUL-terminated file names specified in file F; if F is -, then read names from standard input +du-help-time = show time of the last modification of any file in the directory, or any of its subdirectories. If WORD is given, show time as WORD instead of modification time: atime, access, use, ctime, status, birth or creation +du-help-time-style = show times using style STYLE: full-iso, long-iso, iso, +FORMAT FORMAT is interpreted like 'date' + +# Error messages +du-error-invalid-max-depth = invalid maximum depth { $depth } +du-error-summarize-depth-conflict = summarizing conflicts with --max-depth={ $depth } +du-error-invalid-time-style = invalid argument { $style } for 'time style' + Valid arguments are: + - 'full-iso' + - 'long-iso' + - 'iso' + - +FORMAT (e.g., +%H:%M) for a 'date'-style format + Try '{ $help }' for more information. +du-error-invalid-time-arg = 'birth' and 'creation' arguments for --time are not supported on this platform. +du-error-invalid-glob = Invalid exclude syntax: { $error } +du-error-cannot-read-directory = cannot read directory { $path } +du-error-cannot-access = cannot access { $path } +du-error-read-error-is-directory = { $file }: read error: Is a directory +du-error-cannot-open-for-reading = cannot open '{ $file }' for reading: No such file or directory +du-error-invalid-zero-length-file-name = { $file }:{ $line }: invalid zero-length file name +du-error-extra-operand-with-files0-from = extra operand { $file } + file operands cannot be combined with --files0-from +du-error-invalid-block-size-argument = invalid --{ $option } argument { $value } +du-error-cannot-access-no-such-file = cannot access { $path }: No such file or directory +du-error-printing-thread-panicked = Printing thread panicked. +du-error-invalid-suffix = invalid suffix in --{ $option } argument { $value } +du-error-invalid-argument = invalid --{ $option } argument { $value } +du-error-argument-too-large = --{ $option } argument { $value } too large + +# Verbose/status messages +du-verbose-ignored = { $path } ignored +du-verbose-adding-to-exclude-list = adding { $pattern } to the exclude list +du-total = total +du-warning-apparent-size-ineffective-with-inodes = options --apparent-size and -b are ineffective with --inodes diff --git a/src/uu/du/locales/fr-FR.ftl b/src/uu/du/locales/fr-FR.ftl new file mode 100644 index 00000000000..e89385213aa --- /dev/null +++ b/src/uu/du/locales/fr-FR.ftl @@ -0,0 +1,77 @@ +du-about = Estimer l'utilisation de l'espace disque des fichiers +du-usage = du [OPTION]... [FICHIER]... + du [OPTION]... --files0-from=F +du-after-help = Les valeurs affichées sont en unités de la première TAILLE disponible de --block-size, + et des variables d'environnement DU_BLOCK_SIZE, BLOCK_SIZE et BLOCKSIZE. + Sinon, les unités par défaut sont 1024 octets (ou 512 si POSIXLY_CORRECT est défini). + + TAILLE est un entier et une unité optionnelle (exemple : 10M est 10*1024*1024). + Les unités sont K, M, G, T, P, E, Z, Y (puissances de 1024) ou KB, MB,... (puissances + de 1000). + + MOTIF permet des exclusions avancées. Par exemple, les syntaxes suivantes + sont supportées : + ? correspondra à un seul caractère + { "*" } correspondra à zéro ou plusieurs caractères + {"{"}a,b{"}"} correspondra à a ou b + +# Messages d'aide +du-help-print-help = Afficher les informations d'aide. +du-help-all = afficher les comptes pour tous les fichiers, pas seulement les répertoires +du-help-apparent-size = afficher les tailles apparentes, plutôt que l'utilisation du disque bien que la taille apparente soit généralement plus petite, elle peut être plus grande en raison de trous dans les fichiers ('sparse'), la fragmentation interne, les blocs indirects, etc. +du-help-block-size = mettre à l'échelle les tailles par TAILLE avant de les afficher. Par ex., '-BM' affiche les tailles en unités de 1 048 576 octets. Voir le format TAILLE ci-dessous. +du-help-bytes = équivalent à '--apparent-size --block-size=1' +du-help-total = produire un total général +du-help-max-depth = afficher le total pour un répertoire (ou fichier, avec --all) seulement s'il est à N niveaux ou moins sous l'argument de ligne de commande ; --max-depth=0 est identique à --summarize +du-help-human-readable = afficher les tailles dans un format lisible par l'homme (p. ex., 1K 234M 2G) +du-help-inodes = lister les informations d'utilisation des inodes au lieu de l'utilisation des blocs comme --block-size=1K +du-help-block-size-1k = comme --block-size=1K +du-help-count-links = compter les tailles plusieurs fois si liées en dur +du-help-dereference = suivre tous les liens symboliques +du-help-dereference-args = suivre seulement les liens symboliques qui sont listés sur la ligne de commande +du-help-no-dereference = ne pas suivre les liens symboliques (c'est le défaut) +du-help-block-size-1m = comme --block-size=1M +du-help-null = terminer chaque ligne de sortie avec un octet 0 plutôt qu'une nouvelle ligne +du-help-separate-dirs = ne pas inclure la taille des sous-répertoires +du-help-summarize = afficher seulement un total pour chaque argument +du-help-si = comme -h, mais utiliser les puissances de 1000 et non 1024 +du-help-one-file-system = ignorer les répertoires sur des systèmes de fichiers différents +du-help-threshold = exclure les entrées plus petites que TAILLE si positive, ou les entrées plus grandes que TAILLE si négative +du-help-verbose = mode verbeux (option non présente dans GNU/Coreutils) +du-help-exclude = exclure les fichiers qui correspondent au MOTIF +du-help-exclude-from = exclure les fichiers qui correspondent à n'importe quel motif dans FICHIER +du-help-files0-from = résumer l'utilisation du périphérique des noms de fichiers terminés par NUL spécifiés dans le fichier F ; si F est -, alors lire les noms depuis l'entrée standard +du-help-time = montrer l'heure de la dernière modification de n'importe quel fichier dans le répertoire, ou n'importe lequel de ses sous-répertoires. Si MOT est donné, montrer l'heure comme MOT au lieu de l'heure de modification : atime, access, use, ctime, status, birth ou creation +du-help-time-style = montrer les heures en utilisant le style STYLE : full-iso, long-iso, iso, +FORMAT FORMAT est interprété comme 'date' + +# Messages d'erreur +du-error-invalid-max-depth = profondeur maximale invalide { $depth } +du-error-summarize-depth-conflict = la synthèse entre en conflit avec --max-depth={ $depth } +du-error-invalid-time-style = argument invalide { $style } pour 'style de temps' + Les arguments valides sont : + - 'full-iso' + - 'long-iso' + - '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-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 } +du-error-read-error-is-directory = { $file } : erreur de lecture : C'est un répertoire +du-error-cannot-open-for-reading = impossible d'ouvrir '{ $file }' en lecture : Aucun fichier ou répertoire de ce type +du-error-invalid-zero-length-file-name = { $file }:{ $line } : nom de fichier de longueur zéro invalide +du-error-extra-operand-with-files0-from = opérande supplémentaire { $file } + les opérandes de fichier ne peuvent pas être combinées avec --files0-from +du-error-invalid-block-size-argument = argument --{ $option } invalide { $value } +du-error-cannot-access-no-such-file = impossible d'accéder à { $path } : Aucun fichier ou répertoire de ce type +du-error-printing-thread-panicked = Le thread d'affichage a paniqué. +du-error-invalid-suffix = suffixe invalide dans l'argument --{ $option } { $value } +du-error-invalid-argument = argument --{ $option } invalide { $value } +du-error-argument-too-large = argument --{ $option } { $value } trop grand + +# Messages verbeux/de statut +du-verbose-ignored = { $path } ignoré +du-verbose-adding-to-exclude-list = ajout de { $pattern } à la liste d'exclusion +du-total = total +du-warning-apparent-size-ineffective-with-inodes = les options --apparent-size et -b sont inefficaces avec --inodes diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index b71d5f44c13..d4e20054d47 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -3,40 +3,42 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use chrono::{DateTime, Local}; -use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; +use clap::{Arg, ArgAction, ArgMatches, Command, builder::PossibleValue}; use glob::Pattern; use std::collections::HashSet; use std::env; -use std::error::Error; -use std::fmt::Display; -#[cfg(not(windows))] +use std::ffi::OsStr; +use std::ffi::OsString; use std::fs::Metadata; -use std::fs::{self, File}; -use std::io::{BufRead, BufReader}; +use std::fs::{self, DirEntry, File}; +use std::io::{BufRead, BufReader, stdout}; #[cfg(not(windows))] use std::os::unix::fs::MetadataExt; #[cfg(windows)] -use std::os::windows::fs::MetadataExt; -#[cfg(windows)] use std::os::windows::io::AsRawHandle; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::mpsc; use std::thread; -use std::time::{Duration, UNIX_EPOCH}; -use uucore::display::{print_verbatim, Quotable}; -use uucore::error::{set_exit_code, FromIo, UError, UResult, USimpleError}; +use thiserror::Error; +use uucore::display::{Quotable, print_verbatim}; +use uucore::error::{FromIo, UError, UResult, USimpleError, set_exit_code}; +use uucore::fsext::{MetadataTimeField, metadata_get_time}; use uucore::line_ending::LineEnding; -use uucore::parse_glob; -use uucore::parse_size::{parse_size_u64, ParseSizeError}; -use uucore::{format_usage, help_about, help_section, help_usage, show, show_error, show_warning}; +use uucore::translate; + +use uucore::LocalizedCommand; +use uucore::parser::parse_glob; +use uucore::parser::parse_size::{ParseSizeError, parse_size_u64}; +use uucore::parser::shortcut_value_parser::ShortcutValueParser; +use uucore::time::{FormatSystemTimeFallback, format, format_system_time}; +use uucore::{format_usage, show, show_error, show_warning}; #[cfg(windows)] use windows_sys::Win32::Foundation::HANDLE; #[cfg(windows)] use windows_sys::Win32::Storage::FileSystem::{ - FileIdInfo, FileStandardInfo, GetFileInformationByHandleEx, FILE_ID_128, FILE_ID_INFO, - FILE_STANDARD_INFO, + FILE_ID_128, FILE_ID_INFO, FILE_STANDARD_INFO, FileIdInfo, FileStandardInfo, + GetFileInformationByHandleEx, }; mod options { @@ -70,13 +72,6 @@ mod options { pub const FILE: &str = "FILE"; } -const ABOUT: &str = help_about!("du.md"); -const AFTER_HELP: &str = help_section!("after help", "du.md"); -const USAGE: &str = help_usage!("du.md"); - -// TODO: Support Z & Y (currently limited by size of u64) -const UNITS: [(char, u32); 6] = [('E', 6), ('P', 5), ('T', 4), ('G', 3), ('M', 2), ('K', 1)]; - struct TraversalOptions { all: bool, separate_dirs: bool, @@ -94,10 +89,11 @@ struct StatPrinter { threshold: Option, apparent_size: bool, size_format: SizeFormat, - time: Option