diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 9befa73fa..4296d58c4 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -12,7 +12,6 @@ RUN apt-get update \ gcc \ gdb \ gperf \ - jq \ libacl1-dev \ libattr1-dev \ libcap-dev \ diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 529ac2091..d3e3161b1 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -4,7 +4,7 @@ name: CICD # 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 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 (shell/tools) binutils choco clippy dmake 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 libsystemd codspeed env: @@ -36,7 +36,7 @@ jobs: name: Style/cargo-deny runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: EmbarkStudios/cargo-deny-action@v2 @@ -55,7 +55,7 @@ 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@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@nightly @@ -109,7 +109,7 @@ jobs: # - { os: macos-latest , features: feat_os_macos } # - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@master @@ -149,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@v20 + - uses: DavidAnson/markdownlint-cli2-action@v21 with: fix: "true" globs: | @@ -168,7 +168,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@master @@ -238,7 +238,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -265,7 +265,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -273,10 +273,7 @@ jobs: target: aarch64-unknown-linux-gnu - 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 + # Test build on the system missing libselinux (don't install libselinux1-dev at here) - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.9 - name: "`make build`" @@ -300,14 +297,17 @@ jobs: mv -t target/ target.cache/release 2>/dev/null || true - name: "`make nextest`" shell: bash - run: make nextest CARGOFLAGS="--profile ci --hide-progress-bar" + run: make nextest PROFILE=ci CARGOFLAGS="--hide-progress-bar" env: RUST_BACKTRACE: "1" - - name: "`make install COMPLETIONS=n MANPAGES=n LOCALES=n`" + + - name: "`make install PROFILE=release-fast COMPLETIONS=n MANPAGES=n LOCALES=n`" shell: bash run: | set -x - DESTDIR=/tmp/ make PROFILE=release COMPLETIONS=n MANPAGES=n LOCALES=n install + DESTDIR=/tmp/ make PROFILE=release-fast COMPLETIONS=n MANPAGES=n LOCALES=n install + # Check that utils are built with given profile + ./target/release-fast/true # Check that the utils are present test -f /tmp/usr/local/bin/tty # Check that the manpage is not present @@ -398,7 +398,7 @@ jobs: - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -427,7 +427,7 @@ jobs: - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@nightly @@ -453,7 +453,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -465,7 +465,7 @@ jobs: run: | ## Install dependencies sudo apt-get update - sudo apt-get install jq libselinux1-dev libsystemd-dev + sudo apt-get install libselinux1-dev libsystemd-dev - name: "`make install`" shell: bash run: | @@ -579,22 +579,21 @@ jobs: # - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: feat_selinux , use-cross: use-cross } - { os: ubuntu-latest , target: i686-unknown-linux-gnu , features: "feat_os_unix,test_risky_names", use-cross: use-cross } - { os: ubuntu-latest , target: i686-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } - - { 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,test_risky_names", use-cross: use-cross, skip-publish: true } - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,uudoc" , use-cross: no, workspace-tests: true } - { os: ubuntu-latest , target: x86_64-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } - # broken by network error - # - { os: ubuntu-latest , target: x86_64-unknown-redox , features: feat_os_unix_redox , use-cross: redoxer , skip-tests: true } + - { os: ubuntu-latest , target: x86_64-unknown-redox , features: feat_os_unix_redox , use-cross: redoxer , skip-tests: true } - { 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 + # PR #7964: Mac should still build even if the feature is not enabled. Do not publish this. + - { os: macos-latest , target: aarch64-apple-darwin , workspace-tests: true, skip-publish: 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@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@master @@ -638,7 +637,6 @@ jobs: case '${{ matrix.job.target }}' in aarch64-*) TARGET_ARCH=arm64 ;; arm-*-*hf) TARGET_ARCH=armhf ;; - i586-*) TARGET_ARCH=i586 ;; i686-*) TARGET_ARCH=i686 ;; x86_64-*) TARGET_ARCH=x86_64 ;; esac; @@ -658,25 +656,6 @@ jobs: # deployable tag? (ie, leading "vM" or "M"; M == version number) unset DEPLOY ; if [[ $REF_TAG =~ ^[vV]?[0-9].* ]]; then DEPLOY='true' ; fi outputs DEPLOY - # DPKG architecture? - unset DPKG_ARCH - case ${{ matrix.job.target }} in - x86_64-*-linux-*) DPKG_ARCH=amd64 ;; - *-linux-*) DPKG_ARCH=${TARGET_ARCH} ;; - esac - outputs DPKG_ARCH - # DPKG version? - unset DPKG_VERSION ; if [[ $REF_TAG =~ ^[vV]?[0-9].* ]]; then DPKG_VERSION=${REF_TAG/#[vV]/} ; fi - outputs DPKG_VERSION - # DPKG base name/conflicts? - DPKG_BASENAME=${PROJECT_NAME} - DPKG_CONFLICTS=${PROJECT_NAME}-musl - case ${{ matrix.job.target }} in *-musl) DPKG_BASENAME=${PROJECT_NAME}-musl ; DPKG_CONFLICTS=${PROJECT_NAME} ;; esac; - outputs DPKG_BASENAME DPKG_CONFLICTS - # DPKG name - unset DPKG_NAME; - if [[ -n $DPKG_ARCH && -n $DPKG_VERSION ]]; then DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb" ; fi - outputs DPKG_NAME # target-specific options # * CARGO_FEATURES_OPTION CARGO_FEATURES_OPTION='' ; @@ -733,9 +712,7 @@ jobs: shell: bash run: | ## Create build/work space - mkdir -p '${{ steps.vars.outputs.STAGING }}' mkdir -p '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}' - mkdir -p '${{ steps.vars.outputs.STAGING }}/dpkg' - name: Install/setup prerequisites shell: bash run: | @@ -783,7 +760,7 @@ jobs: - uses: taiki-e/install-action@v2 if: steps.vars.outputs.CARGO_CMD == 'redoxer' with: - tool: redoxer@0.2.37 + tool: redoxer@0.2.56 - name: Initialize toolchain-dependent workflow variables id: dep_vars shell: bash @@ -867,21 +844,6 @@ jobs: *) tar czf '${{ steps.vars.outputs.PKG_NAME }}' '${{ steps.vars.outputs.PKG_BASENAME }}'/* ;; esac popd >/dev/null - # dpkg - if [ -n "${{ steps.vars.outputs.DPKG_NAME }}" ]; then - DPKG_DIR="${{ steps.vars.outputs.STAGING }}/dpkg" - # binary - install -Dm755 'target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }}' "${DPKG_DIR}/usr/bin/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }}" - if [ -n "${{ steps.vars.outputs.STRIP }}" ]; then "${{ steps.vars.outputs.STRIP }}" "${DPKG_DIR}/usr/bin/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }}" ; fi - # README and LICENSE - (shopt -s nullglob; for f in [R]"EADME"{,.*}; do install -Dm644 "$f" "${DPKG_DIR}/usr/share/doc/${{ env.PROJECT_NAME }}/$f" ; done) - (shopt -s nullglob; for f in [L]"ICENSE"{-*,}{,.*}; do install -Dm644 "$f" "${DPKG_DIR}/usr/share/doc/${{ env.PROJECT_NAME }}/$f" ; done) - # control file - mkdir -p "${DPKG_DIR}/DEBIAN" - printf "Package: ${{ steps.vars.outputs.DPKG_BASENAME }}\nVersion: ${{ steps.vars.outputs.DPKG_VERSION }}\nSection: utils\nPriority: optional\nMaintainer: ${{ env.PROJECT_AUTH }}\nArchitecture: ${{ steps.vars.outputs.DPKG_ARCH }}\nProvides: ${{ env.PROJECT_NAME }}\nConflicts: ${{ steps.vars.outputs.DPKG_CONFLICTS }}\nDescription: ${{ env.PROJECT_DESC }}\n" > "${DPKG_DIR}/DEBIAN/control" - # build dpkg - fakeroot dpkg-deb --build "${DPKG_DIR}" "${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.DPKG_NAME }}" - fi - name: Publish uses: softprops/action-gh-release@v2 if: steps.vars.outputs.DEPLOY && matrix.job.skip-publish != true @@ -889,7 +851,6 @@ jobs: draft: true files: | ${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_NAME }} - ${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.DPKG_NAME }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -912,7 +873,7 @@ jobs: run: | ## VARs setup echo "TEST_SUMMARY_FILE=busybox-result.json" >> $GITHUB_OUTPUT - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: Swatinem/rust-cache@v2 @@ -995,7 +956,7 @@ 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@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@master @@ -1084,7 +1045,7 @@ jobs: # 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@v5 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.job.toolchain }} @@ -1158,11 +1119,6 @@ jobs: ;; 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 -Sy --needed mingw-w64-x86_64-gcc --noconfirm ; echo "C:\msys64\mingw64\bin" >> $GITHUB_PATH ;; - esac - ## Install the llvm-tools component to get access to `llvm-profdata` rustup component add llvm-tools @@ -1201,7 +1157,7 @@ jobs: - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1228,7 +1184,7 @@ jobs: - { os: macos-latest , features: feat_os_macos } # - { os: windows-latest , features: feat_os_windows } https://github.com/uutils/coreutils/issues/7044 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1236,6 +1192,7 @@ jobs: - name: build and test all features individually shell: bash run: | + command -v sudo && sudo rm -rf /usr/share/dotnet # avoid no space left CARGO_FEATURES_OPTION='--features=${{ matrix.job.features }}' ; for f in $(util/show-utils.sh ${CARGO_FEATURES_OPTION}) do @@ -1248,7 +1205,7 @@ jobs: needs: [ min_version, deps ] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1290,7 +1247,7 @@ jobs: - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1300,13 +1257,8 @@ jobs: - name: Verify stub binaries exist shell: bash run: | - if [ "${{ runner.os }}" = "Windows" ]; then - test -f target/debug/chcon.exe - test -f target/debug/runcon.exe - else - test -f target/debug/chcon - test -f target/debug/runcon - fi + test -f target/debug/chcon || test -f target/debug/chcon.exe + test -f target/debug/runcon || test -f target/debug/runcon.exe - name: Verify workspace builds with stubs run: cargo build --features ${{ matrix.job.features }} @@ -1316,7 +1268,7 @@ jobs: needs: [ min_version, deps ] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/CheckScripts.yml b/.github/workflows/CheckScripts.yml index d58f75d83..218695366 100644 --- a/.github/workflows/CheckScripts.yml +++ b/.github/workflows/CheckScripts.yml @@ -29,7 +29,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Run ShellCheck @@ -47,7 +47,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Setup shfmt diff --git a/.github/workflows/FixPR.yml b/.github/workflows/FixPR.yml index e5de0584b..e8451c525 100644 --- a/.github/workflows/FixPR.yml +++ b/.github/workflows/FixPR.yml @@ -26,7 +26,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Initialize job variables diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 1feb49677..55c570808 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -29,7 +29,6 @@ env: 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.8" jobs: native: @@ -38,10 +37,20 @@ jobs: steps: #### Get the code, setup cache - name: Checkout code (uutils) - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: path: 'uutils' persist-credentials: false + - name: Extract GNU version from build-gnu.sh + id: gnu-version + run: | + GNU_VERSION=$(grep '^release_tag_GNU=' uutils/util/build-gnu.sh | cut -d'"' -f2) + if [ -z "$GNU_VERSION" ]; then + echo "Error: Failed to extract GNU version from build-gnu.sh" + exit 1 + fi + echo "REPO_GNU_REF=${GNU_VERSION}" >> $GITHUB_ENV + echo "Extracted GNU version: ${GNU_VERSION}" - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -50,7 +59,7 @@ jobs: with: workspaces: "./uutils -> target" - name: Checkout code (GNU coreutils) - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'coreutils/coreutils' path: 'gnu' @@ -71,7 +80,8 @@ jobs: 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 libselinux1-dev attr quilt + ## Check that build-gnu.sh works on the non SELinux system by installing libselinux only on lima + sudo apt-get install -y autopoint gperf gdb python3-pyinotify valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev attr quilt - name: Add various locales shell: bash run: | @@ -89,6 +99,9 @@ jobs: 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 locale-gen --keep-existing fa_IR.UTF-8 # Iran + sudo locale-gen --keep-existing am_ET.UTF-8 # Ethiopia + sudo locale-gen --keep-existing th_TH.UTF-8 # Thailand sudo update-locale echo "After:" @@ -100,7 +113,7 @@ jobs: run: | ## Build binaries cd 'uutils' - bash util/build-gnu.sh --release-build + env PROFILE=release-small bash util/build-gnu.sh ### Run tests as user - name: Run GNU tests @@ -160,10 +173,20 @@ jobs: steps: #### Get the code, setup cache - name: Checkout code (uutils) - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: path: 'uutils' persist-credentials: false + - name: Extract GNU version from build-gnu.sh + id: gnu-version-selinux + run: | + GNU_VERSION=$(grep '^release_tag_GNU=' uutils/util/build-gnu.sh | cut -d'"' -f2) + if [ -z "$GNU_VERSION" ]; then + echo "Error: Failed to extract GNU version from build-gnu.sh" + exit 1 + fi + echo "REPO_GNU_REF=${GNU_VERSION}" >> $GITHUB_ENV + echo "Extracted GNU version: ${GNU_VERSION}" - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -172,7 +195,7 @@ jobs: with: workspaces: "./uutils -> target" - name: Checkout code (GNU coreutils) - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'coreutils/coreutils' path: 'gnu' @@ -216,7 +239,7 @@ jobs: - 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 sudo dnf -y install git autoconf autopoint bison texinfo gperf gcc gdb jq libacl-devel libattr-devel libcap-devel libselinux-devel attr rustup clang-devel texinfo-tex automake patch quilt lima rustup-init -y --default-toolchain stable - name: Copy the sources to VM run: | @@ -225,7 +248,7 @@ jobs: ### Build - name: Build binaries run: | - lima bash -c "cd ~/work/uutils/ && bash util/build-gnu.sh --release-build" + lima bash -c "cd ~/work/uutils/ && SELINUX_ENABLED=1 PROFILE=release-small bash util/build-gnu.sh" ### Run tests as user - name: Generate SELinux tests list @@ -309,7 +332,7 @@ jobs: outputs TEST_SUMMARY_FILE AGGREGATED_SUMMARY_FILE - name: Checkout code (uutils) - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: path: 'uutils' persist-credentials: false diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 46e2057eb..0dac4e358 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -22,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 -metrics-collection + COMMON_EMULATOR_OPTIONS: -no-window -noaudio -no-boot-anim -camera-back none -gpu off EMULATOR_DISK_SIZE: 12GB EMULATOR_HEAP_SIZE: 2048M EMULATOR_BOOT_TIMEOUT: 1200 # 20min @@ -36,15 +36,10 @@ jobs: matrix: os: [ubuntu-latest] # , macos-latest cores: [4] # , 6 - ram: [4096, 8192] + ram: [4096] api-level: [28] target: [google_apis_playstore] arch: [x86, x86_64] # , arm64-v8a - exclude: - - ram: 8192 - arch: x86 - - ram: 4096 - arch: x86_64 runs-on: ${{ matrix.os }} env: EMULATOR_RAM_SIZE: ${{ matrix.ram }} @@ -80,7 +75,7 @@ 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@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Collect information about runner diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index e1c042e23..1c2245123 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -37,6 +37,7 @@ jobs: - { package: uu_numfmt } - { package: uu_rm } - { package: uu_seq } + - { package: uu_shuf } - { package: uu_sort } - { package: uu_split } - { package: uu_tsort } @@ -45,7 +46,7 @@ jobs: - { package: uu_wc } - { package: uu_factor } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index aca6e829b..971c42bf4 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -32,7 +32,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@master @@ -82,7 +82,7 @@ jobs: - { os: macos-latest , features: feat_os_macos } - { os: windows-latest , features: feat_os_windows } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@master @@ -156,7 +156,7 @@ jobs: job: - { os: ubuntu-latest , features: feat_os_unix } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Initialize workflow variables @@ -174,7 +174,7 @@ jobs: - name: Install/setup prerequisites shell: bash run: | - sudo apt-get -y update ; sudo apt-get -y install npm ; sudo npm install cspell -g ; + sudo npm install cspell -g ; - name: Run `cspell` shell: bash run: | @@ -194,7 +194,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false @@ -206,7 +206,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false @@ -230,7 +230,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/devcontainer.yml b/.github/workflows/devcontainer.yml index cbc0d0f87..ecc65bc99 100644 --- a/.github/workflows/devcontainer.yml +++ b/.github/workflows/devcontainer.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 45 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Run test in devcontainer diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 47d5eb6ff..53104fb71 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install/setup prerequisites shell: bash diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index c7a788a77..84f6b55b2 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -34,7 +34,7 @@ jobs: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: Swatinem/rust-cache@v2 @@ -43,7 +43,7 @@ jobs: with: disable_annotations: true - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.2.6 + uses: vmactions/freebsd-vm@v1.2.9 with: usesh: true sync: rsync @@ -130,7 +130,7 @@ jobs: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: Swatinem/rust-cache@v2 @@ -139,7 +139,7 @@ jobs: with: disable_annotations: true - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.2.6 + uses: vmactions/freebsd-vm@v1.2.9 with: usesh: true sync: rsync @@ -160,7 +160,6 @@ jobs: WORKSPACE="${WORKSPACE_PARENT}/${REPO_NAME}" # pw adduser -n ${TEST_USER} -d /root/ -g wheel -c "Coreutils user to build" -w random - # chown -R ${TEST_USER}:wheel /root/ "${WORKSPACE_PARENT}"/ chown -R ${TEST_USER}:wheel /root/ "${WORKSPACE_PARENT}"/ whoami # @@ -204,7 +203,7 @@ jobs: cargo nextest run --hide-progress-bar --profile ci --features "\$UUCORE_FEATURES" -p uucore || FAULT=1 fi # Test building with make - if (test -z "\$FAULT"); then make PROFILE=ci || FAULT=1 ; fi + if (test -z "\$FAULT"); then make || 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 42c247106..f7ba66595 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -21,7 +21,7 @@ jobs: name: Build and test uufuzz examples runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -55,7 +55,7 @@ jobs: name: Build the fuzzers runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@nightly @@ -99,7 +99,7 @@ jobs: - { name: fuzz_non_utf8_paths, should_pass: true } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@nightly @@ -211,7 +211,7 @@ jobs: runs-on: ubuntu-latest if: always() steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Download all stats diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml index f5f1871f7..c7154f490 100644 --- a/.github/workflows/l10n.yml +++ b/.github/workflows/l10n.yml @@ -36,7 +36,7 @@ jobs: - { os: macos-latest , features: "feat_os_macos" } - { os: windows-latest , features: "feat_os_windows" } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -76,7 +76,7 @@ jobs: name: L10n/Fluent Syntax Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Setup Python @@ -131,7 +131,7 @@ jobs: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -141,7 +141,7 @@ jobs: - name: Install/setup prerequisites shell: bash run: | - sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev locales + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev sudo locale-gen --keep-existing fr_FR.UTF-8 locale -a | grep -i fr || exit 1 - name: Build coreutils with clap localization support @@ -301,7 +301,7 @@ jobs: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -312,7 +312,7 @@ jobs: shell: bash run: | ## Install/setup prerequisites - sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev locales + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev - name: Generate French locale shell: bash run: | @@ -416,7 +416,7 @@ jobs: - { os: ubuntu-latest , features: "feat_os_unix" } - { os: macos-latest , features: "feat_os_macos" } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -429,10 +429,10 @@ jobs: ## Install/setup prerequisites case '${{ matrix.job.os }}' in ubuntu-*) - sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev build-essential + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev ;; macos-*) - brew install coreutils make + brew install coreutils ;; esac - name: Install via make and test multi-call binary @@ -453,22 +453,22 @@ jobs: # 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/" + ls -la target/release-small/coreutils || echo "No coreutils binary in target/release-small/" - make FEATURES="${{ matrix.job.features }}" PROFILE=release MULTICALL=y + make FEATURES="${{ matrix.job.features }}" PROFILE=release-small MULTICALL=y - echo "After build, checking target/release/:" - ls -la target/release/ | grep -E "(coreutils|^total)" || echo "Build may have failed" + echo "After build, checking target/release-small/:" + ls -la target/release-small/ | 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" + ls -la target/release-small/coreutils 2>/dev/null || echo "No coreutils in target/release-small" # 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 "About to run: make install DESTDIR=\"$INSTALL_DIR\" PREFIX=/usr PROFILE=release-small MULTICALL=y" echo "Expected install path: $INSTALL_DIR/usr/bin/coreutils" - make install DESTDIR="$INSTALL_DIR" PREFIX=/usr PROFILE=release MULTICALL=y || { + make install DESTDIR="$INSTALL_DIR" PREFIX=/usr PROFILE=release-small 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" @@ -482,13 +482,13 @@ jobs: 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 + if [ -f "target/release-small/coreutils" ]; then + echo "✓ Build succeeded - coreutils binary exists in target/release-small/" + ls -la target/release-small/coreutils else - echo "✗ Build failed - no coreutils binary in target/release/" - echo "Contents of target/release/:" - ls -la target/release/ | head -20 + echo "✗ Build failed - no coreutils binary in target/release-small/" + echo "Contents of target/release-small/:" + ls -la target/release-small/ | head -20 exit 1 fi @@ -567,7 +567,7 @@ jobs: - { os: ubuntu-latest , features: "feat_os_unix" } - { os: macos-latest , features: "feat_os_macos" } steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -580,13 +580,13 @@ jobs: ## Install/setup prerequisites case '${{ matrix.job.os }}' in ubuntu-*) - sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev build-essential locales + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev # 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 + brew install coreutils ;; esac - name: Test Make installation @@ -600,8 +600,8 @@ jobs: 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 + make FEATURES="${{ matrix.job.features }}" PROFILE=release-small MULTICALL=y + make install DESTDIR="$MAKE_INSTALL_DIR" PREFIX=/usr PROFILE=release-small MULTICALL=y # Verify installation echo "Testing make-installed binaries..." @@ -900,7 +900,7 @@ jobs: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -912,7 +912,7 @@ jobs: run: | ## Install/setup prerequisites including locale support sudo apt-get -y update - sudo apt-get -y install libselinux1-dev locales build-essential + sudo apt-get -y install libselinux1-dev # 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 @@ -928,8 +928,8 @@ jobs: 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 + make FEATURES="feat_os_unix" PROFILE=release-small MULTICALL=y + make install DESTDIR="$INSTALL_DIR" PREFIX=/usr PROFILE=release-small MULTICALL=y # Debug: Show what was installed echo "Contents of installation directory:" @@ -1109,8 +1109,8 @@ jobs: # 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 + make FEATURES="feat_os_unix" PROFILE=release-small MULTICALL=y + make install DESTDIR="$STANDARD_BUILD_INSTALL_DIR" PREFIX=/usr PROFILE=release-small MULTICALL=y # Verify standard build binary works if "$STANDARD_BUILD_INSTALL_DIR/usr/bin/coreutils" --version >/dev/null 2>&1; then @@ -1131,7 +1131,7 @@ jobs: SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1151,7 +1151,7 @@ jobs: name: L10n/Locale Embedding - Cat Utility runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1160,7 +1160,7 @@ jobs: # Use different cache key for each build to avoid conflicts key: cat-locale-embedding - name: Install prerequisites - run: sudo apt-get -y update && sudo apt-get -y install libselinux1-dev build-essential + run: sudo apt-get -y update && sudo apt-get -y install libselinux1-dev - name: Build cat with targeted locale embedding run: UUCORE_TARGET_UTIL=cat cargo build -p uu_cat --release - name: Verify cat locale count @@ -1183,7 +1183,7 @@ jobs: name: L10n/Locale Embedding - Ls Utility runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1192,7 +1192,7 @@ jobs: # Use different cache key for each build to avoid conflicts key: ls-locale-embedding - name: Install prerequisites - run: sudo apt-get -y update && sudo apt-get -y install libselinux1-dev build-essential + run: sudo apt-get -y update && sudo apt-get -y install libselinux1-dev - name: Build ls with targeted locale embedding run: UUCORE_TARGET_UTIL=ls cargo build -p uu_ls --release - name: Verify ls locale count @@ -1215,7 +1215,7 @@ jobs: name: L10n/Locale Embedding - Multicall Binary runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable @@ -1224,7 +1224,7 @@ jobs: # Use different cache key for each build to avoid conflicts key: multicall-locale-embedding - name: Install prerequisites - run: sudo apt-get -y update && sudo apt-get -y install libselinux1-dev build-essential + run: sudo apt-get -y update && sudo apt-get -y install libselinux1-dev - name: Build multicall binary with all locales run: cargo build --release - name: Verify multicall locale count @@ -1245,10 +1245,138 @@ jobs: exit 1 fi + l10n_locale_embedding_cargo_install: + name: L10n/Locale Embedding - Cargo Install + runs-on: ubuntu-latest + env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + key: cargo-install-locale-embedding + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install prerequisites + run: | + sudo apt-get -y update + sudo apt-get -y install libselinux1-dev + # Generate French locale for testing + sudo locale-gen --keep-existing fr_FR.UTF-8 + locale -a | grep -i fr || exit 1 + + - name: Test English locale embedding (default) + run: | + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + + echo "Building uu_yes with LANG=$LANG" + cargo build --package uu_yes --release + + # Find the generated embedded_locales.rs + locale_file=$(find target/release/build -path "*/uu_yes-*/out/embedded_locales.rs" -o -path "*/uucore-*/out/embedded_locales.rs" | head -1) + if [ -z "$locale_file" ]; then + echo "ERROR: Could not find embedded_locales.rs" + exit 1 + fi + + echo "Found embedded_locales.rs at: $locale_file" + echo "Checking embedded locales..." + + # Should contain en-US + if grep -q 'yes/en-US\.ftl' "$locale_file" || grep -q 'uucore/en-US\.ftl' "$locale_file"; then + echo "✓ Found en-US locale (fallback)" + else + echo "✗ ERROR: en-US locale not found" + exit 1 + fi + + # Should NOT contain fr-FR when building with en_US.UTF-8 + if grep -q 'yes/fr-FR\.ftl' "$locale_file" || grep -q 'uucore/fr-FR\.ftl' "$locale_file"; then + echo "✗ ERROR: Unexpectedly found fr-FR locale when LANG=en_US.UTF-8" + exit 1 + else + echo "✓ Correctly omitted fr-FR locale" + fi + + echo "✓ SUCCESS: English locale embedding working correctly" + + - name: Test French locale embedding (system locale) + run: | + export LANG=fr_FR.UTF-8 + export LC_ALL=fr_FR.UTF-8 + + # Clean previous build to ensure fresh compile + cargo clean -p uu_yes + cargo clean -p uucore + + echo "Building uu_yes with LANG=$LANG" + cargo build --package uu_yes --release + + # Find the generated embedded_locales.rs + locale_file=$(find target/release/build -path "*/uu_yes-*/out/embedded_locales.rs" -o -path "*/uucore-*/out/embedded_locales.rs" | head -1) + if [ -z "$locale_file" ]; then + echo "ERROR: Could not find embedded_locales.rs" + exit 1 + fi + + echo "Found embedded_locales.rs at: $locale_file" + echo "Checking embedded locales..." + + # Should contain en-US (fallback) + if grep -q 'yes/en-US\.ftl' "$locale_file" || grep -q 'uucore/en-US\.ftl' "$locale_file"; then + echo "✓ Found en-US locale (fallback)" + else + echo "✗ ERROR: en-US locale not found" + exit 1 + fi + + # Should contain fr-FR when building with fr_FR.UTF-8 + if grep -q 'yes/fr-FR\.ftl' "$locale_file" || grep -q 'uucore/fr-FR\.ftl' "$locale_file"; then + echo "✓ Found fr-FR locale (system locale from LANG)" + else + echo "Note: fr-FR locale not found - this is expected if French translation doesn't exist yet" + echo "::notice::French locale for 'yes' utility may not be available" + fi + + echo "✓ SUCCESS: System locale detection working correctly" + + - name: Test locale count is reasonable + run: | + export LANG=fr_FR.UTF-8 + cargo clean -p uu_yes + cargo build --package uu_yes --release + + locale_file=$(find target/release/build -path "*/uucore-*/out/embedded_locales.rs" | head -1) + if [ -z "$locale_file" ]; then + echo "ERROR: Could not find uucore embedded_locales.rs" + exit 1 + fi + + # Count embedded locales (should be en-US + system locale, not all locales) + locale_count=$(grep -c '/en-US\.ftl\|/fr-FR\.ftl' "$locale_file" || echo "0") + echo "uu_yes has $locale_count embedded locale entries for yes utility" + + # For a single utility build, should have minimal locales (en-US + optionally system locale) + # Not the full multicall set + total_match_count=$(grep -c '=> Some(r###' "$locale_file" || echo "0") + echo "Total embedded entries: $total_match_count" + + if [ "$total_match_count" -le 10 ]; then + echo "✓ SUCCESS: Locale embedding is targeted ($total_match_count entries)" + else + echo "::warning::More locales than expected ($total_match_count entries)" + echo "This might be expected for utility + uucore locales" + fi + l10n_locale_embedding_regression_test: name: L10n/Locale Embedding Regression Test runs-on: ubuntu-latest - needs: [l10n_locale_embedding_cat, l10n_locale_embedding_ls, l10n_locale_embedding_multicall] + needs: [l10n_locale_embedding_cat, l10n_locale_embedding_ls, l10n_locale_embedding_multicall, l10n_locale_embedding_cargo_install] steps: - name: All locale embedding tests passed run: echo "✓ All locale embedding tests passed successfully" diff --git a/.github/workflows/openbsd.yml b/.github/workflows/openbsd.yml new file mode 100644 index 000000000..f7bae27ef --- /dev/null +++ b/.github/workflows/openbsd.yml @@ -0,0 +1,199 @@ +name: OpenBSD + +# spell-checker:ignore sshfs usesh vmactions taiki Swatinem esac fdescfs fdesc sccache nextest copyback logind bindgen libclang + +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 + +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: + style: + name: Style and Lint + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + job: + - { features: unix } + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Prepare, build and test + uses: vmactions/openbsd-vm@v1 + with: + usesh: true + sync: rsync + copyback: false + mem: 4096 + # We need jq and GNU coreutils to run show-utils.sh and bash to use inline shell string replacement + # Use sudo-- to get the default sudo package without ambiguity + # Install rust and cargo from OpenBSD packages + prepare: pkg_add curl sudo-- jq coreutils bash rust rust-clippy rust-rustfmt llvm-- + run: | + ## Prepare, build, and test + # implementation modelled after ref: + # * NOTE: All steps need to be run in this block, otherwise, we are operating back on the mac host + set -e + # + TEST_USER=tester + REPO_NAME=${GITHUB_WORKSPACE##*/} + WORKSPACE_PARENT="/home/runner/work/${REPO_NAME}" + WORKSPACE="${WORKSPACE_PARENT}/${REPO_NAME}" + # + useradd -m -G wheel ${TEST_USER} + chown -R ${TEST_USER}:wheel /root/ "${WORKSPACE_PARENT}"/ + whoami + # + # Further work needs to be done in a sudo as we are changing users + sudo -i -u ${TEST_USER} bash << EOF + set -e + whoami + # Rust is installed from packages, no need for rustup + # Set up PATH for cargo + export PATH="/usr/local/bin:$PATH" + ## VARs setup + cd "${WORKSPACE}" + unset FAIL_ON_FAULT ; case '${{ env.STYLE_FAIL_ON_FAULT }}' in + ''|0|f|false|n|no|off) FAULT_TYPE=warning ;; + *) FAIL_ON_FAULT=true ; FAULT_TYPE=error ;; + esac; + FAULT_PREFIX=\$(echo "\${FAULT_TYPE}" | tr '[:lower:]' '[:upper:]') + # * determine sub-crate utility list + UTILITY_LIST="\$(./util/show-utils.sh --features ${{ matrix.job.features }})" + CARGO_UTILITY_LIST_OPTIONS="\$(for u in \${UTILITY_LIST}; do echo -n "-puu_\${u} "; done;)" + ## Info + # environment + echo "## environment" + echo "CI='${CI}'" + echo "REPO_NAME='${REPO_NAME}'" + echo "TEST_USER='${TEST_USER}'" + echo "WORKSPACE_PARENT='${WORKSPACE_PARENT}'" + echo "WORKSPACE='${WORKSPACE}'" + echo "FAULT_PREFIX='\${FAULT_PREFIX}'" + echo "UTILITY_LIST='\${UTILITY_LIST}'" + env | sort + # tooling info + echo "## tooling info" + cargo -V + rustc -V + # + # To ensure that files are cleaned up, we don't want to exit on error + set +e + unset FAULT + ## cargo fmt testing + echo "## cargo fmt testing" + # * 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 ; } + ## cargo clippy lint testing + if [ -z "\${FAULT}" ]; then + echo "## cargo clippy lint testing" + # * convert any warnings to GHA UI annotations; ref: + S=\$(cargo clippy --all-targets \${CARGO_UTILITY_LIST_OPTIONS} -- -D warnings 2>&1) && printf "%s\n" "\$S" || { printf "%s\n" "\$S" ; printf "%s" "\$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*\$/::\${FAULT_TYPE} file=\2,line=\3,col=\4::\${FAULT_PREFIX}: \\\`cargo clippy\\\`: \1 (file:'\2', line:\3)/p;" -e '}' ; FAULT=true ; } + fi + # Clean to avoid to rsync back the files + cargo clean + if [ -n "\${FAIL_ON_FAULT}" ] && [ -n "\${FAULT}" ]; then exit 1 ; fi + EOF + + test: + name: Tests + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + job: + - { features: unix } + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Prepare, build and test + uses: vmactions/openbsd-vm@v1 + with: + usesh: true + sync: rsync + copyback: false + mem: 4096 + # Install rust and build dependencies from OpenBSD packages (llvm provides libclang for bindgen) + prepare: pkg_add curl gmake sudo-- jq rust llvm-- + run: | + ## Prepare, build, and test + # implementation modelled after ref: + # * NOTE: All steps need to be run in this block, otherwise, we are operating back on the mac host + set -e + # + TEST_USER=tester + REPO_NAME=${GITHUB_WORKSPACE##*/} + WORKSPACE_PARENT="/home/runner/work/${REPO_NAME}" + WORKSPACE="${WORKSPACE_PARENT}/${REPO_NAME}" + # + useradd -m -G wheel ${TEST_USER} + chown -R ${TEST_USER}:wheel /root/ "${WORKSPACE_PARENT}"/ + whoami + # Add fake host for reverse DNS lookup (needed for hostname test) + printf "10.0.2.15\topenbsd.my.domain openbsd\n" >> /etc/hosts + # + # Further work needs to be done in a sudo as we are changing users + sudo -i -u ${TEST_USER} sh << EOF + set -e + whoami + # Increase max open files (512 by default) + ulimit -n 1024 + # Rust is installed from packages, no need for rustup + # Set up PATH for cargo + export PATH="/usr/local/bin:$PATH" + # Install nextest + mkdir -p ~/.cargo/bin + # Note: nextest might not have OpenBSD builds, so we'll use regular cargo test + ## Info + # environment + echo "## environment" + echo "CI='${CI}'" + echo "REPO_NAME='${REPO_NAME}'" + echo "TEST_USER='${TEST_USER}'" + echo "WORKSPACE_PARENT='${WORKSPACE_PARENT}'" + echo "WORKSPACE='${WORKSPACE}'" + env | sort + # tooling info + echo "## tooling info" + cargo -V + rustc -V + # + # To ensure that files are cleaned up, we don't want to exit on error + set +e + cd "${WORKSPACE}" + unset FAULT + cargo build || FAULT=1 + export PATH=~/.cargo/bin:${PATH} + export RUST_BACKTRACE=1 + export CARGO_TERM_COLOR=always + # Use cargo test since nextest might not support OpenBSD + if (test -z "\$FAULT"); then cargo test --features '${{ matrix.job.features }}' || FAULT=1 ; fi + # There is no systemd-logind on OpenBSD, so test all features except feat_systemd_logind + if (test -z "\$FAULT"); then + UUCORE_FEATURES=\$(cargo metadata --format-version=1 --no-deps | jq -r '.packages[] | select(.name == "uucore") | .features | keys | .[]' | grep -v "feat_systemd_logind" | paste -s -d "," -) + cargo test --features "\$UUCORE_FEATURES" -p uucore || FAULT=1 + fi + # Test building with make + if (test -z "\$FAULT"); then make || FAULT=1 ; fi + # Clean to avoid to rsync back the files + cargo clean + if (test -n "\$FAULT"); then exit 1 ; fi + EOF diff --git a/.github/workflows/wsl2.yml b/.github/workflows/wsl2.yml index 4f342847c..1764a03fc 100644 --- a/.github/workflows/wsl2.yml +++ b/.github/workflows/wsl2.yml @@ -27,7 +27,7 @@ jobs: job: - { os: windows-latest, distribution: Ubuntu-24.04, features: feat_os_unix} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Install WSL2 diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index 70cbd9379..a757953b4 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -41,6 +41,7 @@ duplicative dsync endianness enqueue +ERANGE errored executable executables @@ -120,6 +121,7 @@ pseudoprimes quantiles readonly reparse +rposition seedable semver semiprime @@ -187,3 +189,16 @@ nofield # * clippy uninlined nonminimal + +# * CPU/hardware features +ASIMD +asimd +hwcaps +PCLMUL +pclmul +PCLMULQDQ +pclmulqdq +TUNABLES +tunables +VMULL +vmull diff --git a/Cargo.lock b/Cargo.lock index 05501281b..dae4e963c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,12 +32,6 @@ 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" @@ -121,12 +115,6 @@ 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.9" @@ -303,12 +291,6 @@ 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.2.27" @@ -347,34 +329,7 @@ checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", "num-traits", - "windows-link 0.2.1", -] - -[[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", + "windows-link", ] [[package]] @@ -390,18 +345,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -412,9 +367,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.60" +version = "4.5.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e602857739c5a4291dfa33b5a298aeac9006185229a700e5810a3ef7272d971" +checksum = "39615915e2ece2550c0149addac32fb5bd312c657f43845bb9088cb9c8a7c992" dependencies = [ "clap", ] @@ -579,7 +534,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "coreutils" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bincode", "chrono", @@ -744,13 +699,14 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc-fast" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311eddc0ebdb918fb3f9ce10304736a8e94bfbe48e3dfd61c04754fdbb5a4d67" +checksum = "a2f7c8d397a6353ef0c1d6217ab91b3ddb5431daf57fd013f506b967dcf44458" dependencies = [ "crc", "digest", "rustversion", + "spin", ] [[package]] @@ -762,39 +718,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "criterion" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "itertools 0.13.0", - "num-traits", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools 0.10.5", -] - [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -866,9 +789,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffc71fcdcdb40d6f087edddf7f8f1f8f79e6cf922f555a9ee8779752d4819bd" +checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e" dependencies = [ "ctor-proc-macro", "dtor", @@ -1361,20 +1284,20 @@ checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" [[package]] name = "hostname" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" dependencies = [ "cfg-if", "libc", - "windows-link 0.1.3", + "windows-link", ] [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1396,11 +1319,10 @@ dependencies = [ [[package]] name = "icu_collator" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ad4c6a556938dfd31f75a8c54141079e8821dc697ffb799cfe0f0fa11f2edc" +checksum = "32eed11a5572f1088b63fa21dc2e70d4a865e5739fc2d10abc05be93bae97019" dependencies = [ - "displaydoc", "icu_collator_data", "icu_collections", "icu_locale", @@ -1416,15 +1338,15 @@ dependencies = [ [[package]] name = "icu_collator_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d880b8e680799eabd90c054e1b95526cd48db16c95269f3c89fb3117e1ac92c5" +checksum = "5ab06f0e83a613efddba3e4913e00e43ed4001fae651cb7d40fc7e66b83b6fb9" [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -1435,34 +1357,31 @@ dependencies = [ [[package]] name = "icu_decimal" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec61c43fdc4e368a9f450272833123a8ef0d7083a44597660ce94d791b8a2e2" +checksum = "a38c52231bc348f9b982c1868a2af3195199623007ba2c7650f432038f5b3e8e" 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" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b70963bc35f9bdf1bc66a5c1f458f4991c1dc71760e00fa06016b2c76b2738d5" +checksum = "2905b4044eab2dd848fe84199f9195567b63ab3a93094711501363f63546fef7" [[package]] name = "icu_locale" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ae5921528335e91da1b6c695dbf1ec37df5ac13faa3f91e5640be93aa2fbefd" +checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_locale_data", @@ -1474,12 +1393,13 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", @@ -1487,63 +1407,63 @@ dependencies = [ [[package]] name = "icu_locale_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fdef0c124749d06a743c69e938350816554eb63ac979166590e2b4ee4252765" +checksum = "f03e2fcaefecdf05619f3d6f91740e79ab969b4dd54f77cbf546b1d0d28e3147" [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", + "utf16_iter", + "utf8_iter", + "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" 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" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", + "serde", "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1563,9 +1483,9 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65" +checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" dependencies = [ "console", "portable-atomic", @@ -1619,15 +1539,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -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" @@ -1664,7 +1575,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1962,7 +1873,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2036,12 +1947,6 @@ dependencies = [ "libc", ] -[[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" @@ -2076,12 +1981,6 @@ dependencies = [ "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.7.3" @@ -2132,9 +2031,9 @@ dependencies = [ [[package]] name = "parse_datetime" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77d45119ed61100f40b2389d8ed12e51ec869046d4279afbb5a7c73a4733be36" +checksum = "acea383beda9652270f3c9678d83aa58cbfc16880343cae0c0c8c7d6c0974132" dependencies = [ "jiff", "num-traits", @@ -2208,34 +2107,6 @@ dependencies = [ "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 = "1.11.1" @@ -2253,11 +2124,12 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ - "serde", + "serde_core", + "writeable", "zerovec", ] @@ -2567,7 +2439,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2807,6 +2679,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2867,7 +2745,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2984,16 +2862,6 @@ dependencies = [ "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" @@ -3082,9 +2950,9 @@ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" [[package]] name = "unit-prefix" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" [[package]] name = "unty" @@ -3136,7 +3004,7 @@ dependencies = [ [[package]] name = "uu_arch" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3146,7 +3014,7 @@ dependencies = [ [[package]] name = "uu_base32" -version = "0.4.0" +version = "0.5.0" dependencies = [ "base64-simd", "clap", @@ -3156,7 +3024,7 @@ dependencies = [ [[package]] name = "uu_base64" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3168,7 +3036,7 @@ dependencies = [ [[package]] name = "uu_basename" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3177,7 +3045,7 @@ dependencies = [ [[package]] name = "uu_basenc" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3187,7 +3055,7 @@ dependencies = [ [[package]] name = "uu_cat" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3202,7 +3070,7 @@ dependencies = [ [[package]] name = "uu_chcon" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3215,7 +3083,7 @@ dependencies = [ [[package]] name = "uu_chgrp" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3224,7 +3092,7 @@ dependencies = [ [[package]] name = "uu_chmod" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3234,7 +3102,7 @@ dependencies = [ [[package]] name = "uu_chown" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3243,7 +3111,7 @@ dependencies = [ [[package]] name = "uu_chroot" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3253,7 +3121,7 @@ dependencies = [ [[package]] name = "uu_cksum" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3265,7 +3133,7 @@ dependencies = [ [[package]] name = "uu_comm" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3274,7 +3142,7 @@ dependencies = [ [[package]] name = "uu_cp" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3294,7 +3162,7 @@ dependencies = [ [[package]] name = "uu_csplit" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3305,7 +3173,7 @@ dependencies = [ [[package]] name = "uu_cut" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bstr", "clap", @@ -3318,7 +3186,7 @@ dependencies = [ [[package]] name = "uu_date" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3331,7 +3199,7 @@ dependencies = [ [[package]] name = "uu_dd" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3345,7 +3213,7 @@ dependencies = [ [[package]] name = "uu_df" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3357,7 +3225,7 @@ dependencies = [ [[package]] name = "uu_dir" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "uu_ls", @@ -3366,7 +3234,7 @@ dependencies = [ [[package]] name = "uu_dircolors" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3375,7 +3243,7 @@ dependencies = [ [[package]] name = "uu_dirname" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3384,7 +3252,7 @@ dependencies = [ [[package]] name = "uu_du" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3398,7 +3266,7 @@ dependencies = [ [[package]] name = "uu_echo" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3407,7 +3275,7 @@ dependencies = [ [[package]] name = "uu_env" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3419,7 +3287,7 @@ dependencies = [ [[package]] name = "uu_expand" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3432,7 +3300,7 @@ dependencies = [ [[package]] name = "uu_expr" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3445,7 +3313,7 @@ dependencies = [ [[package]] name = "uu_factor" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3453,23 +3321,13 @@ dependencies = [ "num-bigint", "num-prime", "num-traits", + "rand 0.9.2", "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.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3478,7 +3336,7 @@ dependencies = [ [[package]] name = "uu_fmt" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3489,18 +3347,19 @@ dependencies = [ [[package]] name = "uu_fold" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", "fluent", "tempfile", + "unicode-width 0.2.2", "uucore", ] [[package]] name = "uu_groups" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3510,7 +3369,7 @@ dependencies = [ [[package]] name = "uu_hashsum" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3521,7 +3380,7 @@ dependencies = [ [[package]] name = "uu_head" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3532,7 +3391,7 @@ dependencies = [ [[package]] name = "uu_hostid" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3542,7 +3401,7 @@ dependencies = [ [[package]] name = "uu_hostname" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "dns-lookup", @@ -3554,7 +3413,7 @@ dependencies = [ [[package]] name = "uu_id" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3564,7 +3423,7 @@ dependencies = [ [[package]] name = "uu_install" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "file_diff", @@ -3577,7 +3436,7 @@ dependencies = [ [[package]] name = "uu_join" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3588,7 +3447,7 @@ dependencies = [ [[package]] name = "uu_kill" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3598,7 +3457,7 @@ dependencies = [ [[package]] name = "uu_link" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3607,7 +3466,7 @@ dependencies = [ [[package]] name = "uu_ln" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3617,7 +3476,7 @@ dependencies = [ [[package]] name = "uu_logname" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3627,7 +3486,7 @@ dependencies = [ [[package]] name = "uu_ls" -version = "0.4.0" +version = "0.5.0" dependencies = [ "ansi-width", "clap", @@ -3647,7 +3506,7 @@ dependencies = [ [[package]] name = "uu_mkdir" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3656,7 +3515,7 @@ dependencies = [ [[package]] name = "uu_mkfifo" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3666,7 +3525,7 @@ dependencies = [ [[package]] name = "uu_mknod" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3676,7 +3535,7 @@ dependencies = [ [[package]] name = "uu_mktemp" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3688,7 +3547,7 @@ dependencies = [ [[package]] name = "uu_more" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "crossterm", @@ -3700,7 +3559,7 @@ dependencies = [ [[package]] name = "uu_mv" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3716,7 +3575,7 @@ dependencies = [ [[package]] name = "uu_nice" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3727,7 +3586,7 @@ dependencies = [ [[package]] name = "uu_nl" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3739,7 +3598,7 @@ dependencies = [ [[package]] name = "uu_nohup" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3750,7 +3609,7 @@ dependencies = [ [[package]] name = "uu_nproc" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3760,7 +3619,7 @@ dependencies = [ [[package]] name = "uu_numfmt" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3772,18 +3631,19 @@ dependencies = [ [[package]] name = "uu_od" -version = "0.4.0" +version = "0.5.0" dependencies = [ "byteorder", "clap", "fluent", "half", + "libc", "uucore", ] [[package]] name = "uu_paste" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3792,7 +3652,7 @@ dependencies = [ [[package]] name = "uu_pathchk" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3802,7 +3662,7 @@ dependencies = [ [[package]] name = "uu_pinky" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3811,7 +3671,7 @@ dependencies = [ [[package]] name = "uu_pr" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3823,7 +3683,7 @@ dependencies = [ [[package]] name = "uu_printenv" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3832,7 +3692,7 @@ dependencies = [ [[package]] name = "uu_printf" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3841,7 +3701,7 @@ dependencies = [ [[package]] name = "uu_ptx" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3852,7 +3712,7 @@ dependencies = [ [[package]] name = "uu_pwd" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3861,7 +3721,7 @@ dependencies = [ [[package]] name = "uu_readlink" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3870,7 +3730,7 @@ dependencies = [ [[package]] name = "uu_realpath" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3879,7 +3739,7 @@ dependencies = [ [[package]] name = "uu_rm" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3894,7 +3754,7 @@ dependencies = [ [[package]] name = "uu_rmdir" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3904,7 +3764,7 @@ dependencies = [ [[package]] name = "uu_runcon" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3916,7 +3776,7 @@ dependencies = [ [[package]] name = "uu_seq" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bigdecimal", "clap", @@ -3931,7 +3791,7 @@ dependencies = [ [[package]] name = "uu_shred" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3942,18 +3802,20 @@ dependencies = [ [[package]] name = "uu_shuf" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", + "codspeed-divan-compat", "fluent", "rand 0.9.2", "rand_core 0.9.3", + "tempfile", "uucore", ] [[package]] name = "uu_sleep" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -3962,7 +3824,7 @@ dependencies = [ [[package]] name = "uu_sort" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bigdecimal", "binary-heap-plus", @@ -3986,7 +3848,7 @@ dependencies = [ [[package]] name = "uu_split" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -3999,7 +3861,7 @@ dependencies = [ [[package]] name = "uu_stat" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4009,7 +3871,7 @@ dependencies = [ [[package]] name = "uu_stdbuf" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4021,7 +3883,7 @@ dependencies = [ [[package]] name = "uu_stdbuf_libstdbuf" -version = "0.4.0" +version = "0.5.0" dependencies = [ "ctor", "libc", @@ -4029,7 +3891,7 @@ dependencies = [ [[package]] name = "uu_stty" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4039,7 +3901,7 @@ dependencies = [ [[package]] name = "uu_sum" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4048,7 +3910,7 @@ dependencies = [ [[package]] name = "uu_sync" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4059,7 +3921,7 @@ dependencies = [ [[package]] name = "uu_tac" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4072,7 +3934,7 @@ dependencies = [ [[package]] name = "uu_tail" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4088,7 +3950,7 @@ dependencies = [ [[package]] name = "uu_tee" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4098,18 +3960,19 @@ dependencies = [ [[package]] name = "uu_test" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", "libc", + "tempfile", "thiserror 2.0.17", "uucore", ] [[package]] name = "uu_timeout" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4120,7 +3983,7 @@ dependencies = [ [[package]] name = "uu_touch" -version = "0.4.0" +version = "0.5.0" dependencies = [ "chrono", "clap", @@ -4135,7 +3998,7 @@ dependencies = [ [[package]] name = "uu_tr" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bytecount", "clap", @@ -4146,7 +4009,7 @@ dependencies = [ [[package]] name = "uu_true" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4155,7 +4018,7 @@ dependencies = [ [[package]] name = "uu_truncate" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4164,7 +4027,7 @@ dependencies = [ [[package]] name = "uu_tsort" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4176,7 +4039,7 @@ dependencies = [ [[package]] name = "uu_tty" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4186,7 +4049,7 @@ dependencies = [ [[package]] name = "uu_uname" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4196,7 +4059,7 @@ dependencies = [ [[package]] name = "uu_unexpand" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4209,7 +4072,7 @@ dependencies = [ [[package]] name = "uu_uniq" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "codspeed-divan-compat", @@ -4220,7 +4083,7 @@ dependencies = [ [[package]] name = "uu_unlink" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4229,7 +4092,7 @@ dependencies = [ [[package]] name = "uu_uptime" -version = "0.4.0" +version = "0.5.0" dependencies = [ "chrono", "clap", @@ -4241,7 +4104,7 @@ dependencies = [ [[package]] name = "uu_users" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4251,7 +4114,7 @@ dependencies = [ [[package]] name = "uu_vdir" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "uu_ls", @@ -4260,7 +4123,7 @@ dependencies = [ [[package]] name = "uu_wc" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bytecount", "clap", @@ -4276,7 +4139,7 @@ dependencies = [ [[package]] name = "uu_who" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4285,7 +4148,7 @@ dependencies = [ [[package]] name = "uu_whoami" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4295,7 +4158,7 @@ dependencies = [ [[package]] name = "uu_yes" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -4306,7 +4169,7 @@ dependencies = [ [[package]] name = "uucore" -version = "0.4.0" +version = "0.5.0" dependencies = [ "base64-simd", "bigdecimal", @@ -4338,7 +4201,6 @@ dependencies = [ "memchr", "nix", "num-traits", - "number_prefix", "os_display", "phf", "procfs", @@ -4351,6 +4213,7 @@ dependencies = [ "thiserror 2.0.17", "time", "unic-langid", + "unit-prefix", "utmp-classic", "uucore_procs", "walkdir", @@ -4363,7 +4226,7 @@ dependencies = [ [[package]] name = "uucore_procs" -version = "0.4.0" +version = "0.5.0" dependencies = [ "proc-macro2", "quote", @@ -4381,7 +4244,7 @@ dependencies = [ [[package]] name = "uutests" -version = "0.4.0" +version = "0.5.0" dependencies = [ "ctor", "libc", @@ -4505,16 +4368,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "web-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "web-time" version = "1.1.0" @@ -4556,7 +4409,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4567,22 +4420,22 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", + "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -4591,21 +4444,15 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" 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-link" version = "0.2.1" @@ -4614,20 +4461,20 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -4654,7 +4501,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -4804,10 +4651,16 @@ dependencies = [ ] [[package]] -name = "writeable" -version = "0.6.1" +name = "write16" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wyz" @@ -4939,10 +4792,11 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", diff --git a/Cargo.toml b/Cargo.toml index 499eb8741..7c44e64d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -280,7 +280,6 @@ members = [ "src/uu/stdbuf/src/libstdbuf", "src/uucore", "src/uucore_procs", - "tests/benches/factor", "tests/uutests", # "fuzz", # TODO ] @@ -293,7 +292,7 @@ homepage = "https://github.com/uutils/coreutils" keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] license = "MIT" readme = "README.package.md" -version = "0.4.0" +version = "0.5.0" [workspace.dependencies] ansi-width = "0.1.0" @@ -350,7 +349,6 @@ notify = { version = "=8.2.0", features = ["macos_kqueue"] } num-bigint = "0.4.4" num-prime = "0.4.4" num-traits = "0.2.19" -number_prefix = "0.4" onig = { version = "~6.5.1", default-features = false } parse_datetime = "0.13.0" phf = "0.13.1" @@ -374,6 +372,7 @@ textwrap = { version = "0.16.1", features = ["terminal_size"] } thiserror = "2.0.3" time = { version = "0.3.36" } unicode-width = "0.2.0" +unit-prefix = "0.5" utmp-classic = "0.1.6" uutils_term_grid = "0.7" walkdir = "2.5" @@ -399,11 +398,11 @@ fluent-bundle = "0.16.0" unic-langid = "0.9.6" fluent-syntax = "0.12.0" -uucore = { version = "0.4.0", package = "uucore", path = "src/uucore" } -uucore_procs = { version = "0.4.0", package = "uucore_procs", path = "src/uucore_procs" } -uu_ls = { version = "0.4.0", path = "src/uu/ls" } -uu_base32 = { version = "0.4.0", path = "src/uu/base32" } -uutests = { version = "0.4.0", package = "uutests", path = "tests/uutests" } +uucore = { version = "0.5.0", package = "uucore", path = "src/uucore" } +uucore_procs = { version = "0.5.0", package = "uucore_procs", path = "src/uucore_procs" } +uu_ls = { version = "0.5.0", path = "src/uu/ls" } +uu_base32 = { version = "0.5.0", path = "src/uu/base32" } +uutests = { version = "0.5.0", package = "uutests", path = "tests/uutests" } [dependencies] clap.workspace = true @@ -418,109 +417,109 @@ zip = { workspace = true, optional = true } # * uutils -uu_test = { optional = true, version = "0.4.0", package = "uu_test", path = "src/uu/test" } +uu_test = { optional = true, version = "0.5.0", package = "uu_test", path = "src/uu/test" } # -arch = { optional = true, version = "0.4.0", package = "uu_arch", path = "src/uu/arch" } -base32 = { optional = true, version = "0.4.0", package = "uu_base32", path = "src/uu/base32" } -base64 = { optional = true, version = "0.4.0", package = "uu_base64", path = "src/uu/base64" } -basename = { optional = true, version = "0.4.0", package = "uu_basename", path = "src/uu/basename" } -basenc = { optional = true, version = "0.4.0", package = "uu_basenc", path = "src/uu/basenc" } -cat = { optional = true, version = "0.4.0", package = "uu_cat", path = "src/uu/cat" } -chcon = { optional = true, version = "0.4.0", package = "uu_chcon", path = "src/uu/chcon" } -chgrp = { optional = true, version = "0.4.0", package = "uu_chgrp", path = "src/uu/chgrp" } -chmod = { optional = true, version = "0.4.0", package = "uu_chmod", path = "src/uu/chmod" } -chown = { optional = true, version = "0.4.0", package = "uu_chown", path = "src/uu/chown" } -chroot = { optional = true, version = "0.4.0", package = "uu_chroot", path = "src/uu/chroot" } -cksum = { optional = true, version = "0.4.0", package = "uu_cksum", path = "src/uu/cksum" } -comm = { optional = true, version = "0.4.0", package = "uu_comm", path = "src/uu/comm" } -cp = { optional = true, version = "0.4.0", package = "uu_cp", path = "src/uu/cp" } -csplit = { optional = true, version = "0.4.0", package = "uu_csplit", path = "src/uu/csplit" } -cut = { optional = true, version = "0.4.0", package = "uu_cut", path = "src/uu/cut" } -date = { optional = true, version = "0.4.0", package = "uu_date", path = "src/uu/date" } -dd = { optional = true, version = "0.4.0", package = "uu_dd", path = "src/uu/dd" } -df = { optional = true, version = "0.4.0", package = "uu_df", path = "src/uu/df" } -dir = { optional = true, version = "0.4.0", package = "uu_dir", path = "src/uu/dir" } -dircolors = { optional = true, version = "0.4.0", package = "uu_dircolors", path = "src/uu/dircolors" } -dirname = { optional = true, version = "0.4.0", package = "uu_dirname", path = "src/uu/dirname" } -du = { optional = true, version = "0.4.0", package = "uu_du", path = "src/uu/du" } -echo = { optional = true, version = "0.4.0", package = "uu_echo", path = "src/uu/echo" } -env = { optional = true, version = "0.4.0", package = "uu_env", path = "src/uu/env" } -expand = { optional = true, version = "0.4.0", package = "uu_expand", path = "src/uu/expand" } -expr = { optional = true, version = "0.4.0", package = "uu_expr", path = "src/uu/expr" } -factor = { optional = true, version = "0.4.0", package = "uu_factor", path = "src/uu/factor" } -false = { optional = true, version = "0.4.0", package = "uu_false", path = "src/uu/false" } -fmt = { optional = true, version = "0.4.0", package = "uu_fmt", path = "src/uu/fmt" } -fold = { optional = true, version = "0.4.0", package = "uu_fold", path = "src/uu/fold" } -groups = { optional = true, version = "0.4.0", package = "uu_groups", path = "src/uu/groups" } -hashsum = { optional = true, version = "0.4.0", package = "uu_hashsum", path = "src/uu/hashsum" } -head = { optional = true, version = "0.4.0", package = "uu_head", path = "src/uu/head" } -hostid = { optional = true, version = "0.4.0", package = "uu_hostid", path = "src/uu/hostid" } -hostname = { optional = true, version = "0.4.0", package = "uu_hostname", path = "src/uu/hostname" } -id = { optional = true, version = "0.4.0", package = "uu_id", path = "src/uu/id" } -install = { optional = true, version = "0.4.0", package = "uu_install", path = "src/uu/install" } -join = { optional = true, version = "0.4.0", package = "uu_join", path = "src/uu/join" } -kill = { optional = true, version = "0.4.0", package = "uu_kill", path = "src/uu/kill" } -link = { optional = true, version = "0.4.0", package = "uu_link", path = "src/uu/link" } -ln = { optional = true, version = "0.4.0", package = "uu_ln", path = "src/uu/ln" } -ls = { optional = true, version = "0.4.0", package = "uu_ls", path = "src/uu/ls" } -logname = { optional = true, version = "0.4.0", package = "uu_logname", path = "src/uu/logname" } -mkdir = { optional = true, version = "0.4.0", package = "uu_mkdir", path = "src/uu/mkdir" } -mkfifo = { optional = true, version = "0.4.0", package = "uu_mkfifo", path = "src/uu/mkfifo" } -mknod = { optional = true, version = "0.4.0", package = "uu_mknod", path = "src/uu/mknod" } -mktemp = { optional = true, version = "0.4.0", package = "uu_mktemp", path = "src/uu/mktemp" } -more = { optional = true, version = "0.4.0", package = "uu_more", path = "src/uu/more" } -mv = { optional = true, version = "0.4.0", package = "uu_mv", path = "src/uu/mv" } -nice = { optional = true, version = "0.4.0", package = "uu_nice", path = "src/uu/nice" } -nl = { optional = true, version = "0.4.0", package = "uu_nl", path = "src/uu/nl" } -nohup = { optional = true, version = "0.4.0", package = "uu_nohup", path = "src/uu/nohup" } -nproc = { optional = true, version = "0.4.0", package = "uu_nproc", path = "src/uu/nproc" } -numfmt = { optional = true, version = "0.4.0", package = "uu_numfmt", path = "src/uu/numfmt" } -od = { optional = true, version = "0.4.0", package = "uu_od", path = "src/uu/od" } -paste = { optional = true, version = "0.4.0", package = "uu_paste", path = "src/uu/paste" } -pathchk = { optional = true, version = "0.4.0", package = "uu_pathchk", path = "src/uu/pathchk" } -pinky = { optional = true, version = "0.4.0", package = "uu_pinky", path = "src/uu/pinky" } -pr = { optional = true, version = "0.4.0", package = "uu_pr", path = "src/uu/pr" } -printenv = { optional = true, version = "0.4.0", package = "uu_printenv", path = "src/uu/printenv" } -printf = { optional = true, version = "0.4.0", package = "uu_printf", path = "src/uu/printf" } -ptx = { optional = true, version = "0.4.0", package = "uu_ptx", path = "src/uu/ptx" } -pwd = { optional = true, version = "0.4.0", package = "uu_pwd", path = "src/uu/pwd" } -readlink = { optional = true, version = "0.4.0", package = "uu_readlink", path = "src/uu/readlink" } -realpath = { optional = true, version = "0.4.0", package = "uu_realpath", path = "src/uu/realpath" } -rm = { optional = true, version = "0.4.0", package = "uu_rm", path = "src/uu/rm" } -rmdir = { optional = true, version = "0.4.0", package = "uu_rmdir", path = "src/uu/rmdir" } -runcon = { optional = true, version = "0.4.0", package = "uu_runcon", path = "src/uu/runcon" } -seq = { optional = true, version = "0.4.0", package = "uu_seq", path = "src/uu/seq" } -shred = { optional = true, version = "0.4.0", package = "uu_shred", path = "src/uu/shred" } -shuf = { optional = true, version = "0.4.0", package = "uu_shuf", path = "src/uu/shuf" } -sleep = { optional = true, version = "0.4.0", package = "uu_sleep", path = "src/uu/sleep" } -sort = { optional = true, version = "0.4.0", package = "uu_sort", path = "src/uu/sort" } -split = { optional = true, version = "0.4.0", package = "uu_split", path = "src/uu/split" } -stat = { optional = true, version = "0.4.0", package = "uu_stat", path = "src/uu/stat" } -stdbuf = { optional = true, version = "0.4.0", package = "uu_stdbuf", path = "src/uu/stdbuf" } -stty = { optional = true, version = "0.4.0", package = "uu_stty", path = "src/uu/stty" } -sum = { optional = true, version = "0.4.0", package = "uu_sum", path = "src/uu/sum" } -sync = { optional = true, version = "0.4.0", package = "uu_sync", path = "src/uu/sync" } -tac = { optional = true, version = "0.4.0", package = "uu_tac", path = "src/uu/tac" } -tail = { optional = true, version = "0.4.0", package = "uu_tail", path = "src/uu/tail" } -tee = { optional = true, version = "0.4.0", package = "uu_tee", path = "src/uu/tee" } -timeout = { optional = true, version = "0.4.0", package = "uu_timeout", path = "src/uu/timeout" } -touch = { optional = true, version = "0.4.0", package = "uu_touch", path = "src/uu/touch" } -tr = { optional = true, version = "0.4.0", package = "uu_tr", path = "src/uu/tr" } -true = { optional = true, version = "0.4.0", package = "uu_true", path = "src/uu/true" } -truncate = { optional = true, version = "0.4.0", package = "uu_truncate", path = "src/uu/truncate" } -tsort = { optional = true, version = "0.4.0", package = "uu_tsort", path = "src/uu/tsort" } -tty = { optional = true, version = "0.4.0", package = "uu_tty", path = "src/uu/tty" } -uname = { optional = true, version = "0.4.0", package = "uu_uname", path = "src/uu/uname" } -unexpand = { optional = true, version = "0.4.0", package = "uu_unexpand", path = "src/uu/unexpand" } -uniq = { optional = true, version = "0.4.0", package = "uu_uniq", path = "src/uu/uniq" } -unlink = { optional = true, version = "0.4.0", package = "uu_unlink", path = "src/uu/unlink" } -uptime = { optional = true, version = "0.4.0", package = "uu_uptime", path = "src/uu/uptime" } -users = { optional = true, version = "0.4.0", package = "uu_users", path = "src/uu/users" } -vdir = { optional = true, version = "0.4.0", package = "uu_vdir", path = "src/uu/vdir" } -wc = { optional = true, version = "0.4.0", package = "uu_wc", path = "src/uu/wc" } -who = { optional = true, version = "0.4.0", package = "uu_who", path = "src/uu/who" } -whoami = { optional = true, version = "0.4.0", package = "uu_whoami", path = "src/uu/whoami" } -yes = { optional = true, version = "0.4.0", package = "uu_yes", path = "src/uu/yes" } +arch = { optional = true, version = "0.5.0", package = "uu_arch", path = "src/uu/arch" } +base32 = { optional = true, version = "0.5.0", package = "uu_base32", path = "src/uu/base32" } +base64 = { optional = true, version = "0.5.0", package = "uu_base64", path = "src/uu/base64" } +basename = { optional = true, version = "0.5.0", package = "uu_basename", path = "src/uu/basename" } +basenc = { optional = true, version = "0.5.0", package = "uu_basenc", path = "src/uu/basenc" } +cat = { optional = true, version = "0.5.0", package = "uu_cat", path = "src/uu/cat" } +chcon = { optional = true, version = "0.5.0", package = "uu_chcon", path = "src/uu/chcon" } +chgrp = { optional = true, version = "0.5.0", package = "uu_chgrp", path = "src/uu/chgrp" } +chmod = { optional = true, version = "0.5.0", package = "uu_chmod", path = "src/uu/chmod" } +chown = { optional = true, version = "0.5.0", package = "uu_chown", path = "src/uu/chown" } +chroot = { optional = true, version = "0.5.0", package = "uu_chroot", path = "src/uu/chroot" } +cksum = { optional = true, version = "0.5.0", package = "uu_cksum", path = "src/uu/cksum" } +comm = { optional = true, version = "0.5.0", package = "uu_comm", path = "src/uu/comm" } +cp = { optional = true, version = "0.5.0", package = "uu_cp", path = "src/uu/cp" } +csplit = { optional = true, version = "0.5.0", package = "uu_csplit", path = "src/uu/csplit" } +cut = { optional = true, version = "0.5.0", package = "uu_cut", path = "src/uu/cut" } +date = { optional = true, version = "0.5.0", package = "uu_date", path = "src/uu/date" } +dd = { optional = true, version = "0.5.0", package = "uu_dd", path = "src/uu/dd" } +df = { optional = true, version = "0.5.0", package = "uu_df", path = "src/uu/df" } +dir = { optional = true, version = "0.5.0", package = "uu_dir", path = "src/uu/dir" } +dircolors = { optional = true, version = "0.5.0", package = "uu_dircolors", path = "src/uu/dircolors" } +dirname = { optional = true, version = "0.5.0", package = "uu_dirname", path = "src/uu/dirname" } +du = { optional = true, version = "0.5.0", package = "uu_du", path = "src/uu/du" } +echo = { optional = true, version = "0.5.0", package = "uu_echo", path = "src/uu/echo" } +env = { optional = true, version = "0.5.0", package = "uu_env", path = "src/uu/env" } +expand = { optional = true, version = "0.5.0", package = "uu_expand", path = "src/uu/expand" } +expr = { optional = true, version = "0.5.0", package = "uu_expr", path = "src/uu/expr" } +factor = { optional = true, version = "0.5.0", package = "uu_factor", path = "src/uu/factor" } +false = { optional = true, version = "0.5.0", package = "uu_false", path = "src/uu/false" } +fmt = { optional = true, version = "0.5.0", package = "uu_fmt", path = "src/uu/fmt" } +fold = { optional = true, version = "0.5.0", package = "uu_fold", path = "src/uu/fold" } +groups = { optional = true, version = "0.5.0", package = "uu_groups", path = "src/uu/groups" } +hashsum = { optional = true, version = "0.5.0", package = "uu_hashsum", path = "src/uu/hashsum" } +head = { optional = true, version = "0.5.0", package = "uu_head", path = "src/uu/head" } +hostid = { optional = true, version = "0.5.0", package = "uu_hostid", path = "src/uu/hostid" } +hostname = { optional = true, version = "0.5.0", package = "uu_hostname", path = "src/uu/hostname" } +id = { optional = true, version = "0.5.0", package = "uu_id", path = "src/uu/id" } +install = { optional = true, version = "0.5.0", package = "uu_install", path = "src/uu/install" } +join = { optional = true, version = "0.5.0", package = "uu_join", path = "src/uu/join" } +kill = { optional = true, version = "0.5.0", package = "uu_kill", path = "src/uu/kill" } +link = { optional = true, version = "0.5.0", package = "uu_link", path = "src/uu/link" } +ln = { optional = true, version = "0.5.0", package = "uu_ln", path = "src/uu/ln" } +ls = { optional = true, version = "0.5.0", package = "uu_ls", path = "src/uu/ls" } +logname = { optional = true, version = "0.5.0", package = "uu_logname", path = "src/uu/logname" } +mkdir = { optional = true, version = "0.5.0", package = "uu_mkdir", path = "src/uu/mkdir" } +mkfifo = { optional = true, version = "0.5.0", package = "uu_mkfifo", path = "src/uu/mkfifo" } +mknod = { optional = true, version = "0.5.0", package = "uu_mknod", path = "src/uu/mknod" } +mktemp = { optional = true, version = "0.5.0", package = "uu_mktemp", path = "src/uu/mktemp" } +more = { optional = true, version = "0.5.0", package = "uu_more", path = "src/uu/more" } +mv = { optional = true, version = "0.5.0", package = "uu_mv", path = "src/uu/mv" } +nice = { optional = true, version = "0.5.0", package = "uu_nice", path = "src/uu/nice" } +nl = { optional = true, version = "0.5.0", package = "uu_nl", path = "src/uu/nl" } +nohup = { optional = true, version = "0.5.0", package = "uu_nohup", path = "src/uu/nohup" } +nproc = { optional = true, version = "0.5.0", package = "uu_nproc", path = "src/uu/nproc" } +numfmt = { optional = true, version = "0.5.0", package = "uu_numfmt", path = "src/uu/numfmt" } +od = { optional = true, version = "0.5.0", package = "uu_od", path = "src/uu/od" } +paste = { optional = true, version = "0.5.0", package = "uu_paste", path = "src/uu/paste" } +pathchk = { optional = true, version = "0.5.0", package = "uu_pathchk", path = "src/uu/pathchk" } +pinky = { optional = true, version = "0.5.0", package = "uu_pinky", path = "src/uu/pinky" } +pr = { optional = true, version = "0.5.0", package = "uu_pr", path = "src/uu/pr" } +printenv = { optional = true, version = "0.5.0", package = "uu_printenv", path = "src/uu/printenv" } +printf = { optional = true, version = "0.5.0", package = "uu_printf", path = "src/uu/printf" } +ptx = { optional = true, version = "0.5.0", package = "uu_ptx", path = "src/uu/ptx" } +pwd = { optional = true, version = "0.5.0", package = "uu_pwd", path = "src/uu/pwd" } +readlink = { optional = true, version = "0.5.0", package = "uu_readlink", path = "src/uu/readlink" } +realpath = { optional = true, version = "0.5.0", package = "uu_realpath", path = "src/uu/realpath" } +rm = { optional = true, version = "0.5.0", package = "uu_rm", path = "src/uu/rm" } +rmdir = { optional = true, version = "0.5.0", package = "uu_rmdir", path = "src/uu/rmdir" } +runcon = { optional = true, version = "0.5.0", package = "uu_runcon", path = "src/uu/runcon" } +seq = { optional = true, version = "0.5.0", package = "uu_seq", path = "src/uu/seq" } +shred = { optional = true, version = "0.5.0", package = "uu_shred", path = "src/uu/shred" } +shuf = { optional = true, version = "0.5.0", package = "uu_shuf", path = "src/uu/shuf" } +sleep = { optional = true, version = "0.5.0", package = "uu_sleep", path = "src/uu/sleep" } +sort = { optional = true, version = "0.5.0", package = "uu_sort", path = "src/uu/sort" } +split = { optional = true, version = "0.5.0", package = "uu_split", path = "src/uu/split" } +stat = { optional = true, version = "0.5.0", package = "uu_stat", path = "src/uu/stat" } +stdbuf = { optional = true, version = "0.5.0", package = "uu_stdbuf", path = "src/uu/stdbuf" } +stty = { optional = true, version = "0.5.0", package = "uu_stty", path = "src/uu/stty" } +sum = { optional = true, version = "0.5.0", package = "uu_sum", path = "src/uu/sum" } +sync = { optional = true, version = "0.5.0", package = "uu_sync", path = "src/uu/sync" } +tac = { optional = true, version = "0.5.0", package = "uu_tac", path = "src/uu/tac" } +tail = { optional = true, version = "0.5.0", package = "uu_tail", path = "src/uu/tail" } +tee = { optional = true, version = "0.5.0", package = "uu_tee", path = "src/uu/tee" } +timeout = { optional = true, version = "0.5.0", package = "uu_timeout", path = "src/uu/timeout" } +touch = { optional = true, version = "0.5.0", package = "uu_touch", path = "src/uu/touch" } +tr = { optional = true, version = "0.5.0", package = "uu_tr", path = "src/uu/tr" } +true = { optional = true, version = "0.5.0", package = "uu_true", path = "src/uu/true" } +truncate = { optional = true, version = "0.5.0", package = "uu_truncate", path = "src/uu/truncate" } +tsort = { optional = true, version = "0.5.0", package = "uu_tsort", path = "src/uu/tsort" } +tty = { optional = true, version = "0.5.0", package = "uu_tty", path = "src/uu/tty" } +uname = { optional = true, version = "0.5.0", package = "uu_uname", path = "src/uu/uname" } +unexpand = { optional = true, version = "0.5.0", package = "uu_unexpand", path = "src/uu/unexpand" } +uniq = { optional = true, version = "0.5.0", package = "uu_uniq", path = "src/uu/uniq" } +unlink = { optional = true, version = "0.5.0", package = "uu_unlink", path = "src/uu/unlink" } +uptime = { optional = true, version = "0.5.0", package = "uu_uptime", path = "src/uu/uptime" } +users = { optional = true, version = "0.5.0", package = "uu_users", path = "src/uu/users" } +vdir = { optional = true, version = "0.5.0", package = "uu_vdir", path = "src/uu/vdir" } +wc = { optional = true, version = "0.5.0", package = "uu_wc", path = "src/uu/wc" } +who = { optional = true, version = "0.5.0", package = "uu_who", path = "src/uu/who" } +whoami = { optional = true, version = "0.5.0", package = "uu_whoami", path = "src/uu/whoami" } +yes = { optional = true, version = "0.5.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" } @@ -587,17 +586,13 @@ name = "uudoc" path = "src/bin/uudoc.rs" required-features = ["uudoc"] -# The default release profile. It contains all optimizations. -# With this profile (like in the standard release profile), -# the stack traces will still be available. +# The default release profile with some optimizations. [profile.release] lto = true +panic = "abort" -# A release-like profile that is tuned to be fast, even when being fast -# compromises on binary size. This includes aborting on panic. [profile.release-fast] inherits = "release" -panic = "abort" codegen-units = 1 # A release-like profile that is as small as possible. @@ -606,10 +601,11 @@ inherits = "release-fast" opt-level = "z" strip = true -# A release-like profile with debug info, useful for profiling. +# A release-like profile with debug info for profiling. # See https://github.com/mstange/samply . [profile.profiling] inherits = "release" +panic = "unwind" debug = true [lints] diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 912af1ca9..f9636625b 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -230,7 +230,9 @@ To run uutils against the GNU test suite locally, run the following commands: ```shell bash util/build-gnu.sh # Build uutils with release optimizations -bash util/build-gnu.sh --release-build +env PROFILE=release bash util/build-gnu.sh +# Build uutils with SELinux +env SELINUX_ENABLED=1 bash util/build-gnu.sh bash util/run-gnu-test.sh # To run a single test: bash util/run-gnu-test.sh tests/touch/not-owner.sh # for example @@ -289,7 +291,6 @@ brew install \ coreutils \ autoconf \ gettext \ - wget \ texinfo \ xz \ automake \ diff --git a/GNUmakefile b/GNUmakefile index 01b4fd08c..ceb48d2d1 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -1,15 +1,15 @@ # spell-checker:ignore (misc) testsuite runtest findstring (targets) busytest toybox distclean pkgs nextest ; (vars/env) BINDIR BUILDDIR CARGOFLAGS DESTDIR DOCSDIR INSTALLDIR INSTALLEES MULTICALL DATAROOTDIR TESTDIR manpages # Config options +ifneq (,$(filter install, $(MAKECMDGOALS))) + PROFILE?=release +endif 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 @@ -17,9 +17,9 @@ define newline endef -PROFILE_CMD := -ifeq ($(PROFILE),release) - PROFILE_CMD = --release +PROFILE_CMD := --profile=${PROFILE} +ifeq ($(PROFILE),debug) + PROFILE_CMD = endif # Binaries @@ -75,19 +75,6 @@ ifeq ($(OS),Windows_NT) endif LN ?= ln -sf -ifdef SELINUX_ENABLED - override SELINUX_ENABLED := 0 -# Now check if we should enable it (only on non-Windows) - ifneq ($(OS),Windows_NT) - 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 - # Possible programs PROGS := \ arch \ @@ -208,15 +195,11 @@ HASHSUM_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 - ifneq ($(OS),Windows_NT) - PROGS := $(PROGS) $(UNIX_PROGS) -# Build the selinux command even if not on the system - PROGS := $(PROGS) $(SELINUX_PROGS) + PROGS += $(UNIX_PROGS) +endif +ifeq ($(SELINUX_ENABLED),1) + PROGS += $(SELINUX_PROGS) endif UTILS ?= $(filter-out $(SKIP_UTILS),$(PROGS)) @@ -330,8 +313,6 @@ endif all: build -use_default := 1 - build-pkgs: ifneq (${MULTICALL}, y) ifdef BUILD_SPEC_FEATURE @@ -349,10 +330,10 @@ build: build-coreutils build-pkgs locales $(foreach test,$(UTILS),$(eval $(call TEST_BUSYBOX,$(test)))) test: - ${CARGO} test ${CARGOFLAGS} --features "$(TESTS) $(TEST_SPEC_FEATURE)" --no-default-features $(TEST_NO_FAIL_FAST) + ${CARGO} test ${CARGOFLAGS} --features "$(TESTS) $(TEST_SPEC_FEATURE)" $(PROFILE_CMD) --no-default-features $(TEST_NO_FAIL_FAST) nextest: - ${CARGO} nextest run ${CARGOFLAGS} --features "$(TESTS) $(TEST_SPEC_FEATURE)" --no-default-features $(TEST_NO_FAIL_FAST) + ${CARGO} nextest run ${CARGOFLAGS} --features "$(TESTS) $(TEST_SPEC_FEATURE)" $(PROFILE_CMD) --no-default-features $(TEST_NO_FAIL_FAST) test_toybox: -(cd $(TOYBOX_SRC)/ && make tests) @@ -360,7 +341,7 @@ test_toybox: toybox-src: if [ ! -e "$(TOYBOX_SRC)" ] ; then \ mkdir -p "$(TOYBOX_ROOT)" ; \ - wget "https://github.com/landley/toybox/archive/refs/tags/$(TOYBOX_VER).tar.gz" -P "$(TOYBOX_ROOT)" ; \ + curl -Ls "https://github.com/landley/toybox/archive/refs/tags/$(TOYBOX_VER).tar.gz" -o "$(TOYBOX_ROOT)/$(TOYBOX_VER).tar.gz" ; \ tar -C "$(TOYBOX_ROOT)" -xf "$(TOYBOX_ROOT)/$(TOYBOX_VER).tar.gz" ; \ sed -i -e "s|TESTDIR=\".*\"|TESTDIR=\"$(BUILDDIR)\"|g" $(TOYBOX_SRC)/scripts/test.sh; \ sed -i -e "s/ || exit 1//g" $(TOYBOX_SRC)/scripts/test.sh; \ @@ -369,8 +350,8 @@ toybox-src: busybox-src: if [ ! -e "$(BUSYBOX_SRC)" ] ; then \ mkdir -p "$(BUSYBOX_ROOT)" ; \ - wget "https://busybox.net/downloads/busybox-$(BUSYBOX_VER).tar.bz2" -P "$(BUSYBOX_ROOT)" ; \ - tar -C "$(BUSYBOX_ROOT)" -xf "$(BUSYBOX_ROOT)/busybox-$(BUSYBOX_VER).tar.bz2" ; \ + curl -Ls "https://github.com/mirror/busybox/archive/refs/tags/$(subst .,_,$(BUSYBOX_VER)).tar.gz" -o "$(BUSYBOX_ROOT)/busybox-$(BUSYBOX_VER).tar.gz" ; \ + tar -C "$(BUSYBOX_ROOT)" -xf "$(BUSYBOX_ROOT)/busybox-$(BUSYBOX_VER).tar.gz" ; \ fi ; # This is a busybox-specific config file their test suite wants to parse. @@ -469,7 +450,7 @@ install: build install-manpages install-completions install-locales mkdir -p $(INSTALLDIR_BIN) ifneq (,$(and $(findstring stdbuf,$(UTILS)),$(findstring feat_external_libstdbuf,$(CARGOFLAGS)))) mkdir -p $(DESTDIR)$(LIBSTDBUF_DIR) - $(INSTALL) -m 755 $(BUILDDIR)/deps/libstdbuf* $(DESTDIR)$(LIBSTDBUF_DIR)/ + $(INSTALL) -m 755 $(BUILDDIR)/deps/libstdbuf.* $(DESTDIR)$(LIBSTDBUF_DIR)/ endif ifeq (${MULTICALL}, y) $(INSTALL) -m 755 $(BUILDDIR)/coreutils $(INSTALLDIR_BIN)/$(PROG_PREFIX)coreutils @@ -492,7 +473,7 @@ endif uninstall: ifneq ($(OS),Windows_NT) - rm -f $(DESTDIR)$(LIBSTDBUF_DIR)/libstdbuf* + rm -f $(DESTDIR)$(LIBSTDBUF_DIR)/libstdbuf.* -rm -d $(DESTDIR)$(LIBSTDBUF_DIR) 2>/dev/null || true endif ifeq (${MULTICALL}, y) diff --git a/Makefile.toml b/Makefile.toml deleted file mode 100644 index 84698df5f..000000000 --- a/Makefile.toml +++ /dev/null @@ -1,386 +0,0 @@ -# spell-checker:ignore (cargo-make) duckscript - -[config] -min_version = "0.26.2" -default_to_workspace = false -init_task = "_init_task" - -[config.modify_core_tasks] -namespace = "core" - -### initialization - -### * note: the task executed from 'init_task' ignores dependencies; workaround is to run a secondary task via 'run_task' - -[tasks._init_task] -# dependencies are unavailable -# * delegate (via 'run_task') to "real" initialization task ('_init') with full capabilities -private = true -run_task = "_init" - -[tasks._init] -private = true -dependencies = ["_init-vars"] - -[tasks._init-vars] -private = true -script_runner = "@duckscript" -script = [''' -# reset build/test flags -set_env CARGO_MAKE_CARGO_BUILD_TEST_FLAGS "" -# determine features -env_features = get_env CARGO_FEATURES -if is_empty "${env_features}" - env_features = get_env FEATURES -end_if -if is_empty "${env_features}" - if eq "${CARGO_MAKE_RUST_TARGET_OS}" "macos" - features = set "unix" - else - if eq "${CARGO_MAKE_RUST_TARGET_OS}" "linux" - features = set "unix" - else - if eq "${CARGO_MAKE_RUST_TARGET_OS}" "windows" - features = set "windows" - end_if - end_if - end_if -end_if -if is_empty "${features}" - features = set "${env_features}" -else - if not is_empty "${env_features}" - features = set "${features},${env_features}" - end_if -end_if -# set build flags from features -if not is_empty "${features}" - set_env CARGO_MAKE_VAR_BUILD_TEST_FEATURES "${features}" - set_env CARGO_MAKE_CARGO_BUILD_TEST_FLAGS "--features ${features}" -end_if -# determine show-utils helper script -show_utils = set "util/show-utils.sh" -if eq "${CARGO_MAKE_RUST_TARGET_OS}" "windows" - show_utils = set "util/show-utils.BAT" -end_if -set_env CARGO_MAKE_VAR_SHOW_UTILS "${show_utils}" -# rebuild CARGO_MAKE_TASK_ARGS for various targets -args = set ${CARGO_MAKE_TASK_ARGS} -# * rebuild for 'features' target -args_features = replace ${args} ";" "," -set_env CARGO_MAKE_TASK_BUILD_FEATURES_ARGS "${args_features}" -# * rebuild for 'examples' target -args_examples = replace ${args} ";" " --example " -if is_empty "${args_examples}" - args_examples = set "--examples" -end_if -set_env CARGO_MAKE_TASK_BUILD_EXAMPLES_ARGS "${args_examples}" -# * rebuild for 'utils' target -args_utils_list = split "${args}" ";" -for arg in "${args_utils_list}" - if not is_empty "${arg}" - if not starts_with "${arg}" "uu_" - arg = set "uu_${arg}" - end_if - args_utils = set "${args_utils} -p${arg}" - end_if -end -args_utils = trim "${args_utils}" -set_env CARGO_MAKE_TASK_BUILD_UTILS_ARGS "${args_utils}" -'''] - -### tasks - -[tasks.default] -description = "## *DEFAULT* Build (debug-mode) and test project" -category = "[project]" -dependencies = ["action-build-debug", "test-terse"] - -## - -[tasks.build] -description = "## Build (release-mode) project" -category = "[project]" -dependencies = ["core::pre-build", "action-build-release", "core::post-build"] - -[tasks.build-debug] -description = "## Build (debug-mode) project" -category = "[project]" -dependencies = ["action-build-debug"] - -[tasks.build-examples] -description = "## Build (release-mode) project example(s); usage: `cargo make (build-examples | examples) [EXAMPLE]...`" -category = "[project]" -dependencies = ["core::pre-build", "action-build-examples", "core::post-build"] - -[tasks.build-features] -description = "## Build (with features; release-mode) project; usage: `cargo make (build-features | features) FEATURE...`" -category = "[project]" -dependencies = ["core::pre-build", "action-build-features", "core::post-build"] - -[tasks.build-release] -alias = "build" - -[tasks.debug] -alias = "build-debug" - -[tasks.example] -description = "hidden singular-form alias for 'examples'" -category = "[project]" -dependencies = ["examples"] - -[tasks.examples] -alias = "build-examples" - -[tasks.features] -alias = "build-features" - -[tasks.format] -description = "## Format code files (with `cargo fmt`; includes tests)" -category = "[project]" -dependencies = ["action-format", "action-format-tests"] - -[tasks.help] -description = "## Display help" -category = "[project]" -dependencies = ["action-display-help"] - -[tasks.install] -description = "## Install project binary (to $HOME/.cargo/bin)" -category = "[project]" -command = "cargo" -args = ["install", "--path", "."] - -[tasks.lint] -description = "## Display lint report" -category = "[project]" -dependencies = ["action-clippy", "action-fmt_report"] - -[tasks.release] -alias = "build" - -[tasks.test] -description = "## Run project tests" -category = "[project]" -dependencies = ["core::pre-test", "core::test", "core::post-test"] - -[tasks.test-terse] -description = "## Run project tests (with terse/summary output)" -category = "[project]" -dependencies = ["core::pre-test", "action-test_quiet", "core::post-test"] - -[tasks.test-util] -description = "## Test (individual) utilities; usage: `cargo make (test-util | test-uutil) [UTIL_NAME...]`" -category = "[project]" -dependencies = ["action-test-utils"] - -[tasks.test-utils] -description = "hidden plural-form alias for 'test-util'" -category = "[project]" -dependencies = ["test-util"] - -[tasks.test-uutil] -description = "hidden alias for 'test-util'" -category = "[project]" -dependencies = ["test-util"] - -[tasks.test-uutils] -description = "hidden alias for 'test-util'" -category = "[project]" -dependencies = ["test-util"] - -[tasks.uninstall] -description = "## Remove project binary (from $HOME/.cargo/bin)" -category = "[project]" -command = "cargo" -args = ["uninstall"] - -[tasks.util] -description = "## Build (individual; release-mode) utilities; usage: `cargo make (util | uutil) [UTIL_NAME...]`" -category = "[project]" -dependencies = [ - "core::pre-build", - "action-determine-utils", - "action-build-utils", - "core::post-build", -] - -[tasks.utils] -description = "hidden plural-form alias for 'util'" -category = "[project]" -dependencies = ["util"] - -[tasks.uutil] -description = "hidden alias for 'util'" -category = "[project]" -dependencies = ["util"] - -[tasks.uutils] -description = "hidden plural-form alias for 'util'" -category = "[project]" -dependencies = ["util"] - -### actions - -[tasks.action-build-release] -description = "`cargo build --release`" -command = "cargo" -args = ["build", "--release", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )"] - -[tasks.action-build-debug] -description = "`cargo build`" -command = "cargo" -args = ["build", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )"] - -[tasks.action-build-examples] -description = "`cargo build (--examples|(--example EXAMPLE)...)`" -command = "cargo" -args = [ - "build", - "--release", - "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )", - "${CARGO_MAKE_TASK_BUILD_EXAMPLES_ARGS}", -] - -[tasks.action-build-features] -description = "`cargo build --release --features FEATURES`" -command = "cargo" -args = [ - "build", - "--release", - "--no-default-features", - "--features", - "${CARGO_MAKE_TASK_BUILD_FEATURES_ARGS}", -] - -[tasks.action-build-utils] -description = "Build individual utilities" -dependencies = ["action-determine-utils"] -command = "cargo" -# args = ["build", "@@remove-empty(CARGO_MAKE_TASK_BUILD_UTILS_ARGS)" ] -args = ["build", "--release", "@@split(CARGO_MAKE_TASK_BUILD_UTILS_ARGS, )"] - -[tasks.action-clippy] -description = "`cargo clippy` lint report" -command = "cargo" -args = ["clippy", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )"] - -[tasks.action-determine-utils] -script_runner = "@duckscript" -script = [''' -package_options = get_env CARGO_MAKE_TASK_BUILD_UTILS_ARGS -if is_empty "${package_options}" - show_utils = get_env CARGO_MAKE_VAR_SHOW_UTILS - features = get_env CARGO_MAKE_VAR_BUILD_TEST_FEATURES - if not is_empty "${features}" - result = exec "${show_utils}" --features "${features}" - else - result = exec "${show_utils}" - endif - set_env CARGO_MAKE_VAR_UTILS ${result.stdout} - utils = array %{result.stdout} - for util in ${utils} - if not is_empty "${util}" - if not starts_with "${util}" "uu_" - util = set "uu_${util}" - end_if - package_options = set "${package_options} -p${util}" - end_if - end - package_options = trim "${package_options}" -end_if -set_env CARGO_MAKE_TASK_BUILD_UTILS_ARGS "${package_options}" -'''] - -[tasks.action-determine-tests] -script_runner = "@duckscript" -script = [''' -test_files = glob_array tests/**/*.rs -for file in ${test_files} - file = replace "${file}" "\\" "/" - if not is_empty ${file} - if is_empty "${tests}" - tests = set "${file}" - else - tests = set "${tests} ${file}" - end_if - end_if -end -set_env CARGO_MAKE_VAR_TESTS "${tests}" -'''] - -[tasks.action-format] -description = "`cargo fmt`" -command = "cargo" -args = ["fmt"] - -[tasks.action-format-tests] -description = "`cargo fmt` tests" -dependencies = ["action-determine-tests"] -command = "cargo" -args = ["fmt", "--", "@@split(CARGO_MAKE_VAR_TESTS, )"] - -[tasks.action-fmt] -alias = "action-format" - -[tasks.action-fmt_report] -description = "`cargo fmt` lint report" -command = "cargo" -args = ["fmt", "--", "--check"] - -[tasks.action-spellcheck-codespell] -description = "`codespell` spellcheck repository" -command = "codespell" # (from `pip install codespell`) -args = [ - ".", - "--skip=*/.git,./target,./tests/fixtures", - "--ignore-words-list=mut,od", -] - -[tasks.action-test-utils] -description = "Build individual utilities" -dependencies = ["action-determine-utils"] -command = "cargo" -# args = ["build", "@@remove-empty(CARGO_MAKE_TASK_BUILD_UTILS_ARGS)" ] -args = ["test", "@@split(CARGO_MAKE_TASK_BUILD_UTILS_ARGS, )"] - -[tasks.action-test_quiet] -description = "Test (in `--quiet` mode)" -command = "cargo" -args = ["test", "--quiet", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )"] - -[tasks.action-display-help] -script_runner = "@duckscript" -script = [''' - echo "" - echo "usage: `cargo make TARGET [ARGS...]`" - echo "" - echo "TARGETs:" - echo "" - result = exec "cargo" make --list-all-steps - # set_env CARGO_MAKE_VAR_UTILS ${result.stdout} - # echo ${result.stdout} - lines = split ${result.stdout} "\n" - # echo ${lines} - for line in ${lines} - if not is_empty ${line} - if contains ${line} " - ##" - line_segments = split ${line} " - ##" - desc = array_pop ${line_segments} - desc = trim ${desc} - target = array_pop ${line_segments} - target = trim ${target} - l = length ${target} - r = range 0 18 - spacing = set "" - for i in ${r} - if greater_than ${i} ${l} - spacing = set "${spacing} " - end_if - end - echo ${target}${spacing}${desc} - end_if - end_if - end - echo "" -'''] diff --git a/README.md b/README.md index bcd32f75d..3ed607d42 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,8 @@ other Rust program: cargo build --release ``` +Replace `--release` with `--profile=release-fast` or `--profile=release-small` to use all optimizations or save binary size. + This command builds the most portable common core set of uutils into a multicall (BusyBox-type) binary, named 'coreutils', on most Rust-supported platforms. @@ -150,16 +152,16 @@ cargo build -p uu_base32 -p uu_cat -p uu_echo -p uu_rm Building using `make` is a simple process as well. -To simply build all available utilities: +To simply build all available utilities (with debug profile): ```shell make ``` -In release mode: +In release-fast mode: ```shell -make PROFILE=release +make PROFILE=release-fast ``` To build all but a few of the available utilities: @@ -199,6 +201,12 @@ To install all available utilities: make install ``` +To install all utilities with all possible optimizations: + +```shell +make PROFILE=release-fast install +``` + To install using `sudo` switch `-E` must be used: ```shell @@ -220,9 +228,11 @@ make UTILS='UTILITY_1 UTILITY_2' install To install every program with a prefix (e.g. uu-echo uu-cat): ```shell -make PROG_PREFIX=PREFIX_GOES_HERE install +make PROG_PREFIX=uu- install ``` +`PROG_PREFIX` requires separator `-`, `_`, or `=`. + To install the multicall binary: ```shell @@ -312,7 +322,7 @@ make uninstall To uninstall every program with a set prefix: ```shell -make PROG_PREFIX=PREFIX_GOES_HERE uninstall +make PROG_PREFIX=uu- uninstall ``` To uninstall the multicall binary: diff --git a/deny.toml b/deny.toml index 9906813b1..662474b65 100644 --- a/deny.toml +++ b/deny.toml @@ -59,8 +59,6 @@ skip = [ { name = "windows-sys", version = "0.59.0" }, # various crates { name = "windows-sys", version = "0.60.2" }, - # various crates - { name = "windows-link", version = "0.1.3" }, # parking_lot_core { name = "windows-targets", version = "0.52.6" }, # windows-targets diff --git a/docs/src/installation.md b/docs/src/installation.md index 856ca9d22..8ff0f004e 100644 --- a/docs/src/installation.md +++ b/docs/src/installation.md @@ -1,4 +1,4 @@ - + # Installation @@ -122,6 +122,12 @@ apt install rust-coreutils export PATH=/usr/lib/cargo/bin/coreutils:$PATH ``` +### AUR + +[AUR package](https://aur.archlinux.org/packages/uutils-coreutils-git) + +Rust rewrite of the GNU coreutils (main branch). + ## MacOS ### Homebrew @@ -164,6 +170,12 @@ winget install uutils.coreutils scoop install uutils-coreutils ``` +### MSYS2 + +[MSYS2 package (Windows native)](https://packages.msys2.org/base/mingw-w64-uutils-coreutils) + +[MSYS2 package (Cygwin)](https://packages.msys2.org/base/uutils-coreutils) + ## Alternative installers ### Conda @@ -184,11 +196,3 @@ Clone [poky](https://github.com/yoctoproject/poky) and [meta-openembedded](https and then either call `bitbake uutils-coreutils`, or use `PREFERRED_PROVIDER_coreutils = "uutils-coreutils"` in your `build/conf/local.conf` file and then build your usual yocto image. - -## Non-standard packages - -### `coreutils-uutils` (AUR) - -[AUR package](https://aur.archlinux.org/packages/coreutils-uutils) - -Cross-platform Rust rewrite of the GNU coreutils being used as actual system coreutils. diff --git a/docs/src/platforms.md b/docs/src/platforms.md index b84516e3f..12f08d709 100644 --- a/docs/src/platforms.md +++ b/docs/src/platforms.md @@ -1,6 +1,6 @@ # Platform support - + uutils aims to be as "universal" as possible, meaning that we try to support many platforms. However, it is infeasible for us to guarantee that every @@ -19,13 +19,13 @@ platform support, with different guarantees. We support two tiers of platforms: The platforms in tier 1 and the platforms that we test in CI are listed below. -| Operating system | Tested targets | -| ---------------- | -------------- | +| Operating system | Tested targets | +| ---------------- | ------------------------ | | **Linux** | `x86_64-unknown-linux-gnu`
`x86_64-unknown-linux-musl`
`arm-unknown-linux-gnueabihf`
`i686-unknown-linux-gnu`
`aarch64-unknown-linux-gnu` | -| **macOS** | `x86_64-apple-darwin` | +| **macOS** | `x86_64-apple-darwin` | | **Windows** | `i686-pc-windows-msvc`
`x86_64-pc-windows-gnu`
`x86_64-pc-windows-msvc` | | **FreeBSD** | `x86_64-unknown-freebsd` | -| **Android** | `i686-linux-android` | +| **Android** | `i686-linux-android` | The platforms in tier 2 are more vague, but include: diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 63c3c1209..ccb71eaff 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -8,15 +8,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -58,22 +49,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -205,9 +196,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "cc" -version = "1.2.44" +version = "1.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ "find-msvc-tools", "jobserver", @@ -240,18 +231,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -334,9 +325,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -349,15 +340,14 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc-fast" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3" +checksum = "a2f7c8d397a6353ef0c1d6217ab91b3ddb5431daf57fd013f506b967dcf44458" dependencies = [ "crc", "digest", - "rand", - "regex", "rustversion", + "spin", ] [[package]] @@ -402,9 +392,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -525,9 +515,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "flate2" @@ -592,9 +582,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -834,24 +824,24 @@ dependencies = [ [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" dependencies = [ "jiff-static", "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", - "serde", - "windows-sys 0.59.0", + "serde_core", + "windows-sys 0.61.2", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", @@ -885,9 +875,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -904,9 +894,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libfuzzer-sys" @@ -938,9 +928,9 @@ checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "md-5" @@ -1017,12 +1007,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "objc2" version = "0.6.3" @@ -1099,9 +1083,9 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "parse_datetime" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77d45119ed61100f40b2389d8ed12e51ec869046d4279afbb5a7c73a4733be36" +checksum = "acea383beda9652270f3c9678d83aa58cbfc16880343cae0c0c8c7d6c0974132" dependencies = [ "jiff", "num-traits", @@ -1203,9 +1187,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -1265,34 +1249,11 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - [[package]] name = "regex-automata" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rust-ini" @@ -1436,6 +1397,12 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1450,9 +1417,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.108" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -1578,6 +1545,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "utf16_iter" version = "1.0.5" @@ -1598,7 +1571,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uu_cksum" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -1608,7 +1581,7 @@ dependencies = [ [[package]] name = "uu_cut" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bstr", "clap", @@ -1619,7 +1592,7 @@ dependencies = [ [[package]] name = "uu_date" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -1632,7 +1605,7 @@ dependencies = [ [[package]] name = "uu_echo" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -1641,7 +1614,7 @@ dependencies = [ [[package]] name = "uu_env" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -1653,7 +1626,7 @@ dependencies = [ [[package]] name = "uu_expr" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -1666,7 +1639,7 @@ dependencies = [ [[package]] name = "uu_printf" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -1675,7 +1648,7 @@ dependencies = [ [[package]] name = "uu_seq" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bigdecimal", "clap", @@ -1688,7 +1661,7 @@ dependencies = [ [[package]] name = "uu_sort" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bigdecimal", "binary-heap-plus", @@ -1711,7 +1684,7 @@ dependencies = [ [[package]] name = "uu_split" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -1722,7 +1695,7 @@ dependencies = [ [[package]] name = "uu_test" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "fluent", @@ -1733,7 +1706,7 @@ dependencies = [ [[package]] name = "uu_tr" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bytecount", "clap", @@ -1744,7 +1717,7 @@ dependencies = [ [[package]] name = "uu_wc" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bytecount", "clap", @@ -1758,13 +1731,12 @@ dependencies = [ [[package]] name = "uucore" -version = "0.4.0" +version = "0.5.0" dependencies = [ "base64-simd", "bigdecimal", "blake2b_simd", "blake3", - "bstr", "clap", "crc-fast", "data-encoding", @@ -1784,7 +1756,6 @@ dependencies = [ "memchr", "nix", "num-traits", - "number_prefix", "os_display", "phf", "procfs", @@ -1794,6 +1765,7 @@ dependencies = [ "sm3", "thiserror", "unic-langid", + "unit-prefix", "uucore_procs", "wild", "winapi-util", @@ -1826,7 +1798,7 @@ dependencies = [ [[package]] name = "uucore_procs" -version = "0.4.0" +version = "0.5.0" dependencies = [ "proc-macro2", "quote", @@ -1834,7 +1806,7 @@ dependencies = [ [[package]] name = "uufuzz" -version = "0.4.0" +version = "0.5.0" dependencies = [ "console", "libc", @@ -1873,9 +1845,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -1886,9 +1858,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1896,9 +1868,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", @@ -1909,9 +1881,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -1993,22 +1965,13 @@ 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.5", + "windows-targets", ] [[package]] @@ -2020,22 +1983,6 @@ dependencies = [ "windows-link", ] -[[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.5" @@ -2043,106 +1990,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" -[[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[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.1" @@ -2151,9 +2050,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -2207,18 +2106,18 @@ checksum = "9b3a41ce106832b4da1c065baa4c31cf640cf965fa1483816402b7f6b96f0a64" [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", diff --git a/fuzz/uufuzz/Cargo.toml b/fuzz/uufuzz/Cargo.toml index 2a5abeee4..c68bcb428 100644 --- a/fuzz/uufuzz/Cargo.toml +++ b/fuzz/uufuzz/Cargo.toml @@ -3,7 +3,7 @@ name = "uufuzz" authors = ["uutils developers"] description = "uutils ~ 'core' uutils fuzzing library" repository = "https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz" -version = "0.4.0" +version = "0.5.0" edition.workspace = true license.workspace = true @@ -12,5 +12,5 @@ console = "0.16.0" libc = "0.2.153" rand = { version = "0.9.0", features = ["small_rng"] } similar = "2.5.0" -uucore = { version = "0.4.0", path = "../../src/uucore", features = ["parser"] } +uucore = { version = "0.5.0", path = "../../src/uucore", features = ["parser"] } tempfile = "3.15.0" diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index cadc70472..e61d56c8c 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -66,6 +66,7 @@ fn gen_manpage( args: impl Iterator, util_map: &UtilityMap, ) -> ! { + uucore::set_utility_is_second_arg(); let all_utilities = validation::get_all_utilities(util_map); let matches = Command::new("manpage") diff --git a/src/common/validation.rs b/src/common/validation.rs index 1715ad4bf..057e8a912 100644 --- a/src/common/validation.rs +++ b/src/common/validation.rs @@ -25,7 +25,7 @@ pub fn get_all_utilities( /// Prints a "utility not found" error and exits pub fn not_found(util: &OsStr) -> ! { - println!("{}: function/utility not found", util.maybe_quote()); + eprintln!("{}: function/utility not found", util.maybe_quote()); process::exit(1); } @@ -51,9 +51,9 @@ fn get_canonical_util_name(util_name: &str) -> &str { "[" => "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", + "md5sum" | "sha1sum" | "sha224sum" | "sha256sum" | "sha384sum" | "sha512sum" | "b2sum" => { + "hashsum" + } "dir" => "ls", // dir is an alias for ls diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index 96d28e189..65cadc7c3 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -8,11 +8,11 @@ use clap::{Arg, ArgAction, Command}; use std::ffi::OsString; use std::fs::File; -use std::io::{self, ErrorKind, Read, Seek}; +use std::io::{self, ErrorKind, Read, Seek, Write}; use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::encoding::{ - BASE2LSBF, BASE2MSBF, Base58Wrapper, Base64SimdWrapper, EncodingWrapper, Format, + BASE2LSBF, BASE2MSBF, Base32Wrapper, Base58Wrapper, Base64SimdWrapper, EncodingWrapper, Format, SupportsFastDecodeAndEncode, Z85Wrapper, for_base_common::{BASE32, BASE32HEX, BASE64URL, HEXUPPER_PERMISSIVE}, }; @@ -193,7 +193,7 @@ pub fn handle_input(input: &mut R, format: Format, config: Confi let supports_fast_decode_and_encode_ref = supports_fast_decode_and_encode.as_ref(); let mut stdout_lock = io::stdout().lock(); - if config.decode { + let result = if config.decode { fast_decode::fast_decode( read, &mut stdout_lock, @@ -207,6 +207,14 @@ pub fn handle_input(input: &mut R, format: Format, config: Confi supports_fast_decode_and_encode_ref, config.wrap_cols, ) + }; + + // Ensure any pending stdout buffer is flushed even if decoding failed; GNU basenc + // keeps already-decoded bytes visible before reporting the error. + match (result, stdout_lock.flush()) { + (res, Ok(())) => res, + (Ok(_), Err(err)) => Err(err.into()), + (Err(original), Err(_)) => Err(original), } } @@ -247,14 +255,14 @@ pub fn get_supports_fast_decode_and_encode( // spell-checker:disable-next-line b"01", )), - Format::Base32 => Box::from(EncodingWrapper::new( + Format::Base32 => Box::from(Base32Wrapper::new( BASE32, BASE32_VALID_DECODING_MULTIPLE, BASE32_UNPADDED_MULTIPLE, // spell-checker:disable-next-line b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=", )), - Format::Base32Hex => Box::from(EncodingWrapper::new( + Format::Base32Hex => Box::from(Base32Wrapper::new( BASE32HEX, BASE32_VALID_DECODING_MULTIPLE, BASE32_UNPADDED_MULTIPLE, @@ -502,43 +510,21 @@ pub mod fast_encode { pub mod fast_decode { use std::io::{self, Write}; - use uucore::{encoding::SupportsFastDecodeAndEncode, error::UResult}; + 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]; + fn alphabet_lookup(alphabet: &[u8]) -> [bool; 256] { + // Precompute O(1) membership checks so we can validate every byte before decoding. + 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 + for &byte in alphabet { + table[usize::from(byte)] = true; } + + table } fn decode_in_chunks_to_buffer( @@ -553,11 +539,44 @@ pub mod fast_decode { 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())?; + output.flush()?; decoded_buffer.clear(); Ok(()) } + + fn flush_ready_chunks( + buffer: &mut Vec, + block_limit: usize, + valid_multiple: usize, + supports_fast_decode_and_encode: &dyn SupportsFastDecodeAndEncode, + decoded_buffer: &mut Vec, + output: &mut dyn Write, + ) -> UResult<()> { + // While at least one full decode block is buffered, keep draining + // it and never yield more than block_limit per chunk. + while buffer.len() >= valid_multiple { + let take = buffer.len().min(block_limit); + let aligned_take = take - (take % valid_multiple); + + if aligned_take < valid_multiple { + break; + } + + decode_in_chunks_to_buffer( + supports_fast_decode_and_encode, + &buffer[..aligned_take], + decoded_buffer, + )?; + + write_to_output(decoded_buffer, output)?; + + buffer.drain(..aligned_take); + } + + Ok(()) + } // End of helper functions pub fn fast_decode( @@ -569,22 +588,12 @@ pub mod fast_decode { 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; + let alphabet_table = alphabet_lookup(alphabet); + let valid_multiple = supports_fast_decode_and_encode.valid_decoding_multiple(); + let decode_in_chunks_of_size = valid_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); + assert!(valid_multiple > 0); // Start of buffers @@ -595,35 +604,69 @@ pub mod fast_decode { let mut buffer = Vec::with_capacity(decode_in_chunks_of_size); - input - .iter() - .filter(|ch| table[usize::from(**ch)]) - .for_each(|ch| { - buffer.push(*ch); - // How many bytes to steal from `read_buffer` to get - // `leftover_buffer` to the right size - if buffer.len() == decode_in_chunks_of_size { - assert_eq!(decode_in_chunks_of_size, buffer.len()); - // Decode data in chunks, then place it in `decoded_buffer` - decode_in_chunks_to_buffer( - supports_fast_decode_and_encode, - &buffer, - &mut decoded_buffer, - ) - .unwrap(); - // Write all data in `decoded_buffer` to `output` - write_to_output(&mut decoded_buffer, output).unwrap(); - buffer.clear(); - } - }); - // 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(&buffer, &mut decoded_buffer)?; + let supports_partial_decode = supports_fast_decode_and_encode.supports_partial_decode(); - // Write all data in `decoded_buffer` to `output` + for &byte in &input { + if byte == b'\n' || byte == b'\r' { + continue; + } + + if alphabet_table[usize::from(byte)] { + buffer.push(byte); + } else if ignore_garbage { + continue; + } else { + return Err(USimpleError::new(1, "error: invalid input".to_owned())); + } + + if supports_partial_decode { + flush_ready_chunks( + &mut buffer, + decode_in_chunks_of_size, + valid_multiple, + supports_fast_decode_and_encode, + &mut decoded_buffer, + output, + )?; + } else if buffer.len() == decode_in_chunks_of_size { + decode_in_chunks_to_buffer( + supports_fast_decode_and_encode, + &buffer, + &mut decoded_buffer, + )?; + write_to_output(&mut decoded_buffer, output)?; + buffer.clear(); + } + } + + if supports_partial_decode { + flush_ready_chunks( + &mut buffer, + decode_in_chunks_of_size, + valid_multiple, + supports_fast_decode_and_encode, + &mut decoded_buffer, + output, + )?; + } + + if !buffer.is_empty() { + let mut owned_chunk: Option> = None; + let mut had_invalid_tail = false; + + if let Some(pad_result) = supports_fast_decode_and_encode.pad_remainder(&buffer) { + had_invalid_tail = pad_result.had_invalid_tail; + owned_chunk = Some(pad_result.chunk); + } + + let final_chunk = owned_chunk.as_deref().unwrap_or(&buffer); + + supports_fast_decode_and_encode.decode_into_vec(final_chunk, &mut decoded_buffer)?; write_to_output(&mut decoded_buffer, output)?; + + if had_invalid_tail { + return Err(USimpleError::new(1, "error: invalid input".to_owned())); + } } Ok(()) diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index c782ad429..15b608af6 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -522,9 +522,24 @@ impl Chmoder { .safe_chmod_file(&entry_path, dir_fd, &entry_name, meta.mode() & 0o7777) .and(r); - // Recurse into subdirectories + // Recurse into subdirectories using the existing directory fd if meta.is_dir() { - r = self.walk_dir_with_context(&entry_path, false).and(r); + match dir_fd.open_subdir(&entry_name) { + Ok(child_dir_fd) => { + r = self.safe_traverse_dir(&child_dir_fd, &entry_path).and(r); + } + Err(err) => { + let error = if err.kind() == std::io::ErrorKind::PermissionDenied { + ChmodError::PermissionDenied( + entry_path.to_string_lossy().to_string(), + ) + .into() + } else { + err.into() + }; + r = r.and(Err(error)); + } + } } } } diff --git a/src/uu/cksum/Cargo.toml b/src/uu/cksum/Cargo.toml index 01ca5cb16..7e62c5c8f 100644 --- a/src/uu/cksum/Cargo.toml +++ b/src/uu/cksum/Cargo.toml @@ -19,7 +19,12 @@ path = "src/cksum.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["checksum", "encoding", "sum"] } +uucore = { workspace = true, features = [ + "checksum", + "encoding", + "sum", + "hardware", +] } hex = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/cksum/locales/en-US.ftl b/src/uu/cksum/locales/en-US.ftl index 0506d1bbe..834cd77b0 100644 --- a/src/uu/cksum/locales/en-US.ftl +++ b/src/uu/cksum/locales/en-US.ftl @@ -27,7 +27,4 @@ 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 +cksum-help-debug = print CPU hardware capability detection info used by cksum diff --git a/src/uu/cksum/locales/fr-FR.ftl b/src/uu/cksum/locales/fr-FR.ftl index 1a045dddb..01136f606 100644 --- a/src/uu/cksum/locales/fr-FR.ftl +++ b/src/uu/cksum/locales/fr-FR.ftl @@ -27,7 +27,4 @@ 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 +cksum-help-debug = afficher les informations de débogage sur la détection de la prise en charge matérielle du processeur diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index c7a3e969b..dd75dcdee 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -3,269 +3,49 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) fname, algo +// spell-checker:ignore (ToDO) fname, algo, bitlen use clap::builder::ValueParser; use clap::{Arg, ArgAction, Command}; use std::ffi::{OsStr, OsString}; -use std::fs::File; -use std::io::{BufReader, Read, Write, stdin, stdout}; use std::iter; -use std::path::Path; +use uucore::checksum::compute::{ + ChecksumComputeOptions, figure_out_output_format, perform_checksum_computation, +}; +use uucore::checksum::validate::{ + ChecksumValidateOptions, ChecksumVerbose, perform_checksum_validation, +}; use uucore::checksum::{ - ALGORITHM_OPTIONS_BLAKE2B, ALGORITHM_OPTIONS_BSD, ALGORITHM_OPTIONS_CRC, - ALGORITHM_OPTIONS_CRC32B, ALGORITHM_OPTIONS_SHA2, ALGORITHM_OPTIONS_SHA3, - ALGORITHM_OPTIONS_SYSV, ChecksumError, ChecksumOptions, ChecksumVerbose, HashAlgorithm, - LEGACY_ALGORITHMS, SUPPORTED_ALGORITHMS, calculate_blake2b_length_str, detect_algo, - digest_reader, perform_checksum_validation, sanitize_sha2_sha3_length_str, + AlgoKind, ChecksumError, SUPPORTED_ALGORITHMS, SizedAlgoKind, calculate_blake2b_length_str, + sanitize_sha2_sha3_length_str, }; -use uucore::translate; +use uucore::error::UResult; +use uucore::hardware::CpuFeatures; +use uucore::line_ending::LineEnding; +use uucore::{format_usage, translate}; -use uucore::{ - encoding, - error::{FromIo, UResult, USimpleError}, - format_usage, - line_ending::LineEnding, - os_str_as_bytes, show, - sum::Digest, -}; +/// Print CPU hardware capability detection information to stderr +/// This matches GNU cksum's --debug behavior +fn print_cpu_debug_info() { + let features = CpuFeatures::detect(); -struct Options { - algo_name: &'static str, - digest: Box, - output_bits: usize, - length: Option, - output_format: OutputFormat, - line_ending: LineEnding, -} - -/// Reading mode used to compute digest. -/// -/// On most linux systems, this is irrelevant, as there is no distinction -/// between text and binary files. Refer to GNU's cksum documentation for more -/// information. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ReadingMode { - Binary, - Text, -} - -impl ReadingMode { - #[inline] - fn as_char(&self) -> char { - match self { - Self::Binary => '*', - Self::Text => ' ', - } - } -} - -/// Whether to write the digest as hexadecimal or encoded in base64. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum DigestFormat { - Hexadecimal, - Base64, -} - -impl DigestFormat { - #[inline] - fn is_base64(&self) -> bool { - *self == Self::Base64 - } -} - -/// Holds the representation that shall be used for printing a checksum line -#[derive(Debug, PartialEq, Eq)] -enum OutputFormat { - /// Raw digest - Raw, - - /// Selected for older algorithms which had their custom formatting - /// - /// Default for crc, sysv, bsd - Legacy, - - /// `$ALGO_NAME ($FILENAME) = $DIGEST` - Tagged(DigestFormat), - - /// '$DIGEST $FLAG$FILENAME' - /// where 'flag' depends on the reading mode - /// - /// Default for standalone checksum utilities - Untagged(DigestFormat, ReadingMode), -} - -impl OutputFormat { - #[inline] - fn is_raw(&self) -> bool { - *self == Self::Raw - } -} - -fn print_legacy_checksum( - options: &Options, - filename: &OsStr, - sum: &str, - size: usize, -) -> UResult<()> { - debug_assert!(LEGACY_ALGORITHMS.contains(&options.algo_name)); - - // Print the sum - match options.algo_name { - ALGORITHM_OPTIONS_SYSV => print!( - "{} {}", - sum.parse::().unwrap(), - size.div_ceil(options.output_bits), - ), - ALGORITHM_OPTIONS_BSD => { - // The BSD checksum output is 5 digit integer - let bsd_width = 5; - print!( - "{:0bsd_width$} {:bsd_width$}", - sum.parse::().unwrap(), - size.div_ceil(options.output_bits), - ); - } - ALGORITHM_OPTIONS_CRC | ALGORITHM_OPTIONS_CRC32B => { - print!("{sum} {size}"); - } - _ => unreachable!("Not a legacy algorithm"), - } - - // Print the filename after a space if not stdin - if filename != "-" { - print!(" "); - let _dropped_result = stdout().write_all(os_str_as_bytes(filename)?); - } - - Ok(()) -} - -fn print_tagged_checksum(options: &Options, filename: &OsStr, sum: &String) -> UResult<()> { - // Print algo name and opening parenthesis. - print!( - "{} (", - match (options.algo_name, options.length) { - // Multiply the length by 8, as we want to print the length in bits. - (ALGORITHM_OPTIONS_BLAKE2B, Some(l)) => format!("BLAKE2b-{}", l * 8), - (ALGORITHM_OPTIONS_BLAKE2B, None) => "BLAKE2b".into(), - (name, _) => name.to_ascii_uppercase(), - } - ); - - // Print filename - let _dropped_result = stdout().write_all(os_str_as_bytes(filename)?); - - // Print closing parenthesis and sum - print!(") = {sum}"); - - Ok(()) -} - -fn print_untagged_checksum( - filename: &OsStr, - sum: &String, - reading_mode: ReadingMode, -) -> UResult<()> { - // Print checksum and reading mode flag - print!("{sum} {}", reading_mode.as_char()); - - // Print filename - let _dropped_result = stdout().write_all(os_str_as_bytes(filename)?); - - Ok(()) -} - -/// Calculate checksum -/// -/// # 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 -fn cksum<'a, I>(mut options: Options, files: I) -> UResult<()> -where - I: Iterator, -{ - let mut files = files.peekable(); - - while let Some(filename) = files.next() { - // Check that in raw mode, we are not provided with several files. - if options.output_format.is_raw() && files.peek().is_some() { - return Err(Box::new(ChecksumError::RawMultipleFiles)); - } - - let filepath = Path::new(filename); - let stdin_buf; - let file_buf; - if filepath.is_dir() { - show!(USimpleError::new( - 1, - translate!("cksum-error-is-directory", "file" => filepath.display()) - )); - continue; - } - - // Handle the file input - let mut file = BufReader::new(if filename == "-" { - stdin_buf = stdin(); - Box::new(stdin_buf) as Box + fn print_feature(name: &str, available: bool) { + if available { + eprintln!("cksum: using {name} hardware support"); } else { - file_buf = match File::open(filepath) { - Ok(file) => file, - Err(err) => { - show!(err.map_err_context(|| filepath.to_string_lossy().to_string())); - continue; - } - }; - Box::new(file_buf) as Box - }); - - 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"))?; - - // Encodes the sum if df is Base64, leaves as-is otherwise. - let encode_sum = |sum: String, df: DigestFormat| { - if df.is_base64() { - encoding::for_cksum::BASE64.encode(&hex::decode(sum).unwrap()) - } else { - sum - } - }; - - match options.output_format { - OutputFormat::Raw => { - let bytes = match options.algo_name { - ALGORITHM_OPTIONS_CRC | ALGORITHM_OPTIONS_CRC32B => { - sum_hex.parse::().unwrap().to_be_bytes().to_vec() - } - ALGORITHM_OPTIONS_SYSV | ALGORITHM_OPTIONS_BSD => { - sum_hex.parse::().unwrap().to_be_bytes().to_vec() - } - _ => hex::decode(sum_hex).unwrap(), - }; - // Cannot handle multiple files anyway, output immediately. - stdout().write_all(&bytes)?; - return Ok(()); - } - OutputFormat::Legacy => { - print_legacy_checksum(&options, filename, &sum_hex, sz)?; - } - OutputFormat::Tagged(digest_format) => { - print_tagged_checksum(&options, filename, &encode_sum(sum_hex, digest_format))?; - } - OutputFormat::Untagged(digest_format, reading_mode) => { - print_untagged_checksum( - filename, - &encode_sum(sum_hex, digest_format), - reading_mode, - )?; - } + eprintln!("cksum: {name} support not detected"); } - - print!("{}", options.line_ending); } - Ok(()) + + // x86/x86_64 + print_feature("avx512", features.has_avx512()); + print_feature("avx2", features.has_avx2()); + print_feature("pclmul", features.has_pclmul()); + + // ARM aarch64 + if cfg!(target_arch = "aarch64") { + print_feature("vmull", features.has_vmull()); + } } mod options { @@ -285,6 +65,7 @@ mod options { pub const IGNORE_MISSING: &str = "ignore-missing"; pub const QUIET: &str = "quiet"; pub const ZERO: &str = "zero"; + pub const DEBUG: &str = "debug"; } /// cksum has a bunch of legacy behavior. We handle this in this function to @@ -329,46 +110,9 @@ fn handle_tag_text_binary_flags>( Ok((tag, binary)) } -/// Use already-processed arguments to decide the output format. -fn figure_out_output_format( - algo: &HashAlgorithm, - tag: bool, - binary: bool, - raw: bool, - base64: bool, -) -> OutputFormat { - // Raw output format takes precedence over anything else. - if raw { - return OutputFormat::Raw; - } - - // Then, if the algo is legacy, takes precedence over the rest - if LEGACY_ALGORITHMS.contains(&algo.name) { - return OutputFormat::Legacy; - } - - let digest_format = if base64 { - DigestFormat::Base64 - } else { - DigestFormat::Hexadecimal - }; - - // After that, decide between tagged and untagged output - if tag { - OutputFormat::Tagged(digest_format) - } else { - let reading_mode = if binary { - ReadingMode::Binary - } else { - ReadingMode::Text - }; - OutputFormat::Untagged(digest_format, reading_mode) - } -} - /// Sanitize the `--length` argument depending on `--algorithm` and `--length`. fn maybe_sanitize_length( - algo_cli: Option<&str>, + algo_cli: Option, input_length: Option<&str>, ) -> UResult> { match (algo_cli, input_length) { @@ -376,12 +120,12 @@ fn maybe_sanitize_length( (_, None) => Ok(None), // For SHA2 and SHA3, if a length is provided, ensure it is correct. - (Some(algo @ (ALGORITHM_OPTIONS_SHA2 | ALGORITHM_OPTIONS_SHA3)), Some(s_len)) => { + (Some(algo @ (AlgoKind::Sha2 | AlgoKind::Sha3)), Some(s_len)) => { sanitize_sha2_sha3_length_str(algo, s_len).map(Some) } // For BLAKE2b, if a length is provided, validate it. - (Some(ALGORITHM_OPTIONS_BLAKE2B), Some(len)) => calculate_blake2b_length_str(len), + (Some(AlgoKind::Blake2b), Some(len)) => calculate_blake2b_length_str(len), // For any other provided algorithm, check if length is 0. // Otherwise, this is an error. @@ -398,7 +142,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let algo_cli = matches .get_one::(options::ALGORITHM) - .map(String::as_str); + .map(AlgoKind::from_cksum) + .transpose()?; let input_length = matches .get_one::(options::LENGTH) @@ -415,7 +160,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if check { // cksum does not support '--check'ing legacy algorithms - if algo_cli.is_some_and(|algo_name| LEGACY_ALGORITHMS.contains(&algo_name)) { + if algo_cli.is_some_and(AlgoKind::is_legacy) { return Err(ChecksumError::AlgorithmNotSupportedWithCheck.into()); } @@ -435,8 +180,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Execute the checksum validation based on the presence of files or the use of stdin let verbose = ChecksumVerbose::new(status, quiet, warn); - let opts = ChecksumOptions { - binary: binary_flag, + let opts = ChecksumValidateOptions { ignore_missing, strict, verbose, @@ -448,31 +192,35 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Not --check // Set the default algorithm to CRC when not '--check'ing. - let algo_name = algo_cli.unwrap_or(ALGORITHM_OPTIONS_CRC); + let algo_kind = algo_cli.unwrap_or(AlgoKind::Crc); let (tag, binary) = handle_tag_text_binary_flags(std::env::args_os())?; - let algo = detect_algo(algo_name, length)?; + let algo = SizedAlgoKind::from_unsized(algo_kind, length)?; let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); let output_format = figure_out_output_format( - &algo, + algo, tag, binary, matches.get_flag(options::RAW), matches.get_flag(options::BASE64), ); - let opts = Options { - algo_name: algo.name, - digest: (algo.create_fn)(), - output_bits: algo.bits, - length, + // Print hardware debug info if requested + if matches.get_flag(options::DEBUG) { + print_cpu_debug_info(); + } + + let opts = ChecksumComputeOptions { + algo_kind: algo, output_format, line_ending, + binary: false, + no_names: false, }; - cksum(opts, files)?; + perform_checksum_computation(opts, files)?; Ok(()) } @@ -600,5 +348,11 @@ pub fn uu_app() -> Command { .help(translate!("cksum-help-zero")) .action(ArgAction::SetTrue), ) + .arg( + Arg::new(options::DEBUG) + .long(options::DEBUG) + .help(translate!("cksum-help-debug")) + .action(ArgAction::SetTrue), + ) .after_help(translate!("cksum-after-help")) } diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index c6c8789e2..9ef767d05 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1375,10 +1375,19 @@ pub fn copy(sources: &[PathBuf], target: &Path, options: &Options) -> CopyResult { // There is already a file and it isn't a symlink (managed in a different place) 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(CpError::Error( - translate!("cp-error-will-not-overwrite-just-created", "dest" => dest.quote(), "source" => source.quote()), - )); + // If the target was already created in this cp call, check if it's a directory. + // Directories should be merged (GNU cp behavior), but files should not be overwritten. + let dest_is_dir = fs::metadata(&dest).is_ok_and(|m| m.is_dir()); + let source_is_dir = fs::metadata(source).is_ok_and(|m| m.is_dir()); + + // Only prevent overwriting if both source and dest are files (not directories) + // Directories should be merged, which is handled by copy_directory + if !dest_is_dir || !source_is_dir { + // If the target file was already created in this cp call, do not overwrite + return Err(CpError::Error( + translate!("cp-error-will-not-overwrite-just-created", "dest" => dest.quote(), "source" => source.quote()), + )); + } } } diff --git a/src/uu/dd/Cargo.toml b/src/uu/dd/Cargo.toml index 4633bc06b..d1ac79fb5 100644 --- a/src/uu/dd/Cargo.toml +++ b/src/uu/dd/Cargo.toml @@ -23,7 +23,7 @@ gcd = { workspace = true } libc = { workspace = true } uucore = { workspace = true, features = [ "format", - "parser", + "parser-size", "quoting-style", "fs", ] } diff --git a/src/uu/df/Cargo.toml b/src/uu/df/Cargo.toml index 8f0d7d082..93017870d 100644 --- a/src/uu/df/Cargo.toml +++ b/src/uu/df/Cargo.toml @@ -19,7 +19,7 @@ path = "src/df.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["libc", "fsext", "parser", "fs"] } +uucore = { workspace = true, features = ["libc", "fsext", "parser-size", "fs"] } unicode-width = { workspace = true } thiserror = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/du/Cargo.toml b/src/uu/du/Cargo.toml index 87898811f..1241746e7 100644 --- a/src/uu/du/Cargo.toml +++ b/src/uu/du/Cargo.toml @@ -24,7 +24,8 @@ clap = { workspace = true } uucore = { workspace = true, features = [ "format", "fsext", - "parser", + "parser-size", + "parser-glob", "time", "safe-traversal", ] } diff --git a/src/uu/du/locales/en-US.ftl b/src/uu/du/locales/en-US.ftl index b503d8d53..bd6c095ba 100644 --- a/src/uu/du/locales/en-US.ftl +++ b/src/uu/du/locales/en-US.ftl @@ -69,6 +69,7 @@ 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 +du-error-hyphen-file-name-not-allowed = when reading file names from standard input, no file name of '-' allowed # Verbose/status messages du-verbose-ignored = { $path } ignored diff --git a/src/uu/du/locales/fr-FR.ftl b/src/uu/du/locales/fr-FR.ftl index e89385213..81bc80c71 100644 --- a/src/uu/du/locales/fr-FR.ftl +++ b/src/uu/du/locales/fr-FR.ftl @@ -69,6 +69,7 @@ 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 +du-error-hyphen-file-name-not-allowed = le nom de fichier '-' n'est pas autorisé lors de la lecture de l'entrée standard # Messages verbeux/de statut du-verbose-ignored = { $path } ignoré diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 4c29d07d3..f57228d76 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -2,16 +2,15 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// // spell-checker:ignore fstatat openat dirfd use clap::{Arg, ArgAction, ArgMatches, Command, builder::PossibleValue}; use glob::Pattern; use std::collections::HashSet; use std::env; -use std::ffi::OsStr; -use std::ffi::OsString; -use std::fs::Metadata; -use std::fs::{self, DirEntry, File}; +use std::ffi::{OsStr, OsString}; +use std::fs::{self, DirEntry, File, Metadata}; use std::io::{BufRead, BufReader, stdout}; #[cfg(not(windows))] use std::os::unix::fs::MetadataExt; @@ -942,6 +941,9 @@ fn read_files_from(file_name: &OsStr) -> Result, std::io::Error> { translate!("du-error-invalid-zero-length-file-name", "file" => file_name.to_string_lossy(), "line" => line_number) ); set_exit_code(1); + } else if path == b"-" && file_name == "-" { + show_error!("{}", translate!("du-error-hyphen-file-name-not-allowed")); + set_exit_code(1); } else { let p = PathBuf::from(&*uucore::os_str_from_bytes(&path).unwrap()); if !paths.contains(&p) { @@ -1257,6 +1259,7 @@ pub fn uu_app() -> Command { ) .arg( Arg::new(options::APPARENT_SIZE) + .short('A') .long(options::APPARENT_SIZE) .help(translate!("du-help-apparent-size")) .action(ArgAction::SetTrue), diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index fbd233105..da0daf80c 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -102,7 +102,6 @@ struct Options<'a> { } /// print `name=value` env pairs on screen -/// if null is true, separate pairs with a \0, \n otherwise fn print_env(line_ending: LineEnding) { let stdout_raw = io::stdout(); let mut stdout = stdout_raw.lock(); diff --git a/src/uu/factor/Cargo.toml b/src/uu/factor/Cargo.toml index 15d09f7a0..ef672bf93 100644 --- a/src/uu/factor/Cargo.toml +++ b/src/uu/factor/Cargo.toml @@ -31,6 +31,7 @@ path = "src/main.rs" [dev-dependencies] divan = { workspace = true } +rand = { workspace = true } uucore = { workspace = true, features = ["benchmark"] } [lib] diff --git a/src/uu/factor/benches/factor_bench.rs b/src/uu/factor/benches/factor_bench.rs index 89498e0ae..952ea09a6 100644 --- a/src/uu/factor/benches/factor_bench.rs +++ b/src/uu/factor/benches/factor_bench.rs @@ -3,6 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore funcs + use divan::{Bencher, black_box}; use uu_factor::uumain; use uucore::benchmark::run_util_function; @@ -20,41 +22,6 @@ fn factor_multiple_u64s(bencher: Bencher, start_num: u64) { }); } -/* Too much variance -/// Benchmark multiple u128 digits -#[divan::bench(args = [(18446744073709551616)])] -fn factor_multiple_u128s(bencher: Bencher, start_num: u128) { - bencher - .with_inputs(|| { - // this is a range of 1000 different u128 integers - (start_num, start_num + 1000) - }) - .bench_values(|(start_u128, end_u128)| { - for u128_digit in start_u128..=end_u128 { - black_box(run_util_function(uumain, &[&u128_digit.to_string()])); - } - }); -} -*/ - -/* Too much variance -/// Benchmark multiple > u128::MAX digits -#[divan::bench] -fn factor_multiple_big_uint(bencher: Bencher) { - // max u128 value is 340_282_366_920_938_463_463_374_607_431_768_211_455 - bencher - // this is a range of 3 different BigUints. The range is small due to - // some BigUints being unable to be factorized into prime numbers properly - .with_inputs(|| (768_211_459_u64, 768_211_461_u64)) - .bench_values(|(start_big_uint, end_big_uint)| { - for digit in start_big_uint..=end_big_uint { - let big_uint_str = format!("340282366920938463463374607431768211456{digit}"); - black_box(run_util_function(uumain, &[&big_uint_str])); - } - }); -} -*/ - fn main() { divan::main(); } diff --git a/src/uu/fold/Cargo.toml b/src/uu/fold/Cargo.toml index 644d78b41..845ce2c96 100644 --- a/src/uu/fold/Cargo.toml +++ b/src/uu/fold/Cargo.toml @@ -21,6 +21,7 @@ path = "src/fold.rs" clap = { workspace = true } uucore = { workspace = true } fluent = { workspace = true } +unicode-width = { workspace = true } [dev-dependencies] divan = { workspace = true } diff --git a/src/uu/fold/benches/fold_bench.rs b/src/uu/fold/benches/fold_bench.rs index abd69525f..d76ddbeaf 100644 --- a/src/uu/fold/benches/fold_bench.rs +++ b/src/uu/fold/benches/fold_bench.rs @@ -4,7 +4,6 @@ // file that was distributed with this source code. use divan::{Bencher, black_box}; -use std::fmt::Write; use uu_fold::uumain; use uucore::benchmark::{create_test_file, run_util_function}; @@ -12,12 +11,12 @@ use uucore::benchmark::{create_test_file, run_util_function}; #[divan::bench(args = [100_000])] fn fold_many_lines(bencher: Bencher, num_lines: usize) { let temp_dir = tempfile::tempdir().unwrap(); - // Create long lines that need folding - let data = (0..num_lines) - .fold(String::new(), |mut acc, i| { - writeln!(&mut acc, "This is a very long line number {i} that definitely needs to be folded at the default width of 80 columns").unwrap(); - acc - }); + let mut data = String::with_capacity(num_lines * 110); + for i in 0..num_lines { + data.push_str("This is a very long line number "); + append_usize(&mut data, i); + data.push_str(" that definitely needs to be folded at the default width of 80 columns\n"); + } let file_path = create_test_file(data.as_bytes(), temp_dir.path()); let file_path_str = file_path.to_str().unwrap(); @@ -30,14 +29,12 @@ fn fold_many_lines(bencher: Bencher, num_lines: usize) { #[divan::bench(args = [50_000])] fn fold_custom_width(bencher: Bencher, num_lines: usize) { let temp_dir = tempfile::tempdir().unwrap(); - let data = (0..num_lines).fold(String::new(), |mut acc, i| { - writeln!( - &mut acc, - "Line {i} with enough text to exceed width 40 characters and require folding" - ) - .unwrap(); - acc - }); + let mut data = String::with_capacity(num_lines * 80); + for i in 0..num_lines { + data.push_str("Line "); + append_usize(&mut data, i); + data.push_str(" with enough text to exceed width 40 characters and require folding\n"); + } let file_path = create_test_file(data.as_bytes(), temp_dir.path()); let file_path_str = file_path.to_str().unwrap(); @@ -49,3 +46,21 @@ fn fold_custom_width(bencher: Bencher, num_lines: usize) { fn main() { divan::main(); } + +fn append_usize(buf: &mut String, mut value: usize) { + let mut digits = [0u8; 20]; + let mut idx = digits.len(); + + if value == 0 { + buf.push('0'); + return; + } + + while value > 0 { + idx -= 1; + digits[idx] = b'0' + (value % 10) as u8; + value /= 10; + } + + buf.push_str(std::str::from_utf8(&digits[idx..]).unwrap()); +} diff --git a/src/uu/fold/locales/en-US.ftl b/src/uu/fold/locales/en-US.ftl index 9f8c6f3b9..d42416667 100644 --- a/src/uu/fold/locales/en-US.ftl +++ b/src/uu/fold/locales/en-US.ftl @@ -2,6 +2,7 @@ fold-about = Writes each file (or standard input if no files are given) to standard output whilst breaking long lines fold-usage = fold [OPTION]... [FILE]... fold-bytes-help = count using bytes rather than columns (meaning control characters such as newline are not treated specially) +fold-characters-help = count using character positions rather than display columns fold-spaces-help = break lines at word boundaries rather than a hard cut-off fold-width-help = set WIDTH as the maximum line width rather than 80 fold-error-illegal-width = illegal width value diff --git a/src/uu/fold/locales/fr-FR.ftl b/src/uu/fold/locales/fr-FR.ftl index 1a7235940..ce313160c 100644 --- a/src/uu/fold/locales/fr-FR.ftl +++ b/src/uu/fold/locales/fr-FR.ftl @@ -1,6 +1,7 @@ fold-about = Écrit chaque fichier (ou l'entrée standard si aucun fichier n'est donné) sur la sortie standard en coupant les lignes trop longues fold-usage = fold [OPTION]... [FICHIER]... fold-bytes-help = compter en octets plutôt qu'en colonnes (les caractères de contrôle comme retour chariot ne sont pas traités spécialement) +fold-characters-help = compter en caractères plutôt qu'en colonnes d'affichage fold-spaces-help = couper les lignes aux limites de mots plutôt qu'à une largeur fixe fold-width-help = définir WIDTH comme largeur de ligne maximale au lieu de 80 fold-error-illegal-width = valeur de largeur illégale diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index bbaac56be..a2ddbed6a 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -9,6 +9,7 @@ use clap::{Arg, ArgAction, Command}; use std::fs::File; use std::io::{BufRead, BufReader, BufWriter, Read, Write, stdin, stdout}; use std::path::Path; +use unicode_width::UnicodeWidthChar; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::format_usage; @@ -21,11 +22,28 @@ const TAB: u8 = b'\t'; mod options { pub const BYTES: &str = "bytes"; + pub const CHARACTERS: &str = "characters"; pub const SPACES: &str = "spaces"; pub const WIDTH: &str = "width"; pub const FILE: &str = "file"; } +#[derive(Clone, Copy, PartialEq, Eq)] +enum WidthMode { + Columns, + Characters, +} + +struct FoldContext<'a, W: Write> { + spaces: bool, + width: usize, + mode: WidthMode, + writer: &'a mut W, + output: &'a mut Vec, + col_count: &'a mut usize, + last_space: &'a mut Option, +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let args = args.collect_lossy(); @@ -34,6 +52,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; let bytes = matches.get_flag(options::BYTES); + let characters = matches.get_flag(options::CHARACTERS); let spaces = matches.get_flag(options::SPACES); let poss_width = match matches.get_one::(options::WIDTH) { Some(v) => Some(v.clone()), @@ -55,7 +74,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { None => vec!["-".to_owned()], }; - fold(&files, bytes, spaces, width) + fold(&files, bytes, characters, spaces, width) } pub fn uu_app() -> Command { @@ -72,6 +91,13 @@ pub fn uu_app() -> Command { .help(translate!("fold-bytes-help")) .action(ArgAction::SetTrue), ) + .arg( + Arg::new(options::CHARACTERS) + .long(options::CHARACTERS) + .help(translate!("fold-characters-help")) + .conflicts_with(options::BYTES) + .action(ArgAction::SetTrue), + ) .arg( Arg::new(options::SPACES) .long(options::SPACES) @@ -107,7 +133,13 @@ fn handle_obsolete(args: &[String]) -> (Vec, Option) { (args.to_vec(), None) } -fn fold(filenames: &[String], bytes: bool, spaces: bool, width: usize) -> UResult<()> { +fn fold( + filenames: &[String], + bytes: bool, + characters: bool, + spaces: bool, + width: usize, +) -> UResult<()> { let mut output = BufWriter::new(stdout()); for filename in filenames { @@ -125,7 +157,12 @@ fn fold(filenames: &[String], bytes: bool, spaces: bool, width: usize) -> UResul if bytes { fold_file_bytewise(buffer, spaces, width, &mut output)?; } else { - fold_file(buffer, spaces, width, &mut output)?; + let mode = if characters { + WidthMode::Characters + } else { + WidthMode::Columns + }; + fold_file(buffer, spaces, width, mode, &mut output)?; } } @@ -213,6 +250,312 @@ fn fold_file_bytewise( Ok(()) } +fn next_tab_stop(col_count: usize) -> usize { + col_count + TAB_WIDTH - col_count % TAB_WIDTH +} + +fn compute_col_count(buffer: &[u8], mode: WidthMode) -> usize { + match mode { + WidthMode::Characters => std::str::from_utf8(buffer) + .map(|s| s.chars().count()) + .unwrap_or(buffer.len()), + WidthMode::Columns => { + if let Ok(s) = std::str::from_utf8(buffer) { + let mut width = 0; + for ch in s.chars() { + match ch { + '\r' => width = 0, + '\t' => width = next_tab_stop(width), + '\x08' => width = width.saturating_sub(1), + _ => width += UnicodeWidthChar::width(ch).unwrap_or(0), + } + } + width + } else { + let mut width = 0; + for &byte in buffer { + match byte { + CR => width = 0, + TAB => width = next_tab_stop(width), + 0x08 => width = width.saturating_sub(1), + _ => width += 1, + } + } + width + } + } + } +} + +fn emit_output(ctx: &mut FoldContext<'_, W>) -> UResult<()> { + let consume = match *ctx.last_space { + Some(index) => index + 1, + None => ctx.output.len(), + }; + + if consume > 0 { + ctx.writer.write_all(&ctx.output[..consume])?; + } + ctx.writer.write_all(&[NL])?; + + let last_space = *ctx.last_space; + + if consume < ctx.output.len() { + ctx.output.drain(..consume); + } else { + ctx.output.clear(); + } + + *ctx.col_count = compute_col_count(ctx.output, ctx.mode); + + if ctx.spaces { + *ctx.last_space = last_space.and_then(|idx| { + if idx < consume { + None + } else { + Some(idx - consume) + } + }); + } else { + *ctx.last_space = None; + } + Ok(()) +} + +fn process_ascii_line(line: &[u8], ctx: &mut FoldContext<'_, W>) -> UResult<()> { + let mut idx = 0; + let len = line.len(); + + while idx < len { + match line[idx] { + NL => { + *ctx.last_space = None; + emit_output(ctx)?; + break; + } + CR => { + ctx.output.push(CR); + *ctx.col_count = 0; + idx += 1; + } + 0x08 => { + ctx.output.push(0x08); + *ctx.col_count = ctx.col_count.saturating_sub(1); + idx += 1; + } + TAB if ctx.mode == WidthMode::Columns => { + loop { + let next_stop = next_tab_stop(*ctx.col_count); + if next_stop > ctx.width && !ctx.output.is_empty() { + emit_output(ctx)?; + continue; + } + *ctx.col_count = next_stop; + break; + } + if ctx.spaces { + *ctx.last_space = Some(ctx.output.len()); + } else { + *ctx.last_space = None; + } + ctx.output.push(TAB); + idx += 1; + } + 0x00..=0x07 | 0x0B..=0x0C | 0x0E..=0x1F | 0x7F => { + ctx.output.push(line[idx]); + if ctx.spaces && line[idx].is_ascii_whitespace() && line[idx] != CR { + *ctx.last_space = Some(ctx.output.len() - 1); + } else if !ctx.spaces { + *ctx.last_space = None; + } + idx += 1; + } + _ => { + let start = idx; + while idx < len + && !matches!( + line[idx], + NL | CR | TAB | 0x08 | 0x00..=0x07 | 0x0B..=0x0C | 0x0E..=0x1F | 0x7F + ) + { + idx += 1; + } + push_ascii_segment(&line[start..idx], ctx)?; + } + } + } + + Ok(()) +} + +fn push_ascii_segment(segment: &[u8], ctx: &mut FoldContext<'_, W>) -> UResult<()> { + if segment.is_empty() { + return Ok(()); + } + + let mut remaining = segment; + + while !remaining.is_empty() { + if *ctx.col_count >= ctx.width { + emit_output(ctx)?; + continue; + } + + let available = ctx.width - *ctx.col_count; + let take = remaining.len().min(available); + let base_len = ctx.output.len(); + + ctx.output.extend_from_slice(&remaining[..take]); + *ctx.col_count += take; + + if ctx.spaces { + if let Some(pos) = remaining[..take] + .iter() + .rposition(|b| b.is_ascii_whitespace() && *b != CR) + { + *ctx.last_space = Some(base_len + pos); + } + } else { + *ctx.last_space = None; + } + + remaining = &remaining[take..]; + } + + Ok(()) +} + +fn process_utf8_line(line: &str, ctx: &mut FoldContext<'_, W>) -> UResult<()> { + if line.is_ascii() { + return process_ascii_line(line.as_bytes(), ctx); + } + + let line_bytes = line.as_bytes(); + let mut iter = line.char_indices().peekable(); + + while let Some((byte_idx, ch)) = iter.next() { + // Include combining characters with the base character + while let Some(&(_, next_ch)) = iter.peek() { + if unicode_width::UnicodeWidthChar::width(next_ch).unwrap_or(1) == 0 { + iter.next(); + } else { + break; + } + } + + let next_idx = iter.peek().map(|(idx, _)| *idx).unwrap_or(line_bytes.len()); + + if ch == '\n' { + *ctx.last_space = None; + emit_output(ctx)?; + break; + } + + if *ctx.col_count >= ctx.width { + emit_output(ctx)?; + } + + if ch == '\r' { + ctx.output + .extend_from_slice(&line_bytes[byte_idx..next_idx]); + *ctx.col_count = 0; + continue; + } + + if ch == '\x08' { + ctx.output + .extend_from_slice(&line_bytes[byte_idx..next_idx]); + *ctx.col_count = ctx.col_count.saturating_sub(1); + continue; + } + + if ctx.mode == WidthMode::Columns && ch == '\t' { + loop { + let next_stop = next_tab_stop(*ctx.col_count); + if next_stop > ctx.width && !ctx.output.is_empty() { + emit_output(ctx)?; + continue; + } + *ctx.col_count = next_stop; + break; + } + if ctx.spaces { + *ctx.last_space = Some(ctx.output.len()); + } else { + *ctx.last_space = None; + } + ctx.output + .extend_from_slice(&line_bytes[byte_idx..next_idx]); + continue; + } + + let added = match ctx.mode { + WidthMode::Columns => UnicodeWidthChar::width(ch).unwrap_or(0), + WidthMode::Characters => 1, + }; + + if ctx.mode == WidthMode::Columns + && added > 0 + && *ctx.col_count + added > ctx.width + && !ctx.output.is_empty() + { + emit_output(ctx)?; + } + + if ctx.spaces && ch.is_ascii_whitespace() { + *ctx.last_space = Some(ctx.output.len()); + } + + ctx.output + .extend_from_slice(&line_bytes[byte_idx..next_idx]); + *ctx.col_count = ctx.col_count.saturating_add(added); + } + + Ok(()) +} + +fn process_non_utf8_line(line: &[u8], ctx: &mut FoldContext<'_, W>) -> UResult<()> { + for &byte in line { + if byte == NL { + *ctx.last_space = None; + emit_output(ctx)?; + break; + } + + if *ctx.col_count >= ctx.width { + emit_output(ctx)?; + } + + match byte { + CR => *ctx.col_count = 0, + TAB => { + let next_stop = next_tab_stop(*ctx.col_count); + if next_stop > ctx.width && !ctx.output.is_empty() { + emit_output(ctx)?; + } + *ctx.col_count = next_stop; + *ctx.last_space = if ctx.spaces { + Some(ctx.output.len()) + } else { + None + }; + ctx.output.push(byte); + continue; + } + 0x08 => *ctx.col_count = ctx.col_count.saturating_sub(1), + _ if ctx.spaces && byte.is_ascii_whitespace() => { + *ctx.last_space = Some(ctx.output.len()); + *ctx.col_count = ctx.col_count.saturating_add(1); + } + _ => *ctx.col_count = ctx.col_count.saturating_add(1), + } + + ctx.output.push(byte); + } + + Ok(()) +} + /// Fold `file` to fit `width` (number of columns). /// /// By default `fold` treats tab, backspace, and carriage return specially: @@ -226,6 +569,7 @@ fn fold_file( mut file: BufReader, spaces: bool, width: usize, + mode: WidthMode, writer: &mut W, ) -> UResult<()> { let mut line = Vec::new(); @@ -233,30 +577,6 @@ fn fold_file( let mut col_count = 0; let mut last_space = None; - /// Print the output line, resetting the column and character counts. - /// - /// If `spaces` is `true`, print the output line up to the last - /// encountered whitespace character (inclusive) and set the remaining - /// characters as the start of the next line. - macro_rules! emit_output { - () => { - let consume = match last_space { - Some(i) => i + 1, - None => output.len(), - }; - - writer.write_all(&output[..consume])?; - writer.write_all(&[NL])?; - output.drain(..consume); - - // we know there are no tabs left in output, so each char counts - // as 1 column - col_count = output.len(); - - last_space = None; - }; - } - loop { if file .read_until(NL, &mut line) @@ -266,50 +586,27 @@ fn fold_file( break; } - for ch in &line { - if *ch == NL { - // make sure to _not_ split output at whitespace, since we - // know the entire output will fit - last_space = None; - emit_output!(); - break; - } + let mut ctx = FoldContext { + spaces, + width, + mode, + writer, + output: &mut output, + col_count: &mut col_count, + last_space: &mut last_space, + }; - if col_count >= width { - emit_output!(); - } - - match *ch { - CR => col_count = 0, - TAB => { - let next_tab_stop = col_count + TAB_WIDTH - col_count % TAB_WIDTH; - - if next_tab_stop > width && !output.is_empty() { - emit_output!(); - } - - col_count = next_tab_stop; - last_space = if spaces { Some(output.len()) } else { None }; - } - 0x08 => { - col_count = col_count.saturating_sub(1); - } - _ if spaces && ch.is_ascii_whitespace() => { - last_space = Some(output.len()); - col_count += 1; - } - _ => col_count += 1, - } - - output.push(*ch); + match std::str::from_utf8(&line) { + Ok(s) => process_utf8_line(s, &mut ctx)?, + Err(_) => process_non_utf8_line(&line, &mut ctx)?, } - if !output.is_empty() { - writer.write_all(&output)?; - output.truncate(0); - } + line.clear(); + } - line.truncate(0); + if !output.is_empty() { + writer.write_all(&output)?; + output.clear(); } Ok(()) diff --git a/src/uu/hashsum/Cargo.toml b/src/uu/hashsum/Cargo.toml index 00eb152ed..ec382870b 100644 --- a/src/uu/hashsum/Cargo.toml +++ b/src/uu/hashsum/Cargo.toml @@ -19,7 +19,7 @@ path = "src/hashsum.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["checksum", "sum"] } +uucore = { workspace = true, features = ["checksum", "encoding", "sum"] } fluent = { workspace = true } [[bin]] diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index e6500a353..c60bff363 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -3,53 +3,28 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) algo, algoname, regexes, nread, nonames +// spell-checker:ignore (ToDO) algo, algoname, bitlen, regexes, nread, nonames -use clap::ArgAction; -use clap::builder::ValueParser; -use clap::value_parser; -use clap::{Arg, ArgMatches, Command}; use std::ffi::{OsStr, OsString}; -use std::fs::File; -use std::io::{BufReader, Read, stdin}; use std::iter; use std::num::ParseIntError; use std::path::Path; -use uucore::checksum::ChecksumError; -use uucore::checksum::ChecksumOptions; -use uucore::checksum::ChecksumVerbose; -use uucore::checksum::HashAlgorithm; -use uucore::checksum::calculate_blake2b_length; -use uucore::checksum::create_sha3; -use uucore::checksum::detect_algo; -use uucore::checksum::digest_reader; -use uucore::checksum::escape_filename; -use uucore::checksum::perform_checksum_validation; -use uucore::error::{UResult, strip_errno}; -use uucore::format_usage; -use uucore::sum::{Digest, Sha3_224, Sha3_256, Sha3_384, Sha3_512, Shake128, Shake256}; -use uucore::translate; + +use clap::builder::ValueParser; +use clap::{Arg, ArgAction, ArgMatches, Command, value_parser}; + +use uucore::checksum::compute::{ + ChecksumComputeOptions, figure_out_output_format, perform_checksum_computation, +}; +use uucore::checksum::validate::{ + ChecksumValidateOptions, ChecksumVerbose, perform_checksum_validation, +}; +use uucore::checksum::{AlgoKind, ChecksumError, SizedAlgoKind, calculate_blake2b_length}; +use uucore::error::UResult; +use uucore::line_ending::LineEnding; +use uucore::{format_usage, translate}; const NAME: &str = "hashsum"; -// Using the same read buffer size as GNU -const READ_BUFFER_SIZE: usize = 32 * 1024; - -struct Options<'a> { - algoname: &'static str, - digest: Box, - binary: bool, - binary_name: &'a str, - //check: bool, - tag: bool, - nonames: bool, - //status: bool, - //quiet: bool, - //strict: bool, - //warn: bool, - output_bits: usize, - zero: bool, - //ignore_missing: bool, -} /// Creates a hasher instance based on the command-line flags. /// @@ -63,10 +38,10 @@ struct Options<'a> { /// the output length in bits or an Err if multiple hash algorithms are specified or if a /// required flag is missing. #[allow(clippy::cognitive_complexity)] -fn create_algorithm_from_flags(matches: &ArgMatches) -> UResult { - let mut alg: Option = None; +fn create_algorithm_from_flags(matches: &ArgMatches) -> UResult<(AlgoKind, Option)> { + let mut alg: Option<(AlgoKind, Option)> = None; - let mut set_or_err = |new_alg: HashAlgorithm| -> UResult<()> { + let mut set_or_err = |new_alg: (AlgoKind, Option)| -> UResult<()> { if alg.is_some() { return Err(ChecksumError::CombineMultipleAlgorithms.into()); } @@ -75,80 +50,57 @@ fn create_algorithm_from_flags(matches: &ArgMatches) -> UResult { }; if matches.get_flag("md5") { - set_or_err(detect_algo("md5sum", None)?)?; + set_or_err((AlgoKind::Md5, None))?; } if matches.get_flag("sha1") { - set_or_err(detect_algo("sha1sum", None)?)?; + set_or_err((AlgoKind::Sha1, None))?; } if matches.get_flag("sha224") { - set_or_err(detect_algo("sha224sum", None)?)?; + set_or_err((AlgoKind::Sha224, None))?; } if matches.get_flag("sha256") { - set_or_err(detect_algo("sha256sum", None)?)?; + set_or_err((AlgoKind::Sha256, None))?; } if matches.get_flag("sha384") { - set_or_err(detect_algo("sha384sum", None)?)?; + set_or_err((AlgoKind::Sha384, None))?; } if matches.get_flag("sha512") { - set_or_err(detect_algo("sha512sum", None)?)?; + set_or_err((AlgoKind::Sha512, None))?; } if matches.get_flag("b2sum") { - set_or_err(detect_algo("b2sum", None)?)?; + set_or_err((AlgoKind::Blake2b, None))?; } if matches.get_flag("b3sum") { - set_or_err(detect_algo("b3sum", None)?)?; + set_or_err((AlgoKind::Blake3, None))?; } if matches.get_flag("sha3") { match matches.get_one::("bits") { - Some(bits) => set_or_err(create_sha3(*bits)?)?, + Some(bits @ (224 | 256 | 384 | 512)) => set_or_err((AlgoKind::Sha3, Some(*bits)))?, + Some(bits) => return Err(ChecksumError::InvalidLengthForSha(bits.to_string()).into()), None => return Err(ChecksumError::LengthRequired("SHA3".into()).into()), } } if matches.get_flag("sha3-224") { - set_or_err(HashAlgorithm { - name: "SHA3-224", - create_fn: Box::new(|| Box::new(Sha3_224::new())), - bits: 224, - })?; + set_or_err((AlgoKind::Sha3, Some(224)))?; } if matches.get_flag("sha3-256") { - set_or_err(HashAlgorithm { - name: "SHA3-256", - create_fn: Box::new(|| Box::new(Sha3_256::new())), - bits: 256, - })?; + set_or_err((AlgoKind::Sha3, Some(256)))?; } if matches.get_flag("sha3-384") { - set_or_err(HashAlgorithm { - name: "SHA3-384", - create_fn: Box::new(|| Box::new(Sha3_384::new())), - bits: 384, - })?; + set_or_err((AlgoKind::Sha3, Some(384)))?; } if matches.get_flag("sha3-512") { - set_or_err(HashAlgorithm { - name: "SHA3-512", - create_fn: Box::new(|| Box::new(Sha3_512::new())), - bits: 512, - })?; + set_or_err((AlgoKind::Sha3, Some(512)))?; } if matches.get_flag("shake128") { match matches.get_one::("bits") { - Some(bits) => set_or_err(HashAlgorithm { - name: "SHAKE128", - create_fn: Box::new(|| Box::new(Shake128::new())), - bits: *bits, - })?, + Some(bits) => set_or_err((AlgoKind::Shake128, Some(*bits)))?, None => return Err(ChecksumError::LengthRequired("SHAKE128".into()).into()), } } if matches.get_flag("shake256") { match matches.get_one::("bits") { - Some(bits) => set_or_err(HashAlgorithm { - name: "SHAKE256", - create_fn: Box::new(|| Box::new(Shake256::new())), - bits: *bits, - })?, + Some(bits) => set_or_err((AlgoKind::Shake256, Some(*bits)))?, None => return Err(ChecksumError::LengthRequired("SHAKE256".into()).into()), } } @@ -198,10 +150,10 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { None => None, }; - let algo = if is_hashsum_bin { + let (algo_kind, length) = if is_hashsum_bin { create_algorithm_from_flags(&matches)? } else { - detect_algo(&binary_name, length)? + (AlgoKind::from_bin_name(&binary_name)?, length) }; let binary = if matches.get_flag("binary") { @@ -213,9 +165,9 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { }; let check = matches.get_flag("check"); let status = matches.get_flag("status"); - let quiet = matches.get_flag("quiet") || status; + let quiet = matches.get_flag("quiet"); let strict = matches.get_flag("strict"); - let warn = matches.get_flag("warn") && !status; + let warn = matches.get_flag("warn"); let ignore_missing = matches.get_flag("ignore-missing"); if ignore_missing && !check { @@ -227,16 +179,14 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { // on Windows, allow --binary/--text to be used with --check // and keep the behavior of defaulting to binary #[cfg(not(windows))] - let binary = { + { let text_flag = matches.get_flag("text"); let binary_flag = matches.get_flag("binary"); if binary_flag || text_flag { return Err(ChecksumError::BinaryTextConflict.into()); } - - false - }; + } // Execute the checksum validation based on the presence of files or the use of stdin // Determine the source of input: a list of files or stdin. @@ -247,52 +197,51 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { let verbose = ChecksumVerbose::new(status, quiet, warn); - let opts = ChecksumOptions { - binary, + let opts = ChecksumValidateOptions { ignore_missing, strict, verbose, }; // Execute the checksum validation - return perform_checksum_validation( - input.iter().copied(), - Some(algo.name), - Some(algo.bits), - opts, - ); + return perform_checksum_validation(input.iter().copied(), Some(algo_kind), length, opts); } else if quiet { return Err(ChecksumError::QuietNotCheck.into()); } else if strict { return Err(ChecksumError::StrictNotCheck.into()); } - let nonames = *matches + let no_names = *matches .try_get_one("no-names") .unwrap_or(None) .unwrap_or(&false); - let zero = matches.get_flag("zero"); + let line_ending = LineEnding::from_zero_flag(matches.get_flag("zero")); - let opts = Options { - algoname: algo.name, - digest: (algo.create_fn)(), - output_bits: algo.bits, + let algo = SizedAlgoKind::from_unsized(algo_kind, length)?; + + let opts = ChecksumComputeOptions { + algo_kind: algo, + output_format: figure_out_output_format( + algo, + matches.get_flag(options::TAG), + binary, + /* raw */ false, + /* base64: */ false, + ), + line_ending, binary, - binary_name: &binary_name, - tag: matches.get_flag("tag"), - nonames, - //status, - //quiet, - //warn, - zero, - //ignore_missing, + no_names, }; + let files = matches.get_many::(options::FILE).map_or_else( + // No files given, read from stdin. + || Box::new(iter::once(OsStr::new("-"))) as Box>, + // At least one file given, read from them. + |files| Box::new(files.map(OsStr::new)) as Box>, + ); + // Show the hashsum of the input - match matches.get_many::(options::FILE) { - Some(files) => hashsum(opts, files.map(|f| f.as_os_str())), - None => hashsum(opts, iter::once(OsStr::new("-"))), - } + perform_checksum_computation(opts, files) } mod options { @@ -523,87 +472,3 @@ fn uu_app(binary_name: &str) -> (Command, bool) { (command, is_hashsum_bin) } - -#[allow(clippy::cognitive_complexity)] -fn hashsum<'a, I>(mut options: Options, files: I) -> UResult<()> -where - I: Iterator, -{ - let binary_marker = if options.binary { "*" } else { " " }; - let mut err_found = None; - for filename in files { - let filename = Path::new(filename); - - let mut file = BufReader::with_capacity( - READ_BUFFER_SIZE, - if filename == OsStr::new("-") { - Box::new(stdin()) as Box - } else { - let file_buf = match File::open(filename) { - Ok(f) => f, - Err(e) => { - eprintln!( - "{}: {}: {}", - options.binary_name, - filename.to_string_lossy(), - strip_errno(&e) - ); - err_found = Some(ChecksumError::Io(e)); - continue; - } - }; - Box::new(file_buf) as Box - }, - ); - - let sum = match digest_reader( - &mut options.digest, - &mut file, - options.binary, - options.output_bits, - ) { - Ok((sum, _)) => sum, - Err(e) => { - eprintln!( - "{}: {}: {}", - options.binary_name, - filename.to_string_lossy(), - strip_errno(&e) - ); - err_found = Some(ChecksumError::Io(e)); - continue; - } - }; - - let (escaped_filename, prefix) = escape_filename(filename); - if options.tag { - if options.algoname == "blake2b" { - if options.digest.output_bits() == 512 { - println!("BLAKE2b ({escaped_filename}) = {sum}"); - } else { - // special case for BLAKE2b with non-default output length - println!( - "BLAKE2b-{} ({escaped_filename}) = {sum}", - options.digest.output_bits() - ); - } - } else { - println!( - "{prefix}{} ({escaped_filename}) = {sum}", - options.algoname.to_ascii_uppercase() - ); - } - } else if options.nonames { - println!("{sum}"); - } else if options.zero { - // with zero, we don't escape the filename - print!("{sum} {binary_marker}{}\0", filename.display()); - } else { - println!("{prefix}{sum} {binary_marker}{escaped_filename}"); - } - } - match err_found { - None => Ok(()), - Some(e) => Err(Box::new(e)), - } -} diff --git a/src/uu/head/BENCHMARKING.md b/src/uu/head/BENCHMARKING.md index d751d1f7f..79b6821e5 100644 --- a/src/uu/head/BENCHMARKING.md +++ b/src/uu/head/BENCHMARKING.md @@ -20,7 +20,7 @@ William Shakespeare*, which is in the public domain in the United States and most other parts of the world. ```shell -wget -O shakespeare.txt https://www.gutenberg.org/files/100/100-0.txt +curl -o shakespeare.txt https://www.gutenberg.org/files/100/100-0.txt ``` This particular file has about 170,000 lines, each of which is no longer diff --git a/src/uu/head/Cargo.toml b/src/uu/head/Cargo.toml index 9ee84db3d..2c0e18c1a 100644 --- a/src/uu/head/Cargo.toml +++ b/src/uu/head/Cargo.toml @@ -22,7 +22,7 @@ clap = { workspace = true } memchr = { workspace = true } thiserror = { workspace = true } uucore = { workspace = true, features = [ - "parser", + "parser-size", "ringbuffer", "lines", "fs", diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 4242cc04b..582eb91ac 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -25,7 +25,6 @@ use uucore::display::Quotable; use uucore::entries::{grp2gid, usr2uid}; use uucore::error::{FromIo, UError, UResult, UUsageError}; use uucore::fs::dir_strip_dot_for_creation; -use uucore::mode::get_umask; use uucore::perms::{Verbosity, VerbosityLevel, wrap_chown}; use uucore::process::{getegid, geteuid}; #[cfg(feature = "selinux")] @@ -339,7 +338,7 @@ fn behavior(matches: &ArgMatches) -> UResult { let specified_mode: Option = if matches.contains_id(OPT_MODE) { let x = matches.get_one::(OPT_MODE).ok_or(1)?; - Some(mode::parse(x, considering_dir, get_umask()).map_err(|err| { + Some(uucore::mode::parse(x, considering_dir, 0).map_err(|err| { show_error!( "{}", translate!("install-error-invalid-mode", "error" => err) @@ -712,10 +711,9 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR Ok(()) } -/// Handle incomplete user/group parings for chown. +/// Handle ownership changes when -o/--owner or -g/--group flags are used. /// /// Returns a Result type with the Err variant containing the error message. -/// If the user is root, revert the uid & gid /// /// # Parameters /// @@ -736,11 +734,8 @@ fn chown_optional_user_group(path: &Path, b: &Behavior) -> UResult<()> { // Determine the owner and group IDs to be used for chown. let (owner_id, group_id) = if b.owner_id.is_some() || b.group_id.is_some() { (b.owner_id, b.group_id) - } else if geteuid() == 0 { - // Special case for root user. - (Some(0), Some(0)) } else { - // No chown operation needed. + // No chown operation needed - file ownership comes from process naturally. return Ok(()); }; diff --git a/src/uu/install/src/mode.rs b/src/uu/install/src/mode.rs index 9a2fda317..96aae38c4 100644 --- a/src/uu/install/src/mode.rs +++ b/src/uu/install/src/mode.rs @@ -4,19 +4,8 @@ // file that was distributed with this source code. use std::fs; use std::path::Path; -#[cfg(not(windows))] -use uucore::mode; use uucore::translate; -/// Takes a user-supplied string and tries to parse to u16 mode bitmask. -pub fn parse(mode_string: &str, considering_dir: bool, umask: u32) -> Result { - if mode_string.chars().any(|c| c.is_ascii_digit()) { - mode::parse_numeric(0, mode_string, considering_dir) - } else { - mode::parse_symbolic(0, mode_string, umask, considering_dir) - } -} - /// chmod a file or directory on UNIX. /// /// Adapted from mkdir.rs. Handles own error printing. diff --git a/src/uu/join/BENCHMARKING.md b/src/uu/join/BENCHMARKING.md index 988259aa7..1698a2ff8 100644 --- a/src/uu/join/BENCHMARKING.md +++ b/src/uu/join/BENCHMARKING.md @@ -7,14 +7,14 @@ The amount of time spent in which part of the code can vary depending on the files being joined and the flags used. A benchmark with `-j` and `-i` shows the following time: -| Function/Method | Fraction of Samples | Why? | -| ---------------- | ------------------- | ---- | -| `Line::new` | 27% | Linear search for field separators, plus some vector operations. | -| `read_until` | 22% | Mostly libc reading file contents, with a few vector operations to represent them. | -| `Input::compare` | 20% | ~2/3 making the keys lowercase, ~1/3 comparing them. | -| `print_fields` | 11% | Writing to and flushing the buffer. | -| Other | 20% | | -| libc | 25% | I/O and memory allocation. | +| Function/Method | Fraction of Samples | Why? | +| ---------------- | ------------------- | ---------------------------------------------------------------------------------- | +| `Line::new` | 27% | Linear search for field separators, plus some vector operations. | +| `read_until` | 22% | Mostly libc reading file contents, with a few vector operations to represent them. | +| `Input::compare` | 20% | ~2/3 making the keys lowercase, ~1/3 comparing them. | +| `print_fields` | 11% | Writing to and flushing the buffer. | +| Other | 20% | | +| libc | 25% | I/O and memory allocation. | More detailed profiles can be obtained via [flame graphs](https://github.com/flamegraph-rs/flamegraph): diff --git a/src/uu/ln/locales/en-US.ftl b/src/uu/ln/locales/en-US.ftl index 85315070d..54755c7dc 100644 --- a/src/uu/ln/locales/en-US.ftl +++ b/src/uu/ln/locales/en-US.ftl @@ -35,4 +35,5 @@ ln-prompt-replace = replace {$file}? ln-cannot-backup = cannot backup {$file} ln-failed-to-access = failed to access {$file} ln-failed-to-create-hard-link = failed to create hard link {$source} => {$dest} +ln-failed-to-create-hard-link-dir = {$source}: hard link not allowed for directory ln-backup = backup: {$backup} diff --git a/src/uu/ln/locales/fr-FR.ftl b/src/uu/ln/locales/fr-FR.ftl index 483f15c92..f037528c6 100644 --- a/src/uu/ln/locales/fr-FR.ftl +++ b/src/uu/ln/locales/fr-FR.ftl @@ -36,4 +36,5 @@ ln-prompt-replace = remplacer {$file} ? ln-cannot-backup = impossible de sauvegarder {$file} ln-failed-to-access = échec d'accès à {$file} ln-failed-to-create-hard-link = échec de création du lien physique {$source} => {$dest} +ln-failed-to-create-hard-link-dir = {$source} : lien physique non autorisé pour un répertoire ln-backup = sauvegarde : {$backup} diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index a3fde8f4a..094106383 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -62,6 +62,9 @@ enum LnError { #[error("{}", translate!("ln-error-extra-operand", "operand" => _0.to_string_lossy(), "program" => _1.clone()))] ExtraOperand(OsString, String), + + #[error("{}", translate!("ln-failed-to-create-hard-link-dir", "source" => _0.to_string_lossy()))] + FailedToCreateHardLinkDir(PathBuf), } impl UError for LnError { @@ -431,6 +434,12 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> UResult<()> { if settings.symbolic { symlink(&source, dst)?; } else { + // Cannot create hard link to a directory directly + // We can however create hard link to a symlink that points to a directory, so long as -L is not passed + if src.is_dir() && (!src.is_symlink() || settings.logical) { + return Err(LnError::FailedToCreateHardLinkDir(source.to_path_buf()).into()); + } + let p = if settings.logical && source.is_symlink() { // if we want to have an hard link, // source is a symlink and -L is passed diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index 5fab67614..e6cd07fa4 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -36,7 +36,8 @@ uucore = { workspace = true, features = [ "fs", "fsext", "fsxattr", - "parser", + "parser-size", + "parser-glob", "quoting-style", "time", "version-cmp", diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 6f038142a..e66da6b6e 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2305,7 +2305,7 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool { #[allow(clippy::cognitive_complexity)] fn enter_directory( path_data: &PathData, - read_dir: ReadDir, + mut read_dir: ReadDir, config: &Config, state: &mut ListState, listed_ancestors: &mut HashSet, @@ -2334,7 +2334,7 @@ fn enter_directory( }; // Convert those entries to the PathData struct - for raw_entry in read_dir { + for raw_entry in read_dir.by_ref() { let dir_entry = match raw_entry { Ok(path) => path, Err(err) => { diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index a16be0c26..6ee610013 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -57,20 +57,11 @@ fn get_mode(_matches: &ArgMatches) -> Result { #[cfg(not(windows))] fn get_mode(matches: &ArgMatches) -> Result { // Not tested on Windows - let mut new_mode = DEFAULT_PERM; - if let Some(m) = matches.get_one::(options::MODE) { - for mode in m.split(',') { - if mode.chars().any(|c| c.is_ascii_digit()) { - new_mode = mode::parse_numeric(new_mode, m, true)?; - } else { - new_mode = mode::parse_symbolic(new_mode, mode, mode::get_umask(), true)?; - } - } - Ok(new_mode) + mode::parse_chmod(DEFAULT_PERM, m, true, mode::get_umask()) } else { // If no mode argument is specified return the mode derived from umask - Ok(!mode::get_umask() & 0o0777) + Ok(!mode::get_umask() & DEFAULT_PERM) } } diff --git a/src/uu/mkfifo/src/mkfifo.rs b/src/uu/mkfifo/src/mkfifo.rs index 572ea00b8..c55593dcb 100644 --- a/src/uu/mkfifo/src/mkfifo.rs +++ b/src/uu/mkfifo/src/mkfifo.rs @@ -119,19 +119,11 @@ pub fn uu_app() -> Command { fn calculate_mode(mode_option: Option<&String>) -> Result { let umask = uucore::mode::get_umask(); - let mut mode = 0o666; // Default mode for FIFOs + let mode = 0o666; // Default mode for FIFOs if let Some(m) = mode_option { - if m.chars().any(|c| c.is_ascii_digit()) { - mode = uucore::mode::parse_numeric(mode, m, false)?; - } else { - for item in m.split(',') { - mode = uucore::mode::parse_symbolic(mode, item, umask, false)?; - } - } + uucore::mode::parse_chmod(mode, m, false, umask) } else { - mode &= !umask; // Apply umask if no mode is specified + Ok(mode & !umask) // Apply umask if no mode is specified } - - Ok(mode) } diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index ca2640b68..cc22aee5f 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -225,8 +225,10 @@ pub fn uu_app() -> Command { ) } +#[allow(clippy::unnecessary_cast)] fn parse_mode(str_mode: &str) -> Result { - uucore::mode::parse_mode(str_mode) + let default_mode = (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) as u32; + uucore::mode::parse_chmod(default_mode, str_mode, true, uucore::mode::get_umask()) .map_err(|e| { translate!( "mknod-error-invalid-mode", @@ -237,7 +239,7 @@ fn parse_mode(str_mode: &str) -> Result { if mode > 0o777 { Err(translate!("mknod-error-mode-permission-bits-only")) } else { - Ok(mode) + Ok(mode as mode_t) } }) } diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs index e94fa847d..7d1f862aa 100644 --- a/src/uu/nl/src/nl.rs +++ b/src/uu/nl/src/nl.rs @@ -245,6 +245,7 @@ pub fn uu_app() -> Command { .after_help(translate!("nl-after-help")) .infer_long_args(true) .disable_help_flag(true) + .args_override_self(true) .arg( Arg::new(options::HELP) .long(options::HELP) diff --git a/src/uu/numfmt/src/format.rs b/src/uu/numfmt/src/format.rs index 816b2fda5..e091f2320 100644 --- a/src/uu/numfmt/src/format.rs +++ b/src/uu/numfmt/src/format.rs @@ -74,6 +74,7 @@ fn parse_suffix(s: &str) -> Result<(f64, Option)> { } let suffix = match iter.next_back() { Some('K') => Some((RawSuffix::K, with_i)), + Some('k') => Some((RawSuffix::K, with_i)), Some('M') => Some((RawSuffix::M, with_i)), Some('G') => Some((RawSuffix::G, with_i)), Some('T') => Some((RawSuffix::T, with_i)), @@ -81,8 +82,20 @@ fn parse_suffix(s: &str) -> Result<(f64, Option)> { Some('E') => Some((RawSuffix::E, with_i)), Some('Z') => Some((RawSuffix::Z, with_i)), Some('Y') => Some((RawSuffix::Y, with_i)), + Some('R') => Some((RawSuffix::R, with_i)), + Some('Q') => Some((RawSuffix::Q, with_i)), Some('0'..='9') if !with_i => None, _ => { + // If with_i is true, the string ends with 'i' but there's no valid suffix letter + // This is always an invalid suffix (e.g., "1i", "2Ai") + if with_i { + return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote())); + } + // For other cases, check if the number part (without the last character) is valid + let number_part = &s[..s.len() - 1]; + if number_part.is_empty() || number_part.parse::().is_err() { + return Err(translate!("numfmt-error-invalid-number", "input" => s.quote())); + } return Err(translate!("numfmt-error-invalid-suffix", "input" => s.quote())); } }; @@ -123,6 +136,8 @@ fn remove_suffix(i: f64, s: Option, u: &Unit) -> Result { RawSuffix::E => Ok(i * 1e18), RawSuffix::Z => Ok(i * 1e21), RawSuffix::Y => Ok(i * 1e24), + RawSuffix::R => Ok(i * 1e27), + RawSuffix::Q => Ok(i * 1e30), }, (Some((raw_suffix, false)), &Unit::Iec(false)) | (Some((raw_suffix, true)), &Unit::Auto | &Unit::Iec(true)) => match raw_suffix { @@ -134,6 +149,8 @@ fn remove_suffix(i: f64, s: Option, u: &Unit) -> Result { RawSuffix::E => Ok(i * IEC_BASES[6]), RawSuffix::Z => Ok(i * IEC_BASES[7]), RawSuffix::Y => Ok(i * IEC_BASES[8]), + RawSuffix::R => Ok(i * IEC_BASES[9]), + RawSuffix::Q => Ok(i * IEC_BASES[10]), }, (Some((raw_suffix, false)), &Unit::Iec(true)) => Err( translate!("numfmt-error-missing-i-suffix", "number" => i, "suffix" => format!("{raw_suffix:?}")), @@ -212,10 +229,10 @@ fn consider_suffix( round_method: RoundMethod, precision: usize, ) -> Result<(f64, Option)> { - use crate::units::RawSuffix::{E, G, K, M, P, T, Y, Z}; + use crate::units::RawSuffix::{E, G, K, M, P, Q, R, T, Y, Z}; let abs_n = n.abs(); - let suffixes = [K, M, G, T, P, E, Z, Y]; + let suffixes = [K, M, G, T, P, E, Z, Y, R, Q]; let (bases, with_i) = match *u { Unit::Si => (&SI_BASES, false), @@ -234,6 +251,8 @@ fn consider_suffix( _ if abs_n < bases[7] => 6, _ if abs_n < bases[8] => 7, _ if abs_n < bases[9] => 8, + _ if abs_n < bases[10] => 9, + _ if abs_n < bases[10] * 1000.0 => 10, _ => return Err(translate!("numfmt-error-number-too-big")), }; @@ -334,38 +353,41 @@ fn format_string( fn format_and_print_delimited(s: &str, options: &NumfmtOptions) -> Result<()> { let delimiter = options.delimiter.as_ref().unwrap(); + let mut output = String::new(); for (n, field) in (1..).zip(s.split(delimiter)) { let field_selected = uucore::ranges::contain(&options.fields, n); - // print delimiter before second and subsequent fields + // add delimiter before second and subsequent fields if n > 1 { - print!("{delimiter}"); + output.push_str(delimiter); } if field_selected { - print!("{}", format_string(field.trim_start(), options, None)?); + output.push_str(&format_string(field.trim_start(), options, None)?); } else { - // print unselected field without conversion - print!("{field}"); + // add unselected field without conversion + output.push_str(field); } } - println!(); + println!("{output}"); Ok(()) } fn format_and_print_whitespace(s: &str, options: &NumfmtOptions) -> Result<()> { + let mut output = String::new(); + for (n, (prefix, field)) in (1..).zip(WhitespaceSplitter { s: Some(s) }) { let field_selected = uucore::ranges::contain(&options.fields, n); if field_selected { let empty_prefix = prefix.is_empty(); - // print delimiter before second and subsequent fields + // add delimiter before second and subsequent fields let prefix = if n > 1 { - print!(" "); + output.push(' '); &prefix[1..] } else { prefix @@ -377,22 +399,24 @@ fn format_and_print_whitespace(s: &str, options: &NumfmtOptions) -> Result<()> { None }; - print!("{}", format_string(field, options, implicit_padding)?); + output.push_str(&format_string(field, options, implicit_padding)?); } else { // the -z option converts an initial \n into a space let prefix = if options.zero_terminated && prefix.starts_with('\n') { - print!(" "); + output.push(' '); &prefix[1..] } else { prefix }; - // print unselected field without conversion - print!("{prefix}{field}"); + // add unselected field without conversion + output.push_str(prefix); + output.push_str(field); } } let eol = if options.zero_terminated { '\0' } else { '\n' }; - print!("{eol}"); + output.push(eol); + print!("{output}"); Ok(()) } @@ -445,4 +469,123 @@ mod tests { assert_eq!(2, parse_implicit_precision("1.23K")); assert_eq!(3, parse_implicit_precision("1.234K")); } + + #[test] + fn test_parse_suffix_q_r_k() { + let result = parse_suffix("1Q"); + assert!(result.is_ok()); + let (number, suffix) = result.unwrap(); + assert_eq!(number, 1.0); + assert!(suffix.is_some()); + let (raw_suffix, with_i) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); + assert!(!with_i); + + let result = parse_suffix("2R"); + assert!(result.is_ok()); + let (number, suffix) = result.unwrap(); + assert_eq!(number, 2.0); + assert!(suffix.is_some()); + let (raw_suffix, with_i) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::R as i32); + assert!(!with_i); + + let result = parse_suffix("3k"); + assert!(result.is_ok()); + let (number, suffix) = result.unwrap(); + assert_eq!(number, 3.0); + assert!(suffix.is_some()); + let (raw_suffix, with_i) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::K as i32); + assert!(!with_i); + + let result = parse_suffix("4Qi"); + assert!(result.is_ok()); + let (number, suffix) = result.unwrap(); + assert_eq!(number, 4.0); + assert!(suffix.is_some()); + let (raw_suffix, with_i) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); + assert!(with_i); + + let result = parse_suffix("5Ri"); + assert!(result.is_ok()); + let (number, suffix) = result.unwrap(); + assert_eq!(number, 5.0); + assert!(suffix.is_some()); + let (raw_suffix, with_i) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::R as i32); + assert!(with_i); + } + + #[test] + fn test_parse_suffix_error_messages() { + let result = parse_suffix("foo"); + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(error.contains("numfmt-error-invalid-number") || error.contains("invalid number")); + assert!(!error.contains("invalid suffix")); + + let result = parse_suffix("World"); + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(error.contains("numfmt-error-invalid-number") || error.contains("invalid number")); + assert!(!error.contains("invalid suffix")); + + let result = parse_suffix("123i"); + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(error.contains("numfmt-error-invalid-suffix") || error.contains("invalid suffix")); + } + + #[test] + fn test_remove_suffix_q_r() { + use crate::units::Unit; + + let result = remove_suffix(1.0, Some((RawSuffix::Q, false)), &Unit::Si); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1e30); + + let result = remove_suffix(1.0, Some((RawSuffix::R, false)), &Unit::Si); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1e27); + + let result = remove_suffix(1.0, Some((RawSuffix::Q, true)), &Unit::Iec(true)); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), IEC_BASES[10]); + + let result = remove_suffix(1.0, Some((RawSuffix::R, true)), &Unit::Iec(true)); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), IEC_BASES[9]); + } + + #[test] + fn test_consider_suffix_q_r() { + use crate::options::RoundMethod; + use crate::units::Unit; + + let result = consider_suffix(1e27, &Unit::Si, RoundMethod::FromZero, 0); + assert!(result.is_ok()); + let (value, suffix) = result.unwrap(); + assert!(suffix.is_some()); + let (raw_suffix, _) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::R as i32); + assert_eq!(value, 1.0); + + let result = consider_suffix(1e30, &Unit::Si, RoundMethod::FromZero, 0); + assert!(result.is_ok()); + let (value, suffix) = result.unwrap(); + assert!(suffix.is_some()); + let (raw_suffix, _) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); + assert_eq!(value, 1.0); + + let result = consider_suffix(5e30, &Unit::Si, RoundMethod::FromZero, 0); + assert!(result.is_ok()); + let (value, suffix) = result.unwrap(); + assert!(suffix.is_some()); + let (raw_suffix, _) = suffix.unwrap(); + assert_eq!(raw_suffix as i32, RawSuffix::Q as i32); + assert_eq!(value, 5.0); + } } diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index f472b3593..abeaca256 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -460,9 +460,9 @@ mod tests { let result_display = format!("{result}"); assert_eq!( result_debug, - "FormattingError(\"numfmt-error-invalid-suffix\")" + "FormattingError(\"numfmt-error-invalid-number\")" ); - assert_eq!(result_display, "numfmt-error-invalid-suffix"); + assert_eq!(result_display, "numfmt-error-invalid-number"); assert_eq!(result.code(), 2); } diff --git a/src/uu/numfmt/src/units.rs b/src/uu/numfmt/src/units.rs index c52dee20c..bc5d480be 100644 --- a/src/uu/numfmt/src/units.rs +++ b/src/uu/numfmt/src/units.rs @@ -4,9 +4,9 @@ // file that was distributed with this source code. use std::fmt; -pub const SI_BASES: [f64; 10] = [1., 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21, 1e24, 1e27]; +pub const SI_BASES: [f64; 11] = [1., 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21, 1e24, 1e27, 1e30]; -pub const IEC_BASES: [f64; 10] = [ +pub const IEC_BASES: [f64; 11] = [ 1., 1_024., 1_048_576., @@ -17,6 +17,7 @@ pub const IEC_BASES: [f64; 10] = [ 1_180_591_620_717_411_303_424., 1_208_925_819_614_629_174_706_176., 1_237_940_039_285_380_274_899_124_224., + 1_267_650_600_228_229_401_496_703_205_376., ]; pub type WithI = bool; @@ -41,6 +42,8 @@ pub enum RawSuffix { E, Z, Y, + R, + Q, } pub type Suffix = (RawSuffix, WithI); @@ -60,6 +63,8 @@ impl fmt::Display for DisplayableSuffix { (RawSuffix::E, _) => write!(f, "E"), (RawSuffix::Z, _) => write!(f, "Z"), (RawSuffix::Y, _) => write!(f, "Y"), + (RawSuffix::R, _) => write!(f, "R"), + (RawSuffix::Q, _) => write!(f, "Q"), } .and_then(|()| match with_i { true => write!(f, "i"), diff --git a/src/uu/od/Cargo.toml b/src/uu/od/Cargo.toml index a2cd59a63..13d12a413 100644 --- a/src/uu/od/Cargo.toml +++ b/src/uu/od/Cargo.toml @@ -21,8 +21,9 @@ path = "src/od.rs" byteorder = { workspace = true } clap = { workspace = true } half = { workspace = true } -uucore = { workspace = true, features = ["parser"] } +uucore = { workspace = true, features = ["parser-size"] } fluent = { workspace = true } +libc.workspace = true [[bin]] name = "od" diff --git a/src/uu/od/locales/en-US.ftl b/src/uu/od/locales/en-US.ftl index 208bd3333..bcafe1fe2 100644 --- a/src/uu/od/locales/en-US.ftl +++ b/src/uu/od/locales/en-US.ftl @@ -55,9 +55,10 @@ od-error-invalid-offset = invalid offset: {$offset} od-error-invalid-label = invalid label: {$label} od-error-too-many-inputs = too many inputs after --traditional: {$input} od-error-parse-failed = parse failed -od-error-invalid-suffix = invalid suffix in --{$option} argument {$value} -od-error-invalid-argument = invalid --{$option} argument {$value} -od-error-argument-too-large = --{$option} argument {$value} too large +od-error-overflow = Numerical result out of range +od-error-invalid-suffix = invalid suffix in {$option} argument {$value} +od-error-invalid-argument = invalid {$option} argument {$value} +od-error-argument-too-large = {$option} argument {$value} too large od-error-skip-past-end = tried to skip past end of input # Help messages diff --git a/src/uu/od/locales/fr-FR.ftl b/src/uu/od/locales/fr-FR.ftl index cba433b64..df07eebe6 100644 --- a/src/uu/od/locales/fr-FR.ftl +++ b/src/uu/od/locales/fr-FR.ftl @@ -56,9 +56,9 @@ od-error-invalid-offset = décalage invalide : {$offset} od-error-invalid-label = étiquette invalide : {$label} od-error-too-many-inputs = trop d'entrées après --traditional : {$input} od-error-parse-failed = échec de l'analyse -od-error-invalid-suffix = suffixe invalide dans l'argument --{$option} {$value} -od-error-invalid-argument = argument --{$option} invalide {$value} -od-error-argument-too-large = argument --{$option} {$value} trop grand +od-error-invalid-suffix = suffixe invalide dans l'argument {$option} {$value} +od-error-invalid-argument = argument {$option} invalide {$value} +od-error-argument-too-large = argument {$option} {$value} trop grand od-error-skip-past-end = tentative d'ignorer au-delà de la fin de l'entrée # Messages d'aide diff --git a/src/uu/od/src/byteorder_io.rs b/src/uu/od/src/byteorder_io.rs index 545016ff3..8cc7a8bac 100644 --- a/src/uu/od/src/byteorder_io.rs +++ b/src/uu/od/src/byteorder_io.rs @@ -52,5 +52,6 @@ gen_byte_order_ops! { read_i32, write_i32 -> i32, read_i64, write_i64 -> i64, read_f32, write_f32 -> f32, - read_f64, write_f64 -> f64 + read_f64, write_f64 -> f64, + read_u128, write_u128 -> u128 } diff --git a/src/uu/od/src/formatter_item_info.rs b/src/uu/od/src/formatter_item_info.rs index e530a0a3e..472c9fc4e 100644 --- a/src/uu/od/src/formatter_item_info.rs +++ b/src/uu/od/src/formatter_item_info.rs @@ -12,6 +12,7 @@ use std::fmt; pub enum FormatWriter { IntWriter(fn(u64) -> String), FloatWriter(fn(f64) -> String), + LongDoubleWriter(fn(f64) -> String), // On most platforms, long double is f64 or emulated BFloatWriter(fn(f64) -> String), MultibyteWriter(fn(&[u8]) -> String), } @@ -27,6 +28,10 @@ impl fmt::Debug for FormatWriter { f.write_str("FloatWriter:")?; fmt::Pointer::fmt(p, f) } + Self::LongDoubleWriter(ref p) => { + f.write_str("LongDoubleWriter:")?; + fmt::Pointer::fmt(p, f) + } Self::BFloatWriter(ref p) => { f.write_str("BFloatWriter:")?; fmt::Pointer::fmt(p, f) diff --git a/src/uu/od/src/input_decoder.rs b/src/uu/od/src/input_decoder.rs index a65e7613b..416badb44 100644 --- a/src/uu/od/src/input_decoder.rs +++ b/src/uu/od/src/input_decoder.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 bfloat multifile +// spell-checker:ignore bfloat multifile mant use half::{bf16, f16}; use std::io; @@ -165,6 +165,61 @@ impl MemoryDecoder<'_> { let val = f32::from(bf16::from_bits(bits)); f64::from(val) } + + /// Returns a long double from the internal buffer at position `start`. + /// We read 16 bytes as u128 (respecting endianness) and convert to f64. + /// This ensures that endianness swapping works correctly even if we lose precision. + pub fn read_long_double(&self, start: usize) -> f64 { + let bits = self.byte_order.read_u128(&self.data[start..start + 16]); + u128_to_f64(bits) + } +} + +fn u128_to_f64(u: u128) -> f64 { + let sign = (u >> 127) as u64; + let exp = ((u >> 112) & 0x7FFF) as u64; + let mant = u & ((1 << 112) - 1); + + if exp == 0x7FFF { + // Infinity or NaN + if mant == 0 { + if sign == 0 { + f64::INFINITY + } else { + f64::NEG_INFINITY + } + } else { + f64::NAN + } + } else if exp == 0 { + // Subnormal or zero + if mant == 0 { + if sign == 0 { 0.0 } else { -0.0 } + } else { + // Subnormal f128 is too small for f64, flush to zero + if sign == 0 { 0.0 } else { -0.0 } + } + } else { + // Normal + let new_exp = exp as i64 - 16383 + 1023; + if new_exp >= 2047 { + // Overflow to infinity + if sign == 0 { + f64::INFINITY + } else { + f64::NEG_INFINITY + } + } else if new_exp <= 0 { + // Underflow to zero + if sign == 0 { 0.0 } else { -0.0 } + } else { + // Normal f64 + // Mantissa: take top 52 bits of 112-bit mantissa + let new_mant = (mant >> (112 - 52)) as u64; + let bits = (sign << 63) | ((new_exp as u64) << 52) | new_mant; + f64::from_bits(bits) + } + } } #[cfg(test)] diff --git a/src/uu/od/src/multifile_reader.rs b/src/uu/od/src/multifile_reader.rs index 7d4709ce1..48e1f1225 100644 --- a/src/uu/od/src/multifile_reader.rs +++ b/src/uu/od/src/multifile_reader.rs @@ -87,7 +87,13 @@ impl MultifileReader<'_> { // print an error at the time that the file is needed, // then move to the next file. // This matches the behavior of the original `od` - show_error!("{}: {e}", fname.maybe_quote()); + // Format error without OS error code to match GNU od + let error_msg = match e.kind() { + io::ErrorKind::NotFound => "No such file or directory", + io::ErrorKind::PermissionDenied => "Permission denied", + _ => "I/O error", + }; + show_error!("{}: {}", fname.maybe_quote().external(true), error_msg); self.any_err = true; } } diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 80e6893d1..e8f9841b1 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -80,14 +80,19 @@ struct OdOptions { } /// Helper function to parse bytes with error handling -fn parse_bytes_option(matches: &ArgMatches, option_name: &str) -> UResult> { +fn parse_bytes_option( + matches: &ArgMatches, + args: &[String], + option_name: &str, + short: Option, +) -> UResult> { match matches.get_one::(option_name) { None => Ok(None), Some(s) => match parse_number_of_bytes(s) { Ok(n) => Ok(Some(n)), Err(e) => Err(USimpleError::new( 1, - format_error_message(&e, s, option_name), + format_error_message(&e, s, &option_display_name(args, option_name, short)), )), }, } @@ -110,12 +115,12 @@ impl OdOptions { ByteOrder::Native }; - let mut skip_bytes = parse_bytes_option(matches, options::SKIP_BYTES)?.unwrap_or(0); + let mut skip_bytes = + parse_bytes_option(matches, args, options::SKIP_BYTES, Some('j'))?.unwrap_or(0); let mut label: Option = None; - let parsed_input = parse_inputs(matches) - .map_err(|e| USimpleError::new(1, translate!("od-error-invalid-inputs", "msg" => e)))?; + let parsed_input = parse_inputs(matches).map_err(|e| USimpleError::new(1, e))?; let input_strings = match parsed_input { CommandLineInputs::FileNames(v) => v, CommandLineInputs::FileAndOffset((f, s, l)) => { @@ -131,16 +136,30 @@ impl OdOptions { None => 16, Some(s) => { if matches.value_source(options::WIDTH) == Some(ValueSource::CommandLine) { - match parse_number_of_bytes(s) { - Ok(n) => usize::try_from(n) - .map_err(|_| USimpleError::new(1, format!("‘{s}‘ is too large")))?, - Err(e) => { - return Err(USimpleError::new( - 1, - format_error_message(&e, s, options::WIDTH), - )); - } + let width_display = option_display_name(args, options::WIDTH, Some('w')); + let parsed = parse_number_of_bytes(s).map_err(|e| { + USimpleError::new(1, format_error_message(&e, s, &width_display)) + })?; + if parsed == 0 { + return Err(USimpleError::new( + 1, + translate!( + "od-error-invalid-argument", + "option" => width_display.clone(), + "value" => s.quote() + ), + )); } + usize::try_from(parsed).map_err(|_| { + USimpleError::new( + 1, + translate!( + "od-error-argument-too-large", + "option" => width_display.clone(), + "value" => s.quote() + ), + ) + })? } else { 16 } @@ -160,9 +179,9 @@ impl OdOptions { let output_duplicates = matches.get_flag(options::OUTPUT_DUPLICATES); - let read_bytes = parse_bytes_option(matches, options::READ_BYTES)?; + let read_bytes = parse_bytes_option(matches, args, options::READ_BYTES, Some('N'))?; - let string_min_length = match parse_bytes_option(matches, options::STRINGS)? { + let string_min_length = match parse_bytes_option(matches, args, options::STRINGS, Some('S'))? { None => None, Some(n) => Some(usize::try_from(n).map_err(|_| { USimpleError::new( @@ -491,7 +510,9 @@ where let length = memory_decoder.length(); if length == 0 { - input_offset.print_final_offset(); + if !input_decoder.has_error() { + input_offset.print_final_offset(); + } break; } @@ -669,6 +690,10 @@ fn print_bytes(prefix: &str, input_decoder: &MemoryDecoder, output_info: &Output let p = input_decoder.read_float(b, f.formatter_item_info.byte_size); output_text.push_str(&func(p)); } + FormatWriter::LongDoubleWriter(func) => { + let p = input_decoder.read_long_double(b); + output_text.push_str(&func(p)); + } FormatWriter::BFloatWriter(func) => { let p = input_decoder.read_bfloat(b); output_text.push_str(&func(p)); @@ -745,6 +770,27 @@ impl HasError for BufReader { } } +fn option_display_name(args: &[String], option_name: &str, short: Option) -> String { + let long_form = format!("--{option_name}"); + let long_form_with_eq = format!("{long_form}="); + if let Some(short_char) = short { + let short_form = format!("-{short_char}"); + for arg in args.iter().skip(1) { + if !arg.starts_with("--") && arg.starts_with(&short_form) { + return short_form; + } + } + for arg in args.iter().skip(1) { + if arg == &long_form || arg.starts_with(&long_form_with_eq) { + return long_form; + } + } + short_form + } else { + long_form + } +} + fn format_error_message(error: &ParseSizeError, s: &str, option: &str) -> String { // NOTE: // GNU's od echos affected flag, -N or --read-bytes (-j or --skip-bytes, etc.), depending user's selection diff --git a/src/uu/od/src/output_info.rs b/src/uu/od/src/output_info.rs index 38218cde8..ef63c1602 100644 --- a/src/uu/od/src/output_info.rs +++ b/src/uu/od/src/output_info.rs @@ -11,7 +11,7 @@ use crate::formatter_item_info::FormatterItemInfo; use crate::parse_formats::ParsedFormatterItemInfo; /// Size in bytes of the max datatype. ie set to 16 for 128-bit numbers. -const MAX_BYTES_PER_UNIT: usize = 8; +const MAX_BYTES_PER_UNIT: usize = 16; /// Contains information to output single output line in human readable form pub struct SpacedFormatterItemInfo { @@ -204,6 +204,36 @@ impl TypeSizeInfo for TypeInfo { } } +#[cfg(test)] +fn assert_alignment( + expected: &[usize], + type_info: TypeInfo, + byte_size_block: usize, + print_width_block: usize, +) { + assert_eq!( + expected.len(), + byte_size_block, + "expected spacing must describe every byte in the block" + ); + + let spacing = OutputInfo::calculate_alignment(&type_info, byte_size_block, print_width_block); + + assert_eq!( + expected, + &spacing[..byte_size_block], + "unexpected spacing for byte_size={} print_width={} block_width={}", + type_info.byte_size, + type_info.print_width, + print_width_block + ); + assert!( + spacing[byte_size_block..].iter().all(|&s| s == 0), + "spacing beyond the active block should remain zero: {:?}", + &spacing[byte_size_block..] + ); +} + #[test] #[allow(clippy::cognitive_complexity)] fn test_calculate_alignment() { @@ -213,40 +243,34 @@ fn test_calculate_alignment() { // ffff ffff ffff ffff ffff ffff ffff ffff // the first line has no additional spacing: - assert_eq!( - [0, 0, 0, 0, 0, 0, 0, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 8, - print_width: 23, - }, - 8, - 23 - ) + assert_alignment( + &[0, 0, 0, 0, 0, 0, 0, 0], + TypeInfo { + byte_size: 8, + print_width: 23, + }, + 8, + 23, ); // the second line a single space at the start of the block: - assert_eq!( - [1, 0, 0, 0, 0, 0, 0, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 4, - print_width: 11, - }, - 8, - 23 - ) + assert_alignment( + &[1, 0, 0, 0, 0, 0, 0, 0], + TypeInfo { + byte_size: 4, + print_width: 11, + }, + 8, + 23, ); // the third line two spaces at pos 0, and 1 space at pos 4: - assert_eq!( - [2, 0, 0, 0, 1, 0, 0, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 2, - print_width: 5, - }, - 8, - 23 - ) + assert_alignment( + &[2, 0, 0, 0, 1, 0, 0, 0], + TypeInfo { + byte_size: 2, + print_width: 5, + }, + 8, + 23, ); // For this example `byte_size_block` is 8 and 'print_width_block' is 28: @@ -255,195 +279,161 @@ fn test_calculate_alignment() { // 177777 177777 177777 177777 177777 177777 177777 177777 // ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff - assert_eq!( - [7, 0, 0, 0, 0, 0, 0, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 8, - print_width: 21, - }, - 8, - 28 - ) + assert_alignment( + &[7, 0, 0, 0, 0, 0, 0, 0], + TypeInfo { + byte_size: 8, + print_width: 21, + }, + 8, + 28, ); - assert_eq!( - [5, 0, 0, 0, 5, 0, 0, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 4, - print_width: 9, - }, - 8, - 28 - ) + assert_alignment( + &[5, 0, 0, 0, 5, 0, 0, 0], + TypeInfo { + byte_size: 4, + print_width: 9, + }, + 8, + 28, ); - assert_eq!( - [0, 0, 0, 0, 0, 0, 0, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 2, - print_width: 7, - }, - 8, - 28 - ) + assert_alignment( + &[0, 0, 0, 0, 0, 0, 0, 0], + TypeInfo { + byte_size: 2, + print_width: 7, + }, + 8, + 28, ); - assert_eq!( - [1, 0, 1, 0, 1, 0, 1, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 3, - }, - 8, - 28 - ) + assert_alignment( + &[1, 0, 1, 0, 1, 0, 1, 0], + TypeInfo { + byte_size: 1, + print_width: 3, + }, + 8, + 28, ); // 9 tests where 8 .. 16 spaces are spread across 8 positions - assert_eq!( - [1, 1, 1, 1, 1, 1, 1, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 8 - ) + assert_alignment( + &[1, 1, 1, 1, 1, 1, 1, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 8, ); - assert_eq!( - [2, 1, 1, 1, 1, 1, 1, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 9 - ) + assert_alignment( + &[2, 1, 1, 1, 1, 1, 1, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 9, ); - assert_eq!( - [2, 1, 1, 1, 2, 1, 1, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 10 - ) + assert_alignment( + &[2, 1, 1, 1, 2, 1, 1, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 10, ); - assert_eq!( - [3, 1, 1, 1, 2, 1, 1, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 11 - ) + assert_alignment( + &[3, 1, 1, 1, 2, 1, 1, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 11, ); - assert_eq!( - [2, 1, 2, 1, 2, 1, 2, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 12 - ) + assert_alignment( + &[2, 1, 2, 1, 2, 1, 2, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 12, ); - assert_eq!( - [3, 1, 2, 1, 2, 1, 2, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 13 - ) + assert_alignment( + &[3, 1, 2, 1, 2, 1, 2, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 13, ); - assert_eq!( - [3, 1, 2, 1, 3, 1, 2, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 14 - ) + assert_alignment( + &[3, 1, 2, 1, 3, 1, 2, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 14, ); - assert_eq!( - [4, 1, 2, 1, 3, 1, 2, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 15 - ) + assert_alignment( + &[4, 1, 2, 1, 3, 1, 2, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 15, ); - assert_eq!( - [2, 2, 2, 2, 2, 2, 2, 2], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 16 - ) + assert_alignment( + &[2, 2, 2, 2, 2, 2, 2, 2], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 16, ); // 4 tests where 15 spaces are spread across 8, 4, 2 or 1 position(s) - assert_eq!( - [4, 1, 2, 1, 3, 1, 2, 1], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 1, - print_width: 2, - }, - 8, - 16 + 15 - ) + assert_alignment( + &[4, 1, 2, 1, 3, 1, 2, 1], + TypeInfo { + byte_size: 1, + print_width: 2, + }, + 8, + 16 + 15, ); - assert_eq!( - [5, 0, 3, 0, 4, 0, 3, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 2, - print_width: 4, - }, - 8, - 16 + 15 - ) + assert_alignment( + &[5, 0, 3, 0, 4, 0, 3, 0], + TypeInfo { + byte_size: 2, + print_width: 4, + }, + 8, + 16 + 15, ); - assert_eq!( - [8, 0, 0, 0, 7, 0, 0, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 4, - print_width: 8, - }, - 8, - 16 + 15 - ) + assert_alignment( + &[8, 0, 0, 0, 7, 0, 0, 0], + TypeInfo { + byte_size: 4, + print_width: 8, + }, + 8, + 16 + 15, ); - assert_eq!( - [15, 0, 0, 0, 0, 0, 0, 0], - OutputInfo::calculate_alignment( - &TypeInfo { - byte_size: 8, - print_width: 16, - }, - 8, - 16 + 15 - ) + assert_alignment( + &[15, 0, 0, 0, 0, 0, 0, 0], + TypeInfo { + byte_size: 8, + print_width: 16, + }, + 8, + 16 + 15, ); } diff --git a/src/uu/od/src/parse_formats.rs b/src/uu/od/src/parse_formats.rs index a62adc1ad..0edb48474 100644 --- a/src/uu/od/src/parse_formats.rs +++ b/src/uu/od/src/parse_formats.rs @@ -81,6 +81,7 @@ fn od_format_type(type_char: FormatType, byte_size: u8) -> Option Some(FORMAT_ITEM_F16), (FormatType::Float, 0 | 4) => Some(FORMAT_ITEM_F32), (FormatType::Float, 8) => Some(FORMAT_ITEM_F64), + (FormatType::Float, 16) => Some(FORMAT_ITEM_LONG_DOUBLE), _ => None, } @@ -238,7 +239,10 @@ fn is_format_size_char( *byte_size = 2; true } - // FormatTypeCategory::Float, 'L' => *byte_size = 16, // TODO support f128 + (FormatTypeCategory::Float, Some('L')) => { + *byte_size = 16; + true + } _ => false, } } diff --git a/src/uu/od/src/parse_inputs.rs b/src/uu/od/src/parse_inputs.rs index b185e5427..8f5e6434b 100644 --- a/src/uu/od/src/parse_inputs.rs +++ b/src/uu/od/src/parse_inputs.rs @@ -69,17 +69,30 @@ pub fn parse_inputs(matches: &dyn CommandLineOpts) -> Result { + // if there is just 1 input (stdin), an offset must start with '+' + if input_strings.len() == 1 && input_strings[0].starts_with('+') { + return Ok(CommandLineInputs::FileAndOffset(("-".to_string(), n, None))); + } + if input_strings.len() == 2 { + return Ok(CommandLineInputs::FileAndOffset(( + input_strings[0].to_string(), + n, + None, + ))); + } } - if input_strings.len() == 2 { - return Ok(CommandLineInputs::FileAndOffset(( - input_strings[0].to_string(), - n, - None, - ))); + Err(e) => { + // If it's an overflow error, propagate it + // Otherwise, treat it as a filename + let err = std::io::Error::from_raw_os_error(libc::ERANGE); + let msg = err.to_string(); + let expected_msg = msg.split(" (os error").next().unwrap_or(&msg).to_string(); + + if e == expected_msg { + return Err(format!("{}: {}", input_strings[input_strings.len() - 1], e)); + } } } } @@ -123,7 +136,7 @@ pub fn parse_inputs_traditional(input_strings: &[&str]) -> Result Err(translate!("od-error-invalid-offset", "offset" => input_strings[1])), + (_, Err(e)) => Err(format!("{}: {}", input_strings[1], e)), } } 3 => { @@ -135,12 +148,8 @@ pub fn parse_inputs_traditional(input_strings: &[&str]) -> Result { - Err(translate!("od-error-invalid-offset", "offset" => input_strings[1])) - } - (_, Err(_)) => { - Err(translate!("od-error-invalid-label", "label" => input_strings[2])) - } + (Err(e), _) => Err(format!("{}: {}", input_strings[1], e)), + (_, Err(e)) => Err(format!("{}: {}", input_strings[2], e)), } } _ => Err(translate!("od-error-too-many-inputs", "input" => input_strings[3])), @@ -148,7 +157,24 @@ pub fn parse_inputs_traditional(input_strings: &[&str]) -> Result Result { +pub fn parse_offset_operand(s: &str) -> Result { + if s.is_empty() { + return Err(translate!("od-error-parse-failed")); + } + + if s.contains(' ') { + return Err(translate!("od-error-parse-failed")); + } + + if s.starts_with("++") || s.starts_with("+-") { + return Err(translate!("od-error-parse-failed")); + } + + // Reject strings starting with "-" (negative numbers not allowed) + if s.starts_with('-') { + return Err(translate!("od-error-parse-failed")); + } + let mut start = 0; let mut len = s.len(); let mut radix = 8; @@ -171,9 +197,40 @@ pub fn parse_offset_operand(s: &str) -> Result { radix = 10; } } + + // Check if the substring is empty after processing prefixes/suffixes + if start >= len { + return Err(translate!("od-error-parse-failed")); + } + match u64::from_str_radix(&s[start..len], radix) { - Ok(i) => Ok(i * multiply), - Err(_) => Err(translate!("od-error-parse-failed").leak()), + Ok(i) => { + // Check for overflow during multiplication + match i.checked_mul(multiply) { + Some(result) => Ok(result), + None => { + let err = std::io::Error::from_raw_os_error(libc::ERANGE); + let msg = err.to_string(); + // Strip "(os error N)" if present to match Perl's $! + let msg = msg.split(" (os error").next().unwrap_or(&msg).to_string(); + Err(msg) + } + } + } + Err(e) => { + // Distinguish between overflow and parse failure + // from_str_radix returns IntErrorKind::PosOverflow for overflow + use std::num::IntErrorKind; + match e.kind() { + IntErrorKind::PosOverflow => { + let err = std::io::Error::from_raw_os_error(libc::ERANGE); + let msg = err.to_string(); + let msg = msg.split(" (os error").next().unwrap_or(&msg).to_string(); + Err(msg) + } + _ => Err(translate!("od-error-parse-failed")), + } + } } } @@ -340,7 +397,7 @@ mod tests { .unwrap_err(); } - fn parse_offset_operand_str(s: &str) -> Result { + fn parse_offset_operand_str(s: &str) -> Result { parse_offset_operand(&String::from(s)) } diff --git a/src/uu/od/src/prn_float.rs b/src/uu/od/src/prn_float.rs index 2e1ff6988..c93b02d25 100644 --- a/src/uu/od/src/prn_float.rs +++ b/src/uu/od/src/prn_float.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use half::f16; +use half::{bf16, f16}; use std::num::FpCategory; use crate::formatter_item_info::{FormatWriter, FormatterItemInfo}; @@ -25,14 +25,73 @@ pub static FORMAT_ITEM_F64: FormatterItemInfo = FormatterItemInfo { formatter: FormatWriter::FloatWriter(format_item_f64), }; +pub static FORMAT_ITEM_LONG_DOUBLE: FormatterItemInfo = FormatterItemInfo { + byte_size: 16, + print_width: 40, + formatter: FormatWriter::LongDoubleWriter(format_item_long_double), +}; + pub static FORMAT_ITEM_BF16: FormatterItemInfo = FormatterItemInfo { byte_size: 2, print_width: 16, formatter: FormatWriter::BFloatWriter(format_item_bf16), }; +/// Clean up a normalized float string by removing unnecessary padding and digits. +/// - Strip leading spaces. +/// - Trim trailing zeros after the decimal point (and the dot itself if empty). +/// - Leave the exponent part (e/E...) untouched. +fn trim_float_repr(raw: &str) -> String { + // Drop padding added by `format!` width specification + let mut s = raw.trim_start().to_string(); + + // Keep NaN/Inf representations as-is + let lower = s.to_ascii_lowercase(); + if lower == "nan" || lower == "inf" || lower == "-inf" { + return s; + } + + // Separate exponent from mantissa + let mut exp_part = String::new(); + if let Some(idx) = s.find(['e', 'E']) { + exp_part = s[idx..].to_string(); + s.truncate(idx); + } + + // Trim trailing zeros in mantissa, then remove trailing dot if left alone + if s.contains('.') { + while s.ends_with('0') { + s.pop(); + } + if s.ends_with('.') { + s.pop(); + } + } + + // If everything was trimmed, leave a single zero + if s.is_empty() || s == "-" || s == "+" { + s.push('0'); + } + + s.push_str(&exp_part); + s +} + +/// Pad a floating value to a fixed width for column alignment while keeping +/// the original precision (including trailing zeros). This mirrors the +/// behavior of other float formatters (`f32`, `f64`) and keeps the output +/// stable across platforms. +fn pad_float_repr(raw: &str, width: usize) -> String { + format!("{raw:>width$}") +} + pub fn format_item_f16(f: f64) -> String { - format!(" {}", format_f16(f16::from_f64(f))) + let value = f16::from_f64(f); + let width = FORMAT_ITEM_F16.print_width - 1; + // Format once, trim redundant zeros, then re-pad to the canonical width + let raw = format_f16(value); + let trimmed = trim_float_repr(&raw); + format!(" {}", pad_float_repr(&trimmed, width)) } pub fn format_item_f32(f: f64) -> String { @@ -43,6 +102,10 @@ pub fn format_item_f64(f: f64) -> String { format!(" {}", format_f64(f)) } +pub fn format_item_long_double(f: f64) -> String { + format!(" {}", format_long_double(f)) +} + fn format_f32_exp(f: f32, width: usize) -> String { if f.abs().log10() < 0.0 { return format!("{f:width$e}"); @@ -71,11 +134,33 @@ fn format_f64_exp_precision(f: f64, width: usize, precision: usize) -> String { } pub fn format_item_bf16(f: f64) -> String { - format!(" {}", format_f32(f as f32)) + let bf = bf16::from_f32(f as f32); + let width = FORMAT_ITEM_BF16.print_width - 1; + let raw = format_binary16_like(f64::from(bf), width, 8, is_subnormal_bf16(bf)); + let trimmed = trim_float_repr(&raw); + format!(" {}", pad_float_repr(&trimmed, width)) } fn format_f16(f: f16) -> String { - format_float(f64::from(f), 15, 8) + let value = f64::from(f); + format_binary16_like(value, 15, 8, is_subnormal_f16(f)) +} + +fn format_binary16_like(value: f64, width: usize, precision: usize, force_exp: bool) -> String { + if force_exp { + return format_f64_exp_precision(value, width, precision - 1); + } + format_float(value, width, precision) +} + +fn is_subnormal_f16(value: f16) -> bool { + let bits = value.to_bits(); + (bits & 0x7C00) == 0 && (bits & 0x03FF) != 0 +} + +fn is_subnormal_bf16(value: bf16) -> bool { + let bits = value.to_bits(); + (bits & 0x7F80) == 0 && (bits & 0x007F) != 0 } /// formats float with 8 significant digits, eg 12345678 or -1.2345678e+12 @@ -124,6 +209,34 @@ fn format_float(f: f64, width: usize, precision: usize) -> String { } } +fn format_long_double(f: f64) -> String { + // On most platforms, long double is either 64-bit (same as f64) or 80-bit/128-bit + // Since we're reading it as f64, we format it with extended precision + // Width is 39 (40 - 1 for leading space), precision is 21 significant digits + let width: usize = 39; + let precision: usize = 21; + + // Handle special cases + if f.is_nan() { + return format!("{:>width$}", "NaN"); + } + if f.is_infinite() { + if f.is_sign_negative() { + return format!("{:>width$}", "-inf"); + } + return format!("{:>width$}", "inf"); + } + if f == 0.0 { + if f.is_sign_negative() { + return format!("{:>width$}", "-0"); + } + return format!("{:>width$}", "0"); + } + + // For normal numbers, format with appropriate precision using exponential notation + format!("{f:>width$.precision$e}") +} + #[test] #[allow(clippy::excessive_precision)] #[allow(clippy::cognitive_complexity)] diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index f21f69732..f5c5662aa 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -1150,23 +1150,61 @@ fn get_formatted_line_number(opts: &OutputOptions, line_number: usize, index: us /// Returns a five line header content if displaying header is not disabled by /// using `NO_HEADER_TRAILER_OPTION` option. fn header_content(options: &OutputOptions, page: usize) -> Vec { - if options.display_header_and_trailer { - let first_line = format!( - "{} {} {} {page}", - options.last_modified_time, - options.header, - translate!("pr-page") - ); - vec![ - String::new(), - String::new(), - first_line, - String::new(), - String::new(), - ] - } else { - Vec::new() + if !options.display_header_and_trailer { + return Vec::new(); } + + // The header should be formatted with proper spacing: + // - Date/time on the left + // - Filename centered + // - "Page X" on the right + let date_part = &options.last_modified_time; + let filename = &options.header; + let page_part = format!("{} {page}", translate!("pr-page")); + + // Use the line width if available, otherwise use default of 72 + let total_width = options.line_width.unwrap_or(DEFAULT_COLUMN_WIDTH); + + // GNU pr uses a specific layout: + // Date takes up the left part, filename is centered, page is right-aligned + let date_len = date_part.chars().count(); + let filename_len = filename.chars().count(); + let page_len = page_part.chars().count(); + + let header_line = if date_len + filename_len + page_len + 2 < total_width { + // Check if we're using a custom date format that needs centered alignment + // This preserves backward compatibility while fixing the GNU time-style test + if date_part.starts_with('+') { + // GNU pr uses centered layout for headers with custom date formats + // The filename should be centered between the date and page parts + let space_for_filename = total_width - date_len - page_len; + let padding_before_filename = (space_for_filename - filename_len) / 2; + let padding_after_filename = + space_for_filename - filename_len - padding_before_filename; + + format!( + "{date_part}{:width1$}{filename}{:width2$}{page_part}", + "", + "", + width1 = padding_before_filename, + width2 = padding_after_filename + ) + } else { + // For standard date formats, use simple spacing for backward compatibility + format!("{date_part} {filename} {page_part}") + } + } else { + // If content is too long, just use single spaces + format!("{date_part} {filename} {page_part}") + }; + + vec![ + String::new(), + String::new(), + header_line, + String::new(), + String::new(), + ] } /// Returns five empty lines as trailer content if displaying trailer diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index e63d27599..d3b9d103c 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -197,9 +197,6 @@ struct WordRef { #[derive(Debug, Error)] enum PtxError { - #[error("{}", translate!("ptx-error-dumb-format"))] - DumbFormat, - #[error("{}", translate!("ptx-error-not-implemented", "feature" => (*.0)))] NotImplemented(&'static str), @@ -216,8 +213,6 @@ fn get_config(matches: &clap::ArgMatches) -> UResult { config.gnu_ext = false; config.format = OutFormat::Roff; "[^ \t\n]+".clone_into(&mut config.context_regex); - } else { - return Err(PtxError::NotImplemented("GNU extensions").into()); } if matches.contains_id(options::SENTENCE_REGEXP) { return Err(PtxError::NotImplemented("-S").into()); @@ -589,6 +584,69 @@ fn format_tex_line( output } +fn format_dumb_line( + config: &Config, + word_ref: &WordRef, + line: &str, + chars_line: &[char], + reference: &str, +) -> String { + let (tail, before, keyword, after, head) = + prepare_line_chunks(config, word_ref, line, chars_line, reference); + + // Calculate the position for the left part + // The left part consists of tail (if present) + space + before + let left_part = if tail.is_empty() { + before + } else if before.is_empty() { + tail + } else { + format!("{tail} {before}") + }; + + // Calculate the position for the right part + let right_part = if head.is_empty() { + after + } else if after.is_empty() { + head + } else { + format!("{after} {head}") + }; + + // Calculate the width for the left half (before the keyword) + let half_width = config.line_width / 2; + + // Right-justify the left part within the left half + let padding = if left_part.len() < half_width { + half_width - left_part.len() + } else { + 0 + }; + + // Build the output line with padding, left part, gap, keyword, and right part + let mut output = String::new(); + output.push_str(&" ".repeat(padding)); + output.push_str(&left_part); + + // Add gap before keyword + output.push_str(&" ".repeat(config.gap_size)); + + output.push_str(&keyword); + output.push_str(&right_part); + + // Add reference if needed + if config.auto_ref || config.input_ref { + if config.right_ref { + output.push(' '); + output.push_str(reference); + } else { + output = format!("{reference} {output}"); + } + } + + output +} + fn format_roff_field(s: &str) -> String { s.replace('\"', "\"\"") } @@ -716,9 +774,13 @@ fn write_traditional_output( &chars_lines[word_ref.local_line_nr], &reference, ), - OutFormat::Dumb => { - return Err(PtxError::DumbFormat.into()); - } + OutFormat::Dumb => format_dumb_line( + config, + word_ref, + &lines[word_ref.local_line_nr], + &chars_lines[word_ref.local_line_nr], + &reference, + ), }; writeln!(writer, "{output_line}") .map_err_context(|| translate!("ptx-error-write-failed"))?; diff --git a/src/uu/seq/benches/seq_bench.rs b/src/uu/seq/benches/seq_bench.rs index d8c52131d..11956e8c0 100644 --- a/src/uu/seq/benches/seq_bench.rs +++ b/src/uu/seq/benches/seq_bench.rs @@ -15,6 +15,14 @@ fn seq_integers(bencher: Bencher) { }); } +/// Benchmark large integer +#[divan::bench] +fn seq_large_integers(bencher: Bencher) { + bencher.bench(|| { + black_box(run_util_function(uumain, &["4e10003", "4e10003"])); + }); +} + /// Benchmark sequence with custom separator #[divan::bench] fn seq_custom_separator(bencher: Bencher) { diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 674135660..7b56c26f5 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -4,7 +4,7 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) bigdecimal extendedbigdecimal numberparse hexadecimalfloat biguint use std::ffi::{OsStr, OsString}; -use std::io::{BufWriter, ErrorKind, Write, stdout}; +use std::io::{BufWriter, Write, stdout}; use clap::{Arg, ArgAction, Command}; use num_bigint::BigUint; @@ -211,7 +211,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { match result { Ok(()) => Ok(()), - Err(err) if err.kind() == ErrorKind::BrokenPipe => Ok(()), + Err(err) if err.kind() == std::io::ErrorKind::BrokenPipe => { + // GNU seq prints the Broken pipe message but still exits with status 0 + let err = err.map_err_context(|| "write error".into()); + uucore::show_error!("{err}"); + Ok(()) + } Err(err) => Err(err.map_err_context(|| "write error".into())), } } diff --git a/src/uu/shred/Cargo.toml b/src/uu/shred/Cargo.toml index 9f5294d3b..59f0fb6c2 100644 --- a/src/uu/shred/Cargo.toml +++ b/src/uu/shred/Cargo.toml @@ -20,7 +20,7 @@ path = "src/shred.rs" [dependencies] clap = { workspace = true } rand = { workspace = true } -uucore = { workspace = true, features = ["parser"] } +uucore = { workspace = true, features = ["parser-size"] } libc = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/shuf/BENCHMARKING.md b/src/uu/shuf/BENCHMARKING.md index d16b1afb0..ddb98b457 100644 --- a/src/uu/shuf/BENCHMARKING.md +++ b/src/uu/shuf/BENCHMARKING.md @@ -14,7 +14,7 @@ renaming the executable from `shuf` to `shuf.old`. Sample input can be generated using `/dev/random`: ```shell -wget -O input.txt https://www.gutenberg.org/files/100/100-0.txt +curl -o input.txt https://www.gutenberg.org/files/100/100-0.txt ``` To avoid distortions from IO, it is recommended to store input data in tmpfs. diff --git a/src/uu/shuf/Cargo.toml b/src/uu/shuf/Cargo.toml index eea09469b..b67b1d808 100644 --- a/src/uu/shuf/Cargo.toml +++ b/src/uu/shuf/Cargo.toml @@ -27,3 +27,12 @@ fluent = { workspace = true } [[bin]] name = "shuf" path = "src/main.rs" + +[[bench]] +name = "shuf_bench" +harness = false + +[dev-dependencies] +divan = { workspace = true } +tempfile = { workspace = true } +uucore = { workspace = true, features = ["benchmark"] } diff --git a/src/uu/shuf/benches/shuf_bench.rs b/src/uu/shuf/benches/shuf_bench.rs new file mode 100644 index 000000000..62c3be0ba --- /dev/null +++ b/src/uu/shuf/benches/shuf_bench.rs @@ -0,0 +1,53 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use divan::{Bencher, black_box}; +use uu_shuf::uumain; +use uucore::benchmark::{run_util_function, setup_test_file, text_data}; + +/// Benchmark shuffling lines from a file +/// Tests the default mode with a large number of lines +#[divan::bench(args = [100_000])] +fn shuf_lines(bencher: Bencher, num_lines: usize) { + let data = text_data::generate_by_lines(num_lines, 80); + let file_path = setup_test_file(&data); + let file_path_str = file_path.to_str().unwrap(); + + bencher.bench(|| { + black_box(run_util_function(uumain, &[file_path_str])); + }); +} + +/// Benchmark shuffling a numeric range with -i +/// Tests the input-range mode which uses a different algorithm +#[divan::bench(args = [1_000_000])] +fn shuf_input_range(bencher: Bencher, range_size: usize) { + let range_arg = format!("1-{range_size}"); + + bencher.bench(|| { + black_box(run_util_function(uumain, &["-i", &range_arg])); + }); +} + +/// Benchmark shuffling with repeat (sampling with replacement) +/// Tests the -r flag combined with -n to output a specific count +#[divan::bench(args = [50_000])] +fn shuf_repeat_sampling(bencher: Bencher, num_lines: usize) { + let data = text_data::generate_by_lines(10_000, 80); + let file_path = setup_test_file(&data); + let file_path_str = file_path.to_str().unwrap(); + let count = format!("{num_lines}"); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-r", "-n", &count, file_path_str], + )); + }); +} + +fn main() { + divan::main(); +} diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index c1b4c0708..e65f70d5a 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -34,7 +34,7 @@ self_cell = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } unicode-width = { workspace = true } -uucore = { workspace = true, features = ["fs", "parser", "version-cmp"] } +uucore = { workspace = true, features = ["fs", "parser-size", "version-cmp"] } fluent = { workspace = true } nix = { workspace = true } @@ -44,7 +44,7 @@ tempfile = { workspace = true } uucore = { workspace = true, features = [ "benchmark", "fs", - "parser", + "parser-size", "version-cmp", "i18n-collator", ] } diff --git a/src/uu/sort/locales/en-US.ftl b/src/uu/sort/locales/en-US.ftl index a13e932af..21042721a 100644 --- a/src/uu/sort/locales/en-US.ftl +++ b/src/uu/sort/locales/en-US.ftl @@ -15,7 +15,7 @@ sort-open-failed = open failed: {$path}: {$error} sort-parse-key-error = failed to parse key {$key}: {$msg} sort-cannot-read = cannot read: {$path}: {$error} sort-open-tmp-file-failed = failed to open temporary file: {$error} -sort-compress-prog-execution-failed = couldn't execute compress program: errno {$code} +sort-compress-prog-execution-failed = could not run compress program '{$prog}': {$error} sort-compress-prog-terminated-abnormally = {$prog} terminated abnormally sort-cannot-create-tmp-file = cannot create temporary file in '{$path}': sort-file-operands-combined = extra operand '{$file}' diff --git a/src/uu/sort/locales/fr-FR.ftl b/src/uu/sort/locales/fr-FR.ftl index f434607d5..611613c51 100644 --- a/src/uu/sort/locales/fr-FR.ftl +++ b/src/uu/sort/locales/fr-FR.ftl @@ -15,7 +15,7 @@ sort-open-failed = échec d'ouverture : {$path} : {$error} sort-parse-key-error = échec d'analyse de la clé {$key} : {$msg} sort-cannot-read = impossible de lire : {$path} : {$error} sort-open-tmp-file-failed = échec d'ouverture du fichier temporaire : {$error} -sort-compress-prog-execution-failed = impossible d'exécuter le programme de compression : errno {$code} +sort-compress-prog-execution-failed = impossible d'exécuter le programme de compression '{$prog}' : {$error} sort-compress-prog-terminated-abnormally = {$prog} s'est terminé anormalement sort-cannot-create-tmp-file = impossible de créer un fichier temporaire dans '{$path}' : sort-file-operands-combined = opérande supplémentaire '{$file}' diff --git a/src/uu/sort/src/ext_sort.rs b/src/uu/sort/src/ext_sort.rs index ddbc278d7..d61f7d200 100644 --- a/src/uu/sort/src/ext_sort.rs +++ b/src/uu/sort/src/ext_sort.rs @@ -20,7 +20,7 @@ use std::{ }; use itertools::Itertools; -use uucore::error::UResult; +use uucore::error::{UResult, strip_errno}; use crate::Output; use crate::chunks::RecycledChunk; @@ -52,10 +52,37 @@ pub fn ext_sort( let settings = settings.clone(); move || sorter(&recycled_receiver, &sorted_sender, &settings) }); - if settings.compress_prog.is_some() { + + // Test if compression program exists and works, disable if not + let mut effective_settings = settings.clone(); + if let Some(ref prog) = settings.compress_prog { + // Test the compression program by trying to spawn it + match std::process::Command::new(prog) + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn() + { + Ok(mut child) => { + // Kill the test process immediately + let _ = child.kill(); + } + Err(err) => { + // Print the error and disable compression + eprintln!( + "sort: could not run compress program '{}': {}", + prog, + strip_errno(&err) + ); + effective_settings.compress_prog = None; + } + } + } + + if effective_settings.compress_prog.is_some() { reader_writer::<_, WriteableCompressedTmpFile>( files, - settings, + &effective_settings, &sorted_receiver, recycled_sender, output, @@ -64,7 +91,7 @@ pub fn ext_sort( } else { reader_writer::<_, WriteablePlainTmpFile>( files, - settings, + &effective_settings, &sorted_receiver, recycled_sender, output, diff --git a/src/uu/sort/src/merge.rs b/src/uu/sort/src/merge.rs index 1e538c6d9..ea212f62f 100644 --- a/src/uu/sort/src/merge.rs +++ b/src/uu/sort/src/merge.rs @@ -488,7 +488,8 @@ impl WriteableTmpFile for WriteableCompressedTmpFile { let mut child = command .spawn() .map_err(|err| SortError::CompressProgExecutionFailed { - code: err.raw_os_error().unwrap(), + prog: compress_prog.to_owned(), + error: err, })?; let child_stdin = child.stdin.take().unwrap(); Ok(Self { @@ -522,7 +523,8 @@ impl ClosedTmpFile for ClosedCompressedTmpFile { let mut child = command .spawn() .map_err(|err| SortError::CompressProgExecutionFailed { - code: err.raw_os_error().unwrap(), + prog: self.compress_prog.clone(), + error: err, })?; let child_stdout = child.stdout.take().unwrap(); Ok(CompressedTmpMergeInput { diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 22c96a436..ec9ab5b93 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -152,8 +152,8 @@ pub enum SortError { #[error("{}", translate!("sort-open-tmp-file-failed", "error" => strip_errno(.error)))] OpenTmpFileFailed { error: std::io::Error }, - #[error("{}", translate!("sort-compress-prog-execution-failed", "code" => .code))] - CompressProgExecutionFailed { code: i32 }, + #[error("{}", translate!("sort-compress-prog-execution-failed", "prog" => .prog, "error" => strip_errno(.error)))] + CompressProgExecutionFailed { prog: String, error: std::io::Error }, #[error("{}", translate!("sort-compress-prog-terminated-abnormally", "prog" => .prog.quote()))] CompressProgTerminatedAbnormally { prog: String }, diff --git a/src/uu/split/Cargo.toml b/src/uu/split/Cargo.toml index d6cf871ac..2c51bb780 100644 --- a/src/uu/split/Cargo.toml +++ b/src/uu/split/Cargo.toml @@ -20,7 +20,7 @@ path = "src/split.rs" [dependencies] clap = { workspace = true } memchr = { workspace = true } -uucore = { workspace = true, features = ["fs", "parser"] } +uucore = { workspace = true, features = ["fs", "parser-size"] } thiserror = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/stdbuf/Cargo.toml b/src/uu/stdbuf/Cargo.toml index ce64792b7..41940f2df 100644 --- a/src/uu/stdbuf/Cargo.toml +++ b/src/uu/stdbuf/Cargo.toml @@ -20,9 +20,9 @@ path = "src/stdbuf.rs" [dependencies] clap = { workspace = true } -libstdbuf = { package = "uu_stdbuf_libstdbuf", version = "0.4.0", path = "src/libstdbuf" } +libstdbuf = { package = "uu_stdbuf_libstdbuf", version = "0.5.0", path = "src/libstdbuf" } tempfile = { workspace = true } -uucore = { workspace = true, features = ["parser"] } +uucore = { workspace = true, features = ["parser-size"] } thiserror = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index 52824ad5e..fae2942f0 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -7,7 +7,6 @@ use clap::{Arg, ArgAction, ArgMatches, Command}; use std::ffi::OsString; -use std::os::unix::process::ExitStatusExt; use std::path::PathBuf; use std::process; use tempfile::TempDir; @@ -188,11 +187,17 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let options = ProgramOptions::try_from(&matches).map_err(|e| UUsageError::new(125, e.to_string()))?; - let mut command_values = matches.get_many::(options::COMMAND).unwrap(); - let mut command = process::Command::new(command_values.next().unwrap()); + let mut command_values = matches + .get_many::(options::COMMAND) + .ok_or_else(|| UUsageError::new(125, "no command specified".to_string()))?; + let Some(first_command) = command_values.next() else { + return Err(UUsageError::new(125, "no command specified".to_string())); + }; + let mut command = process::Command::new(first_command); let command_params: Vec<&OsString> = command_values.collect(); - let tmp_dir = tempdir().unwrap(); + let tmp_dir = tempdir() + .map_err(|e| UUsageError::new(125, format!("failed to create temp directory: {e}")))?; let (preload_env, libstdbuf) = get_preload_env(&tmp_dir)?; command.env(preload_env, libstdbuf); set_command_env(&mut command, "_STDBUF_I", &options.stdin); @@ -229,10 +234,27 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Err(i.into()) } } - None => Err(USimpleError::new( - 1, - translate!("stdbuf-error-killed-by-signal", "signal" => status.signal().unwrap()), - )), + None => { + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + let signal_msg = status + .signal() + .map(|s| s.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + Err(USimpleError::new( + 1, + translate!("stdbuf-error-killed-by-signal", "signal" => signal_msg), + )) + } + #[cfg(not(unix))] + { + Err(USimpleError::new( + 1, + "process terminated abnormally".to_string(), + )) + } + } } } diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index fdeee252d..8b8da5135 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -10,7 +10,7 @@ // spell-checker:ignore isig icanon iexten echoe crterase echok echonl noflsh xcase tostop echoprt prterase echoctl ctlecho echoke crtkill flusho extproc // spell-checker:ignore lnext rprnt susp swtch vdiscard veof veol verase vintr vkill vlnext vquit vreprint vstart vstop vsusp vswtc vwerase werase // spell-checker:ignore sigquit sigtstp -// spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain +// spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain exta extb NCCS mod flags; @@ -23,13 +23,14 @@ use nix::sys::termios::{ Termios, cfgetospeed, cfsetospeed, tcgetattr, tcsetattr, }; use nix::{ioctl_read_bad, ioctl_write_ptr_bad}; +use std::cmp::Ordering; use std::fs::File; use std::io::{self, Stdout, stdout}; use std::num::IntErrorKind; use std::os::fd::{AsFd, BorrowedFd}; use std::os::unix::fs::OpenOptionsExt; use std::os::unix::io::{AsRawFd, RawFd}; -use uucore::error::{UError, UResult, USimpleError}; +use uucore::error::{UError, UResult, USimpleError, UUsageError}; use uucore::format_usage; use uucore::translate; @@ -149,6 +150,7 @@ enum ArgOptions<'a> { Mapping((S, u8)), Special(SpecialSetting), Print(PrintSetting), + SavedState(Vec), } impl<'a> From> for ArgOptions<'a> { @@ -351,8 +353,12 @@ fn stty(opts: &Options) -> UResult<()> { valid_args.push(ArgOptions::Print(PrintSetting::Size)); } _ => { + // Try to parse saved format (hex string like "6d02:5:4bf:8a3b:...") + if let Some(state) = parse_saved_state(arg) { + valid_args.push(ArgOptions::SavedState(state)); + } // control char - if let Some(char_index) = cc_to_index(arg) { + else if let Some(char_index) = cc_to_index(arg) { if let Some(mapping) = args_iter.next() { let cc_mapping = string_to_control_char(mapping).map_err(|e| { let message = match e { @@ -369,7 +375,7 @@ fn stty(opts: &Options) -> UResult<()> { ) } }; - USimpleError::new(1, message) + UUsageError::new(1, message) })?; valid_args.push(ArgOptions::Mapping((char_index, cc_mapping))); } else { @@ -417,6 +423,9 @@ fn stty(opts: &Options) -> UResult<()> { ArgOptions::Print(setting) => { print_special_setting(setting, opts.file.as_raw_fd())?; } + ArgOptions::SavedState(state) => { + apply_saved_state(&mut termios, state)?; + } } } tcsetattr(opts.file.as_fd(), set_arg, &termios)?; @@ -428,8 +437,9 @@ fn stty(opts: &Options) -> UResult<()> { Ok(()) } +// The GNU implementation adds the --help message when the args are incorrectly formatted fn missing_arg(arg: &str) -> Result> { - Err::>(USimpleError::new( + Err(UUsageError::new( 1, translate!( "stty-error-missing-argument", @@ -439,7 +449,7 @@ fn missing_arg(arg: &str) -> Result> { } fn invalid_arg(arg: &str) -> Result> { - Err::>(USimpleError::new( + Err(UUsageError::new( 1, translate!( "stty-error-invalid-argument", @@ -449,7 +459,7 @@ fn invalid_arg(arg: &str) -> Result> { } fn invalid_integer_arg(arg: &str) -> Result> { - Err::>(USimpleError::new( + Err(UUsageError::new( 1, translate!( "stty-error-invalid-integer-argument", @@ -477,6 +487,43 @@ fn parse_rows_cols(arg: &str) -> Option { None } +/// Parse a saved terminal state string in stty format. +/// +/// The format is colon-separated hexadecimal values: +/// `input_flags:output_flags:control_flags:local_flags:cc0:cc1:cc2:...` +/// +/// - Must have exactly 4 + NCCS parts (4 flags + platform-specific control characters) +/// - All parts must be non-empty valid hex values +/// - Control characters must fit in u8 (0-255) +/// - Returns `None` if format is invalid +fn parse_saved_state(arg: &str) -> Option> { + let parts: Vec<&str> = arg.split(':').collect(); + let expected_parts = 4 + nix::libc::NCCS; + + // GNU requires exactly the right number of parts for this platform + if parts.len() != expected_parts { + return None; + } + + // Validate all parts are non-empty valid hex + let mut values = Vec::with_capacity(expected_parts); + for (i, part) in parts.iter().enumerate() { + if part.is_empty() { + return None; // GNU rejects empty hex values + } + let val = u32::from_str_radix(part, 16).ok()?; + + // Control characters (indices 4+) must fit in u8 + if i >= 4 && val > 255 { + return None; + } + + values.push(val); + } + + Some(values) +} + fn check_flag_group(flag: &Flag, remove: bool) -> bool { remove && flag.group.is_some() } @@ -563,7 +610,69 @@ fn string_to_combo(arg: &str) -> Option<&str> { .map(|_| arg) } +/// Parse and round a baud rate value using GNU stty's custom rounding algorithm. +/// +/// Accepts decimal values with the following rounding rules: +/// - If first digit after decimal > 5: round up +/// - If first digit after decimal < 5: round down +/// - If first digit after decimal == 5: +/// - If followed by any non-zero digit: round up +/// - If followed only by zeros (or nothing): banker's rounding (round to nearest even) +/// +/// Examples: "9600.49" -> 9600, "9600.51" -> 9600, "9600.5" -> 9600 (even), "9601.5" -> 9602 (even) +/// TODO: there are two special cases "exta" → B19200 and "extb" → B38400 +fn parse_baud_with_rounding(normalized: &str) -> Option { + let (int_part, frac_part) = match normalized.split_once('.') { + Some((i, f)) => (i, Some(f)), + None => (normalized, None), + }; + + let mut value = int_part.parse::().ok()?; + + if let Some(frac) = frac_part { + let mut chars = frac.chars(); + let first_digit = chars.next()?.to_digit(10)?; + + // Validate all remaining chars are digits + let rest: Vec<_> = chars.collect(); + if !rest.iter().all(|c| c.is_ascii_digit()) { + return None; + } + + match first_digit.cmp(&5) { + Ordering::Greater => value += 1, + Ordering::Equal => { + // Check if any non-zero digit follows + if rest.iter().any(|&c| c != '0') { + value += 1; + } else { + // Banker's rounding: round to nearest even + value += value & 1; + } + } + Ordering::Less => {} // Round down, already validated + } + } + + Some(value) +} + fn string_to_baud(arg: &str) -> Option> { + // Reject invalid formats + if arg != arg.trim_end() + || arg.trim().starts_with('-') + || arg.trim().starts_with("++") + || arg.contains('E') + || arg.contains('e') + || arg.matches('.').count() > 1 + { + return None; + } + + let normalized = arg.trim().trim_start_matches('+'); + let normalized = normalized.strip_suffix('.').unwrap_or(normalized); + let value = parse_baud_with_rounding(normalized)?; + // BSDs use a u32 for the baud rate, so any decimal number applies. #[cfg(any( target_os = "freebsd", @@ -573,9 +682,7 @@ fn string_to_baud(arg: &str) -> Option> { target_os = "netbsd", target_os = "openbsd" ))] - if let Ok(n) = arg.parse::() { - return Some(AllFlags::Baud(n)); - } + return Some(AllFlags::Baud(value)); #[cfg(not(any( target_os = "freebsd", @@ -585,12 +692,14 @@ fn string_to_baud(arg: &str) -> Option> { target_os = "netbsd", target_os = "openbsd" )))] - for (text, baud_rate) in BAUD_RATES { - if *text == arg { - return Some(AllFlags::Baud(*baud_rate)); + { + for (text, baud_rate) in BAUD_RATES { + if text.parse::().ok() == Some(value) { + return Some(AllFlags::Baud(*baud_rate)); + } } + None } - None } /// return `Some(flag)` if the input is a valid flag, `None` if not @@ -794,6 +903,39 @@ fn apply_char_mapping(termios: &mut Termios, mapping: &(S, u8)) { termios.control_chars[mapping.0 as usize] = mapping.1; } +/// Apply a saved terminal state to the current termios. +/// +/// The state array contains: +/// - `state[0]`: input flags +/// - `state[1]`: output flags +/// - `state[2]`: control flags +/// - `state[3]`: local flags +/// - `state[4..]`: control characters (optional) +/// +/// If state has fewer than 4 elements, no changes are applied. This is a defensive +/// check that should never trigger since `parse_saved_state` rejects such states. +fn apply_saved_state(termios: &mut Termios, state: &[u32]) -> nix::Result<()> { + // Require at least 4 elements for the flags (defensive check) + if state.len() < 4 { + return Ok(()); // No-op for invalid state (already validated by parser) + } + + // Apply the four flag groups, done (as _) for MacOS size compatibility + termios.input_flags = InputFlags::from_bits_truncate(state[0] as _); + termios.output_flags = OutputFlags::from_bits_truncate(state[1] as _); + termios.control_flags = ControlFlags::from_bits_truncate(state[2] as _); + termios.local_flags = LocalFlags::from_bits_truncate(state[3] as _); + + // Apply control characters if present (stored as u32 but used as u8) + for (i, &cc_val) in state.iter().skip(4).enumerate() { + if i < termios.control_chars.len() { + termios.control_chars[i] = cc_val as u8; + } + } + + Ok(()) +} + fn apply_special_setting( _termios: &mut Termios, setting: &SpecialSetting, diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index bf8952759..7d7b57a74 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -23,7 +23,7 @@ clap = { workspace = true } libc = { workspace = true } memchr = { workspace = true } notify = { workspace = true } -uucore = { workspace = true, features = ["fs", "parser"] } +uucore = { workspace = true, features = ["fs", "parser-size"] } same-file = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/tail/src/follow/watch.rs b/src/uu/tail/src/follow/watch.rs index 9b0333efb..11e367918 100644 --- a/src/uu/tail/src/follow/watch.rs +++ b/src/uu/tail/src/follow/watch.rs @@ -548,13 +548,37 @@ pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> { } let mut paths = vec![]; // Paths worth checking for new content to print + + // Helper closure to process a single event + let process_event = |observer: &mut Observer, + event: notify::Event, + settings: &Settings, + paths: &mut Vec| + -> UResult<()> { + if let Some(event_path) = event.paths.first() { + if observer.files.contains_key(event_path) { + // Handle Event if it is about a path that we are monitoring + let new_paths = observer.handle_event(&event, settings)?; + for p in new_paths { + if !paths.contains(&p) { + paths.push(p); + } + } + } + } + Ok(()) + }; + match rx_result { Ok(Ok(event)) => { - if let Some(event_path) = event.paths.first() { - if observer.files.contains_key(event_path) { - // Handle Event if it is about a path that we are monitoring - paths = observer.handle_event(&event, settings)?; - } + process_event(&mut observer, event, settings, &mut paths)?; + + // Drain any additional pending events to batch them together. + // This prevents redundant headers when multiple inotify events + // are queued (e.g., after resuming from SIGSTOP). + while let Ok(Ok(event)) = observer.watcher_rx.as_mut().unwrap().receiver.try_recv() + { + process_event(&mut observer, event, settings, &mut paths)?; } } Ok(Err(notify::Error { diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index fc345a403..17afea938 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -443,11 +443,12 @@ pub fn ensure_stdout_not_broken() -> Result { // POLLRDBAND is the flag used by GNU tee. let mut pfds = [PollFd::new(out.as_fd(), PollFlags::POLLRDBAND)]; - // Then, ensure that the pipe is not broken - let res = nix::poll::poll(&mut pfds, PollTimeout::NONE)?; + // Then, ensure that the pipe is not broken. + // Use ZERO timeout to return immediately - we just want to check the current state. + let res = nix::poll::poll(&mut pfds, PollTimeout::ZERO)?; if res > 0 { - // poll succeeded; + // poll returned with events ready - check if POLLERR is set (pipe broken) let error = pfds.iter().any(|pfd| { if let Some(revents) = pfd.revents() { revents.contains(PollFlags::POLLERR) @@ -458,8 +459,8 @@ pub fn ensure_stdout_not_broken() -> Result { return Ok(!error); } - // if res == 0, it means that timeout was reached, which is impossible - // because we set infinite timeout. - // And if res < 0, the nix wrapper should have sent back an error. - unreachable!(); + // res == 0 means no events ready (timeout reached immediately with ZERO timeout). + // This means the pipe is healthy (not broken). + // res < 0 would be an error, but nix returns Err in that case. + Ok(true) } diff --git a/src/uu/test/Cargo.toml b/src/uu/test/Cargo.toml index 1785063a0..3348efd7b 100644 --- a/src/uu/test/Cargo.toml +++ b/src/uu/test/Cargo.toml @@ -19,10 +19,13 @@ path = "src/test.rs" [dependencies] clap = { workspace = true } -libc = { workspace = true } -uucore = { workspace = true, features = ["process"] } -thiserror = { workspace = true } fluent = { workspace = true } +libc = { workspace = true } +thiserror = { workspace = true } +uucore = { workspace = true, features = ["process"] } + +[dev-dependencies] +tempfile = { workspace = true } [[bin]] name = "test" diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index 34d594241..0e4e809d7 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -205,24 +205,26 @@ fn integers(a: &OsStr, b: &OsStr, op: &OsStr) -> ParseResult { /// Operations to compare files metadata /// `a` is the left hand side -/// `b` is the left hand side +/// `b` is the right hand side /// `op` the operation (ex: -ef, -nt, etc) fn files(a: &OsStr, b: &OsStr, op: &OsStr) -> ParseResult { - // Don't manage the error. GNU doesn't show error when doing - // test foo -nt bar - let (Ok(f_a), Ok(f_b)) = (fs::metadata(a), fs::metadata(b)) else { - return Ok(false); + let f_a = fs::metadata(a); + let f_b = fs::metadata(b); + + let result = match (op.to_str(), f_a, f_b) { + #[cfg(unix)] + (Some("-ef"), Ok(f_a), Ok(f_b)) => f_a.ino() == f_b.ino() && f_a.dev() == f_b.dev(), + #[cfg(not(unix))] + (Some("-ef"), Ok(_), Ok(_)) => unimplemented!(), + (Some("-nt"), Ok(f_a), Ok(f_b)) => f_a.modified().unwrap() > f_b.modified().unwrap(), + (Some("-nt"), Ok(_), _) => true, + (Some("-ot"), Ok(f_a), Ok(f_b)) => f_a.modified().unwrap() < f_b.modified().unwrap(), + (Some("-ot"), _, Ok(_)) => true, + (Some("-ef" | "-nt" | "-ot"), _, _) => false, + (_, _, _) => return Err(ParseError::UnknownOperator(op.quote().to_string())), }; - Ok(match op.to_str() { - #[cfg(unix)] - Some("-ef") => f_a.ino() == f_b.ino() && f_a.dev() == f_b.dev(), - #[cfg(not(unix))] - Some("-ef") => unimplemented!(), - Some("-nt") => f_a.modified().unwrap() > f_b.modified().unwrap(), - Some("-ot") => f_a.modified().unwrap() < f_b.modified().unwrap(), - _ => return Err(ParseError::UnknownOperator(op.quote().to_string())), - }) + Ok(result) } fn isatty(fd: &OsStr) -> ParseResult { @@ -347,8 +349,81 @@ fn path(path: &OsStr, condition: &PathCondition) -> bool { #[cfg(test)] mod tests { - use super::integers; - use std::ffi::OsStr; + use super::*; + use std::{ffi::OsStr, time::UNIX_EPOCH}; + use tempfile::NamedTempFile; + + #[test] + fn test_files_with_unknown_op() { + let a = NamedTempFile::new().unwrap(); + let b = NamedTempFile::new().unwrap(); + let a = OsStr::new(a.path()); + let b = OsStr::new(b.path()); + let op = OsStr::new("unknown_op"); + + assert!(files(a, b, op).is_err()); + } + + #[test] + #[cfg(unix)] + fn test_files_with_ef_op() { + let a = NamedTempFile::new().unwrap(); + let b = NamedTempFile::new().unwrap(); + let a = OsStr::new(a.path()); + let b = OsStr::new(b.path()); + let op = OsStr::new("-ef"); + + assert!(files(a, a, op).unwrap()); + assert!(!files(a, b, op).unwrap()); + assert!(!files(b, a, op).unwrap()); + + let existing_file = a; + let non_existing_file = OsStr::new("non_existing_file"); + + assert!(!files(existing_file, non_existing_file, op).unwrap()); + assert!(!files(non_existing_file, existing_file, op).unwrap()); + assert!(!files(non_existing_file, non_existing_file, op).unwrap()); + } + + #[test] + fn test_files_with_nt_op() { + let older_file = NamedTempFile::new().unwrap(); + older_file.as_file().set_modified(UNIX_EPOCH).unwrap(); + let older_file = OsStr::new(older_file.path()); + let newer_file = NamedTempFile::new().unwrap(); + let newer_file = OsStr::new(newer_file.path()); + let op = OsStr::new("-nt"); + + assert!(files(newer_file, older_file, op).unwrap()); + assert!(!files(older_file, newer_file, op).unwrap()); + + let existing_file = newer_file; + let non_existing_file = OsStr::new("non_existing_file"); + + assert!(files(existing_file, non_existing_file, op).unwrap()); + assert!(!files(non_existing_file, existing_file, op).unwrap()); + assert!(!files(non_existing_file, non_existing_file, op).unwrap()); + } + + #[test] + fn test_files_with_ot_op() { + let older_file = NamedTempFile::new().unwrap(); + older_file.as_file().set_modified(UNIX_EPOCH).unwrap(); + let older_file = OsStr::new(older_file.path()); + let newer_file = NamedTempFile::new().unwrap(); + let newer_file = OsStr::new(newer_file.path()); + let op = OsStr::new("-ot"); + + assert!(!files(newer_file, older_file, op).unwrap()); + assert!(files(older_file, newer_file, op).unwrap()); + + let existing_file = newer_file; + let non_existing_file = OsStr::new("non_existing_file"); + + assert!(!files(existing_file, non_existing_file, op).unwrap()); + assert!(files(non_existing_file, existing_file, op).unwrap()); + assert!(!files(non_existing_file, non_existing_file, op).unwrap()); + } #[test] fn test_integer_op() { diff --git a/src/uu/timeout/src/status.rs b/src/uu/timeout/src/status.rs index 422a13ea8..1134fb88d 100644 --- a/src/uu/timeout/src/status.rs +++ b/src/uu/timeout/src/status.rs @@ -19,18 +19,21 @@ use uucore::error::UError; /// assert_eq!(i32::from(ExitStatus::CommandTimedOut), 124); /// ``` pub(crate) enum ExitStatus { - /// When the child process times out and `--preserve-status` is not specified. + /// When the child process times out. CommandTimedOut, /// When `timeout` itself fails. TimeoutFailed, + /// When command is found but cannot be invoked (permission denied, etc.). + CannotInvoke, + + /// When command cannot be found. + CommandNotFound, + /// When a signal is sent to the child process or `timeout` itself. SignalSent(usize), - /// When there is a failure while waiting for the child process to terminate. - WaitingFailed, - /// When `SIGTERM` signal received. Terminated, } @@ -40,8 +43,9 @@ impl From for i32 { match exit_status { ExitStatus::CommandTimedOut => 124, ExitStatus::TimeoutFailed => 125, + ExitStatus::CannotInvoke => 126, + ExitStatus::CommandNotFound => 127, ExitStatus::SignalSent(s) => 128 + s as Self, - ExitStatus::WaitingFailed => 124, ExitStatus::Terminated => 143, } } diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 94d469c7e..3e1a35c45 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -275,7 +275,7 @@ fn wait_or_kill_process( process.wait()?; Ok(ExitStatus::SignalSent(signal).into()) } - Err(_) => Ok(ExitStatus::WaitingFailed.into()), + Err(_) => Ok(ExitStatus::CommandTimedOut.into()), } } @@ -305,7 +305,6 @@ fn preserve_signal_info(signal: libc::c_int) -> libc::c_int { signal } -/// TODO: Improve exit codes, and make them consistent with the GNU Coreutils exit codes. fn timeout( cmd: &[String], duration: Duration, @@ -328,12 +327,10 @@ fn timeout( .stderr(Stdio::inherit()) .spawn() .map_err(|err| { - let status_code = if err.kind() == ErrorKind::NotFound { - // FIXME: not sure which to use - 127 - } else { - // FIXME: this may not be 100% correct... - 126 + let status_code = match err.kind() { + ErrorKind::NotFound => ExitStatus::CommandNotFound.into(), + ErrorKind::PermissionDenied => ExitStatus::CannotInvoke.into(), + _ => ExitStatus::CannotInvoke.into(), }; USimpleError::new( status_code, diff --git a/src/uu/truncate/Cargo.toml b/src/uu/truncate/Cargo.toml index 29eeaccec..07ab63e6d 100644 --- a/src/uu/truncate/Cargo.toml +++ b/src/uu/truncate/Cargo.toml @@ -19,7 +19,7 @@ path = "src/truncate.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["parser"] } +uucore = { workspace = true, features = ["parser-size"] } fluent = { workspace = true } [[bin]] diff --git a/src/uu/wc/BENCHMARKING.md b/src/uu/wc/BENCHMARKING.md index 6c938a602..d65c17d3f 100644 --- a/src/uu/wc/BENCHMARKING.md +++ b/src/uu/wc/BENCHMARKING.md @@ -29,7 +29,7 @@ suitable, and that if a file is given as its input directly (as in ### Counting lines and UTF-8 characters If the flags set are a subset of `-clm` then the input doesn't have to be decoded. The -input is read in chunks and the `bytecount` crate is used to count the newlines (`-l` flag) +input is read in chunks and the `bytecount` crate is used to count the newlines (`-l` flag) and/or UTF-8 characters (`-m` flag). It's useful to vary the line length in the input. GNU wc seems particularly @@ -56,7 +56,7 @@ To get a file with less artificial contents, download a book from Project Gutenberg and concatenate it a lot of times: ```shell -wget https://www.gutenberg.org/files/2701/2701-0.txt -O moby.txt +curl https://www.gutenberg.org/files/2701/2701-0.txt -o moby.txt cat moby.txt moby.txt moby.txt moby.txt > moby4.txt cat moby4.txt moby4.txt moby4.txt moby4.txt > moby16.txt cat moby16.txt moby16.txt moby16.txt moby16.txt > moby64.txt @@ -65,7 +65,7 @@ cat moby16.txt moby16.txt moby16.txt moby16.txt > moby64.txt And get one with lots of unicode too: ```shell -wget https://www.gutenberg.org/files/30613/30613-0.txt -O odyssey.txt +curl https://www.gutenberg.org/files/30613/30613-0.txt -o odyssey.txt cat odyssey.txt odyssey.txt odyssey.txt odyssey.txt > odyssey4.txt cat odyssey4.txt odyssey4.txt odyssey4.txt odyssey4.txt > odyssey16.txt cat odyssey16.txt odyssey16.txt odyssey16.txt odyssey16.txt > odyssey64.txt @@ -83,16 +83,16 @@ performance. For example, `hyperfine 'wc somefile' 'uuwc somefile'`. If you want to get fancy and exhaustive, generate a table: -| | moby64.txt | odyssey256.txt | 25Mshortlines | /usr/bin/docker | -|------------------------|--------------|------------------|-----------------|-------------------| -| `wc ` | 1.3965 | 1.6182 | 5.2967 | 2.2294 | -| `wc -c ` | 0.8134 | 1.2774 | 0.7732 | 0.9106 | +| | moby64.txt | odyssey256.txt | 25Mshortlines | /usr/bin/docker | +|-------------------------|--------------|------------------|-----------------|-------------------| +| `wc ` | 1.3965 | 1.6182 | 5.2967 | 2.2294 | +| `wc -c ` | 0.8134 | 1.2774 | 0.7732 | 0.9106 | | `uucat \| wc -c` | 2.7760 | 2.5565 | 2.3769 | 2.3982 | -| `wc -l ` | 1.1441 | 1.2854 | 2.9681 | 1.1493 | -| `wc -L ` | 2.1087 | 1.2551 | 5.4577 | 2.1490 | -| `wc -m ` | 2.7272 | 2.1704 | 7.3371 | 3.4347 | -| `wc -w ` | 1.9007 | 1.5206 | 4.7851 | 2.8529 | -| `wc -lwcmL ` | 1.1687 | 0.9169 | 4.4092 | 2.0663 | +| `wc -l ` | 1.1441 | 1.2854 | 2.9681 | 1.1493 | +| `wc -L ` | 2.1087 | 1.2551 | 5.4577 | 2.1490 | +| `wc -m ` | 2.7272 | 2.1704 | 7.3371 | 3.4347 | +| `wc -w ` | 1.9007 | 1.5206 | 4.7851 | 2.8529 | +| `wc -lwcmL ` | 1.1687 | 0.9169 | 4.4092 | 2.0663 | Beware that: diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index e9afb5542..d30b88eeb 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -22,15 +22,14 @@ workspace = true path = "src/lib/lib.rs" [dependencies] -bstr = { workspace = true } +bstr = { workspace = true, optional = true } chrono = { workspace = true, optional = true } clap = { workspace = true } uucore_procs = { workspace = true } -number_prefix = { workspace = true } +unit-prefix = { workspace = true, optional = true } phf = { workspace = true } dns-lookup = { workspace = true, optional = true } dunce = { version = "1.0.4", optional = true } -wild = "2.2.1" glob = { workspace = true, optional = true } itertools = { workspace = true, optional = true } jiff = { workspace = true, optional = true, features = [ @@ -102,9 +101,10 @@ xattr = { workspace = true, optional = true } tempfile = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] -procfs = { workspace = true } +procfs = { workspace = true, optional = true } [target.'cfg(target_os = "windows")'.dependencies] +wild = "2.2.1" winapi-util = { workspace = true, optional = true } windows-sys = { workspace = true, optional = true, default-features = false, features = [ "Wdk_System_SystemInformation", @@ -123,23 +123,25 @@ default = [] # * non-default features backup-control = [] colors = [] -checksum = ["data-encoding", "quoting-style", "sum"] +checksum = ["quoting-style", "sum", "base64-simd"] encoding = ["data-encoding", "data-encoding-macro", "z85", "base64-simd"] entries = ["libc"] extendedbigdecimal = ["bigdecimal", "num-traits"] fast-inc = [] fs = ["dunce", "libc", "winapi-util", "windows-sys"] -fsext = ["libc", "windows-sys"] +fsext = ["libc", "windows-sys", "bstr"] fsxattr = ["xattr"] +hardware = [] lines = [] feat_systemd_logind = ["utmpx", "libc"] format = [ "bigdecimal", "extendedbigdecimal", "itertools", - "parser", + "parser-num", "num-traits", "quoting-style", + "unit-prefix", ] i18n-all = ["i18n-collator", "i18n-decimal"] i18n-common = ["icu_locale"] @@ -148,7 +150,10 @@ i18n-decimal = ["i18n-common", "icu_decimal", "icu_provider"] mode = ["libc"] perms = ["entries", "libc", "walkdir"] buf-copy = [] -parser = ["extendedbigdecimal", "glob", "num-traits"] +parser-num = ["extendedbigdecimal", "num-traits"] +parser-size = ["parser-num", "procfs"] +parser-glob = ["glob"] +parser = ["parser-num", "parser-size", "parser-glob"] pipes = [] process = ["libc"] proc-info = ["tty", "walkdir"] @@ -170,6 +175,7 @@ sum = [ "blake3", "sm3", "crc-fast", + "data-encoding", ] update-control = ["parser"] utf8 = [] diff --git a/src/uucore/build.rs b/src/uucore/build.rs index d5637ef3f..f79b3922b 100644 --- a/src/uucore/build.rs +++ b/src/uucore/build.rs @@ -31,15 +31,21 @@ pub fn main() -> Result<(), Box> { // Try to detect if we're building for a specific utility by checking build configuration // This attempts to identify individual utility builds vs multicall binary builds let target_utility = detect_target_utility(); + let locales_to_embed = get_locales_to_embed(); match target_utility { Some(util_name) => { // Embed only the specific utility's locale (cat.ftl for cat for example) - embed_single_utility_locale(&mut embedded_file, &project_root()?, &util_name)?; + embed_single_utility_locale( + &mut embedded_file, + &project_root()?, + &util_name, + &locales_to_embed, + )?; } None => { // Embed all utility locales (multicall binary or fallback) - embed_all_utility_locales(&mut embedded_file, &project_root()?)?; + embed_all_utility_locales(&mut embedded_file, &project_root()?, &locales_to_embed)?; } } @@ -118,38 +124,20 @@ fn embed_single_utility_locale( embedded_file: &mut std::fs::File, project_root: &Path, util_name: &str, + locales_to_embed: &(String, Option), ) -> Result<(), Box> { - use std::fs; - - // Embed the specific utility's locale - let locale_path = project_root - .join("src/uu") - .join(util_name) - .join("locales/en-US.ftl"); - - if locale_path.exists() { - let content = fs::read_to_string(&locale_path)?; - writeln!(embedded_file, " // Locale for {util_name}")?; - writeln!( - embedded_file, - " \"{util_name}/en-US.ftl\" => Some(r###\"{content}\"###)," - )?; - - // Tell Cargo to rerun if this file changes - println!("cargo:rerun-if-changed={}", locale_path.display()); - } + // Embed utility-specific locales + embed_component_locales(embedded_file, locales_to_embed, util_name, |locale| { + project_root + .join("src/uu") + .join(util_name) + .join(format!("locales/{locale}.ftl")) + })?; // Always embed uucore locale file if it exists - let uucore_locale_path = project_root.join("src/uucore/locales/en-US.ftl"); - if uucore_locale_path.exists() { - let content = fs::read_to_string(&uucore_locale_path)?; - writeln!(embedded_file, " // Common uucore locale")?; - writeln!( - embedded_file, - " \"uucore/en-US.ftl\" => Some(r###\"{content}\"###)," - )?; - println!("cargo:rerun-if-changed={}", uucore_locale_path.display()); - } + embed_component_locales(embedded_file, locales_to_embed, "uucore", |locale| { + project_root.join(format!("src/uucore/locales/{locale}.ftl")) + })?; Ok(()) } @@ -158,6 +146,7 @@ fn embed_single_utility_locale( fn embed_all_utility_locales( embedded_file: &mut std::fs::File, project_root: &Path, + locales_to_embed: &(String, Option), ) -> Result<(), Box> { use std::fs; @@ -166,7 +155,7 @@ fn embed_all_utility_locales( if !src_uu_dir.exists() { // When src/uu doesn't exist (e.g., standalone uucore from crates.io), // embed a static list of utility locales that are commonly used - embed_static_utility_locales(embedded_file)?; + embed_static_utility_locales(embedded_file, locales_to_embed)?; return Ok(()); } @@ -183,31 +172,17 @@ fn embed_all_utility_locales( // Embed locale files for each utility for util_name in &util_dirs { - let locale_path = src_uu_dir.join(util_name).join("locales/en-US.ftl"); - if locale_path.exists() { - let content = fs::read_to_string(&locale_path)?; - writeln!(embedded_file, " // Locale for {util_name}")?; - writeln!( - embedded_file, - " \"{util_name}/en-US.ftl\" => Some(r###\"{content}\"###)," - )?; - - // Tell Cargo to rerun if this file changes - println!("cargo:rerun-if-changed={}", locale_path.display()); - } + embed_component_locales(embedded_file, locales_to_embed, util_name, |locale| { + src_uu_dir + .join(util_name) + .join(format!("locales/{locale}.ftl")) + })?; } // Also embed uucore locale file if it exists - let uucore_locale_path = project_root.join("src/uucore/locales/en-US.ftl"); - if uucore_locale_path.exists() { - let content = fs::read_to_string(&uucore_locale_path)?; - writeln!(embedded_file, " // Common uucore locale")?; - writeln!( - embedded_file, - " \"uucore/en-US.ftl\" => Some(r###\"{content}\"###)," - )?; - println!("cargo:rerun-if-changed={}", uucore_locale_path.display()); - } + embed_component_locales(embedded_file, locales_to_embed, "uucore", |locale| { + project_root.join(format!("src/uucore/locales/{locale}.ftl")) + })?; embedded_file.flush()?; Ok(()) @@ -215,6 +190,7 @@ fn embed_all_utility_locales( fn embed_static_utility_locales( embedded_file: &mut std::fs::File, + locales_to_embed: &(String, Option), ) -> Result<(), Box> { use std::env; @@ -229,15 +205,9 @@ fn embed_static_utility_locales( }; // First, try to embed uucore locales - critical for common translations like "Usage:" - let uucore_locale_file = Path::new(&manifest_dir).join("locales/en-US.ftl"); - if uucore_locale_file.is_file() { - let content = std::fs::read_to_string(&uucore_locale_file)?; - writeln!(embedded_file, " // Common uucore locale")?; - writeln!( - embedded_file, - " \"uucore/en-US.ftl\" => Some(r###\"{content}\"###)," - )?; - } + embed_component_locales(embedded_file, locales_to_embed, "uucore", |locale| { + Path::new(&manifest_dir).join(format!("locales/{locale}.ftl")) + })?; // Collect and sort for deterministic builds let mut entries: Vec<_> = std::fs::read_dir(registry_dir)? @@ -251,15 +221,12 @@ fn embed_static_utility_locales( // Match uu_- if let Some((util_part, _)) = dir_name.split_once('-') { if let Some(util_name) = util_part.strip_prefix("uu_") { - let locale_file = entry.path().join("locales/en-US.ftl"); - if locale_file.is_file() { - let content = std::fs::read_to_string(&locale_file)?; - writeln!(embedded_file, " // Locale for {util_name}")?; - writeln!( - embedded_file, - " \"{util_name}/en-US.ftl\" => Some(r###\"{content}\"###)," - )?; - } + embed_component_locales( + embedded_file, + locales_to_embed, + util_name, + |locale| entry.path().join(format!("locales/{locale}.ftl")), + )?; } } } @@ -267,3 +234,251 @@ fn embed_static_utility_locales( Ok(()) } + +/// Determines which locales to embed into the binary. +/// +/// To support localized messages in installed binaries (e.g., via `cargo install`), +/// this function identifies the user's current locale from the `LANG` environment +/// variable. +/// +/// It always includes "en-US" to ensure that a fallback is available if the +/// system locale's translation file is missing or if `LANG` is not set. +fn get_locales_to_embed() -> (String, Option) { + let system_locale = env::var("LANG").ok().and_then(|lang| { + let locale = lang.split('.').next()?.replace('_', "-"); + if locale != "en-US" && !locale.is_empty() { + Some(locale) + } else { + None + } + }); + ("en-US".to_string(), system_locale) +} + +/// Helper function to iterate over the locales to embed. +fn for_each_locale( + locales: &(String, Option), + mut f: F, +) -> Result<(), Box> +where + F: FnMut(&str) -> Result<(), Box>, +{ + f(&locales.0)?; + if let Some(ref system_locale) = locales.1 { + f(system_locale)?; + } + Ok(()) +} + +/// Helper function to embed a single locale file. +fn embed_locale_file( + embedded_file: &mut std::fs::File, + locale_path: &Path, + locale_key: &str, + locale: &str, + component: &str, +) -> Result<(), Box> { + use std::fs; + + if locale_path.exists() || locale_path.is_file() { + let content = fs::read_to_string(locale_path)?; + writeln!( + embedded_file, + " // Locale for {component} ({locale})" + )?; + writeln!( + embedded_file, + " \"{locale_key}\" => Some(r###\"{content}\"###)," + )?; + + // Tell Cargo to rerun if this file changes + println!("cargo:rerun-if-changed={}", locale_path.display()); + } + Ok(()) +} + +/// Higher-level helper to embed locale files for a component with a path pattern. +/// This eliminates the repetitive for_each_locale + embed_locale_file pattern. +fn embed_component_locales( + embedded_file: &mut std::fs::File, + locales: &(String, Option), + component_name: &str, + path_builder: F, +) -> Result<(), Box> +where + F: Fn(&str) -> std::path::PathBuf, +{ + for_each_locale(locales, |locale| { + let locale_path = path_builder(locale); + embed_locale_file( + embedded_file, + &locale_path, + &format!("{component_name}/{locale}.ftl"), + locale, + component_name, + ) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_locales_to_embed_no_lang() { + unsafe { + env::remove_var("LANG"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, None); + + unsafe { + env::set_var("LANG", ""); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, None); + unsafe { + env::remove_var("LANG"); + } + + unsafe { + env::set_var("LANG", "en_US.UTF-8"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, None); + unsafe { + env::remove_var("LANG"); + } + } + + #[test] + fn get_locales_to_embed_with_lang() { + unsafe { + env::set_var("LANG", "fr_FR.UTF-8"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, Some("fr-FR".to_string())); + unsafe { + env::remove_var("LANG"); + } + + unsafe { + env::set_var("LANG", "zh_CN.UTF-8"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, Some("zh-CN".to_string())); + unsafe { + env::remove_var("LANG"); + } + + unsafe { + env::set_var("LANG", "de"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, Some("de".to_string())); + unsafe { + env::remove_var("LANG"); + } + } + + #[test] + fn get_locales_to_embed_invalid_lang() { + // invalid locale format + unsafe { + env::set_var("LANG", "invalid"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, Some("invalid".to_string())); + unsafe { + env::remove_var("LANG"); + } + + // numeric values + unsafe { + env::set_var("LANG", "123"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, Some("123".to_string())); + unsafe { + env::remove_var("LANG"); + } + + // special characters + unsafe { + env::set_var("LANG", "@@@@"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, Some("@@@@".to_string())); + unsafe { + env::remove_var("LANG"); + } + + // malformed locale (no country code but with encoding) + unsafe { + env::set_var("LANG", "en.UTF-8"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, Some("en".to_string())); + unsafe { + env::remove_var("LANG"); + } + + // valid format but unusual locale + unsafe { + env::set_var("LANG", "XX_YY.UTF-8"); + } + let (en_locale, system_locale) = get_locales_to_embed(); + assert_eq!(en_locale, "en-US"); + assert_eq!(system_locale, Some("XX-YY".to_string())); + unsafe { + env::remove_var("LANG"); + } + } + + #[test] + fn for_each_locale_basic() { + let locales = ("en-US".to_string(), Some("fr-FR".to_string())); + let mut collected = Vec::new(); + + for_each_locale(&locales, |locale| { + collected.push(locale.to_string()); + Ok(()) + }) + .unwrap(); + + assert_eq!(collected, vec!["en-US", "fr-FR"]); + } + + #[test] + fn for_each_locale_no_system_locale() { + let locales = ("en-US".to_string(), None); + let mut collected = Vec::new(); + + for_each_locale(&locales, |locale| { + collected.push(locale.to_string()); + Ok(()) + }) + .unwrap(); + + assert_eq!(collected, vec!["en-US"]); + } + + #[test] + fn for_each_locale_error_handling() { + let locales = ("en-US".to_string(), Some("fr-FR".to_string())); + + let result = for_each_locale(&locales, |_locale| Err("test error".into())); + + assert!(result.is_err()); + } +} diff --git a/src/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl index 09fb45783..384e4a83d 100644 --- a/src/uucore/locales/en-US.ftl +++ b/src/uucore/locales/en-US.ftl @@ -29,6 +29,7 @@ error-io = I/O error error-permission-denied = Permission denied error-file-not-found = No such file or directory error-invalid-argument = Invalid argument +error-is-a-directory = { $file }: Is a directory # Common actions action-copying = copying @@ -54,3 +55,21 @@ safe-traversal-error-unlink-failed = failed to unlink '{ $path }': { $source } safe-traversal-error-invalid-fd = invalid file descriptor safe-traversal-current-directory = safe-traversal-directory = + +# checksum-related messages +checksum-no-properly-formatted = { $checksum_file }: no properly formatted checksum lines found +checksum-no-file-verified = { $checksum_file }: no file was verified +checksum-error-failed-to-read-input = failed to read input +checksum-bad-format = { $count -> + [1] { $count } line is improperly formatted + *[other] { $count } lines are improperly formatted +} +checksum-failed-cksum = { $count -> + [1] { $count } computed checksum did NOT match + *[other] { $count } computed checksums did NOT match +} +checksum-failed-open-file = { $count -> + [1] { $count } listed file could not be read + *[other] { $count } listed files could not be read +} +checksum-error-algo-bad-format = { $file }: { $line }: improperly formatted { $algo } checksum line diff --git a/src/uucore/locales/fr-FR.ftl b/src/uucore/locales/fr-FR.ftl index a8a344688..4c844e9b1 100644 --- a/src/uucore/locales/fr-FR.ftl +++ b/src/uucore/locales/fr-FR.ftl @@ -29,6 +29,7 @@ error-io = Erreur E/S error-permission-denied = Permission refusée error-file-not-found = Aucun fichier ou répertoire de ce type error-invalid-argument = Argument invalide +error-is-a-directory = { $file }: Est un répertoire # Actions communes action-copying = copie @@ -54,3 +55,21 @@ safe-traversal-error-unlink-failed = échec de la suppression de '{ $path }' : { safe-traversal-error-invalid-fd = descripteur de fichier invalide safe-traversal-current-directory = safe-traversal-directory = + +# Messages relatifs au module checksum +checksum-no-properly-formatted = { $checksum_file }: aucune ligne correctement formattée n'a été trouvée +checksum-no-file-verified = { $checksum_file }: aucun fichier n'a été vérifié +checksum-error-failed-to-read-input = échec de la lecture de l'entrée +checksum-bad-format = { $count -> + [1] { $count } ligne invalide + *[other] { $count } lignes invalides +} +checksum-failed-cksum = { $count -> + [1] { $count } somme de hachage ne correspond PAS + *[other] { $count } sommes de hachage ne correspondent PAS +} +checksum-failed-open-file = { $count -> + [1] { $count } fichier passé n'a pas pu être lu + *[other] { $count } fichiers passés n'ont pas pu être lu +} +checksum-error-algo-bad-format = { $file }: { $line }: ligne invalide pour { $algo } diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index ac03fb79d..548f7f2bc 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -32,7 +32,12 @@ pub mod fsext; pub mod i18n; #[cfg(feature = "lines")] pub mod lines; -#[cfg(feature = "parser")] +#[cfg(any( + feature = "parser", + feature = "parser-num", + feature = "parser-size", + feature = "parser-glob" +))] pub mod parser; #[cfg(feature = "quoting-style")] pub mod quoting_style; @@ -74,6 +79,8 @@ pub mod tty; #[cfg(all(unix, feature = "fsxattr"))] pub mod fsxattr; +#[cfg(feature = "hardware")] +pub mod hardware; #[cfg(all(target_os = "linux", feature = "selinux"))] pub mod selinux; #[cfg(all(unix, not(target_os = "fuchsia"), feature = "signals"))] diff --git a/src/uucore/src/lib/features/checksum/compute.rs b/src/uucore/src/lib/features/checksum/compute.rs new file mode 100644 index 000000000..956c1e4c1 --- /dev/null +++ b/src/uucore/src/lib/features/checksum/compute.rs @@ -0,0 +1,324 @@ +// 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 bitlen + +use std::ffi::OsStr; +use std::fs::File; +use std::io::{self, BufReader, Read, Write}; +use std::path::Path; + +use crate::checksum::{ChecksumError, SizedAlgoKind, digest_reader, escape_filename}; +use crate::error::{FromIo, UResult, USimpleError}; +use crate::line_ending::LineEnding; +use crate::sum::DigestOutput; +use crate::{show, translate}; + +/// Use the same buffer size as GNU when reading a file to create a checksum +/// from it: 32 KiB. +const READ_BUFFER_SIZE: usize = 32 * 1024; + +pub struct ChecksumComputeOptions { + /// Which algorithm to use to compute the digest. + pub algo_kind: SizedAlgoKind, + + /// Printing format to use for each checksum. + pub output_format: OutputFormat, + + /// Whether to finish lines with '\n' or '\0'. + pub line_ending: LineEnding, + + /// On windows, open files as binary instead of text + pub binary: bool, + + /// (non-GNU option) Do not print file names + pub no_names: bool, +} + +/// Reading mode used to compute digest. +/// +/// On most linux systems, this is irrelevant, as there is no distinction +/// between text and binary files. Refer to GNU's cksum documentation for more +/// information. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReadingMode { + Binary, + Text, +} + +impl ReadingMode { + #[inline] + fn as_char(&self) -> char { + match self { + Self::Binary => '*', + Self::Text => ' ', + } + } +} + +/// Whether to write the digest as hexadecimal or encoded in base64. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DigestFormat { + Hexadecimal, + Base64, +} + +impl DigestFormat { + #[inline] + fn is_base64(&self) -> bool { + *self == Self::Base64 + } +} + +/// Holds the representation that shall be used for printing a checksum line +#[derive(Debug, PartialEq, Eq)] +pub enum OutputFormat { + /// Raw digest + Raw, + + /// Selected for older algorithms which had their custom formatting + /// + /// Default for crc, sysv, bsd + Legacy, + + /// `$ALGO_NAME ($FILENAME) = $DIGEST` + Tagged(DigestFormat), + + /// '$DIGEST $FLAG$FILENAME' + /// where 'flag' depends on the reading mode + /// + /// Default for standalone checksum utilities + Untagged(DigestFormat, ReadingMode), +} + +impl OutputFormat { + #[inline] + fn is_raw(&self) -> bool { + *self == Self::Raw + } +} + +/// Use already-processed arguments to decide the output format. +pub fn figure_out_output_format( + algo: SizedAlgoKind, + tag: bool, + binary: bool, + raw: bool, + base64: bool, +) -> OutputFormat { + // Raw output format takes precedence over anything else. + if raw { + return OutputFormat::Raw; + } + + // Then, if the algo is legacy, takes precedence over the rest + if algo.is_legacy() { + return OutputFormat::Legacy; + } + + let digest_format = if base64 { + DigestFormat::Base64 + } else { + DigestFormat::Hexadecimal + }; + + // After that, decide between tagged and untagged output + if tag { + OutputFormat::Tagged(digest_format) + } else { + let reading_mode = if binary { + ReadingMode::Binary + } else { + ReadingMode::Text + }; + OutputFormat::Untagged(digest_format, reading_mode) + } +} + +fn print_legacy_checksum( + options: &ChecksumComputeOptions, + filename: &OsStr, + sum: &DigestOutput, + size: usize, +) -> UResult<()> { + debug_assert!(options.algo_kind.is_legacy()); + debug_assert!(matches!(sum, DigestOutput::U16(_) | DigestOutput::Crc(_))); + + let (escaped_filename, prefix) = if options.line_ending == LineEnding::Nul { + (filename.to_string_lossy().to_string(), "") + } else { + escape_filename(filename) + }; + + // Print the sum + match (options.algo_kind, sum) { + (SizedAlgoKind::Sysv, DigestOutput::U16(sum)) => print!( + "{prefix}{sum} {}", + size.div_ceil(options.algo_kind.bitlen()), + ), + (SizedAlgoKind::Bsd, DigestOutput::U16(sum)) => { + // The BSD checksum output is 5 digit integer + let bsd_width = 5; + print!( + "{prefix}{sum:0bsd_width$} {:bsd_width$}", + size.div_ceil(options.algo_kind.bitlen()), + ); + } + (SizedAlgoKind::Crc | SizedAlgoKind::Crc32b, DigestOutput::Crc(sum)) => { + print!("{prefix}{sum} {size}"); + } + (algo, output) => unreachable!("Bug: Invalid legacy checksum ({algo:?}, {output:?})"), + } + + // Print the filename after a space if not stdin + if escaped_filename != "-" { + print!(" "); + let _dropped_result = io::stdout().write_all(escaped_filename.as_bytes()); + } + + Ok(()) +} + +fn print_tagged_checksum( + options: &ChecksumComputeOptions, + filename: &OsStr, + sum: &String, +) -> UResult<()> { + let (escaped_filename, prefix) = if options.line_ending == LineEnding::Nul { + (filename.to_string_lossy().to_string(), "") + } else { + escape_filename(filename) + }; + + // Print algo name and opening parenthesis. + print!("{prefix}{} (", options.algo_kind.to_tag()); + + // Print filename + let _dropped_result = io::stdout().write_all(escaped_filename.as_bytes()); + + // Print closing parenthesis and sum + print!(") = {sum}"); + + Ok(()) +} + +fn print_untagged_checksum( + options: &ChecksumComputeOptions, + filename: &OsStr, + sum: &String, + reading_mode: ReadingMode, +) -> UResult<()> { + // early check for the "no-names" option + if options.no_names { + print!("{sum}"); + return Ok(()); + } + + let (escaped_filename, prefix) = if options.line_ending == LineEnding::Nul { + (filename.to_string_lossy().to_string(), "") + } else { + escape_filename(filename) + }; + + // Print checksum and reading mode flag + print!("{prefix}{sum} {}", reading_mode.as_char()); + + // Print filename + let _dropped_result = io::stdout().write_all(escaped_filename.as_bytes()); + + Ok(()) +} + +/// Calculate checksum +/// +/// # 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 +pub fn perform_checksum_computation<'a, I>(options: ChecksumComputeOptions, files: I) -> UResult<()> +where + I: Iterator, +{ + let mut files = files.peekable(); + + while let Some(filename) = files.next() { + // Check that in raw mode, we are not provided with several files. + if options.output_format.is_raw() && files.peek().is_some() { + return Err(Box::new(ChecksumError::RawMultipleFiles)); + } + + let filepath = Path::new(filename); + let stdin_buf; + let file_buf; + if filepath.is_dir() { + show!(USimpleError::new( + 1, + translate!("error-is-a-directory", "file" => filepath.display()) + )); + continue; + } + + // Handle the file input + let mut file = BufReader::with_capacity( + READ_BUFFER_SIZE, + if filename == "-" { + stdin_buf = io::stdin(); + Box::new(stdin_buf) as Box + } else { + file_buf = match File::open(filepath) { + Ok(file) => file, + Err(err) => { + show!(err.map_err_context(|| filepath.to_string_lossy().into())); + continue; + } + }; + Box::new(file_buf) as Box + }, + ); + + let mut digest = options.algo_kind.create_digest(); + + let (digest_output, sz) = digest_reader(&mut digest, &mut file, options.binary) + .map_err_context(|| translate!("checksum-error-failed-to-read-input"))?; + + // Encodes the sum if df is Base64, leaves as-is otherwise. + let encode_sum = |sum: DigestOutput, df: DigestFormat| { + if df.is_base64() { + sum.to_base64() + } else { + sum.to_hex() + } + }; + + match options.output_format { + OutputFormat::Raw => { + // Cannot handle multiple files anyway, output immediately. + digest_output.write_raw(io::stdout())?; + return Ok(()); + } + OutputFormat::Legacy => { + print_legacy_checksum(&options, filename, &digest_output, sz)?; + } + OutputFormat::Tagged(digest_format) => { + print_tagged_checksum( + &options, + filename, + &encode_sum(digest_output, digest_format)?, + )?; + } + OutputFormat::Untagged(digest_format, reading_mode) => { + print_untagged_checksum( + &options, + filename, + &encode_sum(digest_output, digest_format)?, + reading_mode, + )?; + } + } + + print!("{}", options.line_ending); + } + Ok(()) +} diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs new file mode 100644 index 000000000..5339f833f --- /dev/null +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -0,0 +1,605 @@ +// 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 bitlen + +use std::ffi::OsStr; +use std::io::{self, Read}; +use std::num::IntErrorKind; + +use os_display::Quotable; +use thiserror::Error; + +use crate::error::{UError, UResult}; +use crate::show_error; +use crate::sum::{ + Blake2b, Blake3, Bsd, CRC32B, Crc, Digest, DigestOutput, DigestWriter, Md5, Sha1, Sha3_224, + Sha3_256, Sha3_384, Sha3_512, Sha224, Sha256, Sha384, Sha512, Shake128, Shake256, Sm3, SysV, +}; + +pub mod compute; +pub mod validate; + +pub const ALGORITHM_OPTIONS_SYSV: &str = "sysv"; +pub const ALGORITHM_OPTIONS_BSD: &str = "bsd"; +pub const ALGORITHM_OPTIONS_CRC: &str = "crc"; +pub const ALGORITHM_OPTIONS_CRC32B: &str = "crc32b"; +pub const ALGORITHM_OPTIONS_MD5: &str = "md5"; +pub const ALGORITHM_OPTIONS_SHA1: &str = "sha1"; +pub const ALGORITHM_OPTIONS_SHA2: &str = "sha2"; +pub const ALGORITHM_OPTIONS_SHA3: &str = "sha3"; + +pub const ALGORITHM_OPTIONS_SHA224: &str = "sha224"; +pub const ALGORITHM_OPTIONS_SHA256: &str = "sha256"; +pub const ALGORITHM_OPTIONS_SHA384: &str = "sha384"; +pub const ALGORITHM_OPTIONS_SHA512: &str = "sha512"; +pub const ALGORITHM_OPTIONS_BLAKE2B: &str = "blake2b"; +pub const ALGORITHM_OPTIONS_BLAKE3: &str = "blake3"; +pub const ALGORITHM_OPTIONS_SM3: &str = "sm3"; +pub const ALGORITHM_OPTIONS_SHAKE128: &str = "shake128"; +pub const ALGORITHM_OPTIONS_SHAKE256: &str = "shake256"; + +pub const SUPPORTED_ALGORITHMS: [&str; 17] = [ + ALGORITHM_OPTIONS_SYSV, + ALGORITHM_OPTIONS_BSD, + ALGORITHM_OPTIONS_CRC, + ALGORITHM_OPTIONS_CRC32B, + ALGORITHM_OPTIONS_MD5, + ALGORITHM_OPTIONS_SHA1, + ALGORITHM_OPTIONS_SHA2, + ALGORITHM_OPTIONS_SHA3, + ALGORITHM_OPTIONS_BLAKE2B, + ALGORITHM_OPTIONS_SM3, + // Legacy aliases for -a sha2 -l xxx + ALGORITHM_OPTIONS_SHA224, + ALGORITHM_OPTIONS_SHA256, + ALGORITHM_OPTIONS_SHA384, + ALGORITHM_OPTIONS_SHA512, + // Extra algorithms that are not valid `cksum --algorithm` as per GNU. + // TODO: Should we keep them or drop them to align our support with GNU ? + ALGORITHM_OPTIONS_BLAKE3, + ALGORITHM_OPTIONS_SHAKE128, + ALGORITHM_OPTIONS_SHAKE256, +]; + +/// Represents an algorithm kind. In some cases, it is not sufficient by itself +/// to know which algorithm to use exactly, because it lacks a digest length, +/// which is why [`SizedAlgoKind`] exists. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AlgoKind { + Sysv, + Bsd, + Crc, + Crc32b, + Md5, + Sm3, + Sha1, + Sha2, + Sha3, + Blake2b, + + // Available in cksum for backward compatibility + Sha224, + Sha256, + Sha384, + Sha512, + + // Not available in cksum + Shake128, + Shake256, + Blake3, +} + +impl AlgoKind { + /// Parses an [`AlgoKind`] from a string, only accepting valid cksum + /// `--algorithm` values. + pub fn from_cksum(algo: impl AsRef) -> UResult { + use AlgoKind::*; + Ok(match algo.as_ref() { + ALGORITHM_OPTIONS_SYSV => Sysv, + ALGORITHM_OPTIONS_BSD => Bsd, + ALGORITHM_OPTIONS_CRC => Crc, + ALGORITHM_OPTIONS_CRC32B => Crc32b, + ALGORITHM_OPTIONS_MD5 => Md5, + ALGORITHM_OPTIONS_SHA1 => Sha1, + ALGORITHM_OPTIONS_SHA2 => Sha2, + ALGORITHM_OPTIONS_SHA3 => Sha3, + ALGORITHM_OPTIONS_BLAKE2B => Blake2b, + ALGORITHM_OPTIONS_SM3 => Sm3, + + // For backward compatibility + ALGORITHM_OPTIONS_SHA224 => Sha224, + ALGORITHM_OPTIONS_SHA256 => Sha256, + ALGORITHM_OPTIONS_SHA384 => Sha384, + ALGORITHM_OPTIONS_SHA512 => Sha512, + _ => return Err(ChecksumError::UnknownAlgorithm(algo.as_ref().to_string()).into()), + }) + } + + /// Parses an algo kind from a string, accepting standalone binary names. + pub fn from_bin_name(algo: impl AsRef) -> UResult { + use AlgoKind::*; + Ok(match algo.as_ref() { + "md5sum" => Md5, + "sha1sum" => Sha1, + "sha224sum" => Sha224, + "sha256sum" => Sha256, + "sha384sum" => Sha384, + "sha512sum" => Sha512, + "sha3sum" => Sha3, + "b2sum" => Blake2b, + + _ => return Err(ChecksumError::UnknownAlgorithm(algo.as_ref().to_string()).into()), + }) + } + + /// Returns a string corresponding to the algorithm kind. + pub fn to_uppercase(self) -> &'static str { + use AlgoKind::*; + match self { + // Legacy algorithms + Sysv => "SYSV", + Bsd => "BSD", + Crc => "CRC", + Crc32b => "CRC32B", + + Md5 => "MD5", + Sm3 => "SM3", + Sha1 => "SHA1", + Sha2 => "SHA2", + Sha3 => "SHA3", + Blake2b => "BLAKE2b", // Note the lowercase b in the end here. + + // For backward compatibility + Sha224 => "SHA224", + Sha256 => "SHA256", + Sha384 => "SHA384", + Sha512 => "SHA512", + + Shake128 => "SHAKE128", + Shake256 => "SHAKE256", + Blake3 => "BLAKE3", + } + } + + /// Returns a string corresponding to the algorithm option in cksum `-a` + pub fn to_lowercase(self) -> &'static str { + use AlgoKind::*; + match self { + Sysv => "sysv", + Bsd => "bsd", + Crc => "crc", + Crc32b => "crc32b", + Md5 => "md5", + Sm3 => "sm3", + Sha1 => "sha1", + Sha2 => "sha2", + Sha3 => "sha3", + Blake2b => "blake2b", + + // For backward compatibility + Sha224 => "sha224", + Sha256 => "sha256", + Sha384 => "sha384", + Sha512 => "sha512", + + Shake128 => "shake128", + Shake256 => "shake256", + Blake3 => "blake3", + } + } + + pub fn is_legacy(self) -> bool { + use AlgoKind::*; + matches!(self, Sysv | Bsd | Crc | Crc32b) + } +} + +/// Holds a length for a SHA2 of SHA3 algorithm kind. +#[derive(Debug, Clone, Copy)] +pub enum ShaLength { + Len224, + Len256, + Len384, + Len512, +} + +impl ShaLength { + pub fn as_usize(self) -> usize { + match self { + Self::Len224 => 224, + Self::Len256 => 256, + Self::Len384 => 384, + Self::Len512 => 512, + } + } +} + +impl TryFrom for ShaLength { + type Error = ChecksumError; + + fn try_from(value: usize) -> Result { + use ShaLength::*; + match value { + 224 => Ok(Len224), + 256 => Ok(Len256), + 384 => Ok(Len384), + 512 => Ok(Len512), + _ => Err(ChecksumError::InvalidLengthForSha(value.to_string())), + } + } +} + +/// Represents an actual determined algorithm. +#[derive(Debug, Clone, Copy)] +pub enum SizedAlgoKind { + Sysv, + Bsd, + Crc, + Crc32b, + Md5, + Sm3, + Sha1, + Blake3, + Sha2(ShaLength), + Sha3(ShaLength), + // Note: we store Blake2b's length as BYTES. + Blake2b(Option), + Shake128(usize), + Shake256(usize), +} + +impl SizedAlgoKind { + pub fn from_unsized(kind: AlgoKind, byte_length: Option) -> UResult { + use AlgoKind as ak; + match (kind, byte_length) { + ( + ak::Sysv + | ak::Bsd + | ak::Crc + | ak::Crc32b + | ak::Md5 + | ak::Sm3 + | ak::Sha1 + | ak::Blake3 + | ak::Sha224 + | ak::Sha256 + | ak::Sha384 + | ak::Sha512, + Some(_), + ) => Err(ChecksumError::LengthOnlyForBlake2bSha2Sha3.into()), + + (ak::Sysv, _) => Ok(Self::Sysv), + (ak::Bsd, _) => Ok(Self::Bsd), + (ak::Crc, _) => Ok(Self::Crc), + (ak::Crc32b, _) => Ok(Self::Crc32b), + (ak::Md5, _) => Ok(Self::Md5), + (ak::Sm3, _) => Ok(Self::Sm3), + (ak::Sha1, _) => Ok(Self::Sha1), + (ak::Blake3, _) => Ok(Self::Blake3), + + (ak::Shake128, Some(l)) => Ok(Self::Shake128(l)), + (ak::Shake256, Some(l)) => Ok(Self::Shake256(l)), + (ak::Sha2, Some(l)) => Ok(Self::Sha2(ShaLength::try_from(l)?)), + (ak::Sha3, Some(l)) => Ok(Self::Sha3(ShaLength::try_from(l)?)), + (algo @ (ak::Sha2 | ak::Sha3), None) => { + Err(ChecksumError::LengthRequiredForSha(algo.to_lowercase().into()).into()) + } + // [`calculate_blake2b_length`] expects a length in bits but we + // have a length in bytes. + (ak::Blake2b, Some(l)) => Ok(Self::Blake2b(calculate_blake2b_length(8 * l)?)), + (ak::Blake2b, None) => Ok(Self::Blake2b(None)), + + (ak::Sha224, None) => Ok(Self::Sha2(ShaLength::Len224)), + (ak::Sha256, None) => Ok(Self::Sha2(ShaLength::Len256)), + (ak::Sha384, None) => Ok(Self::Sha2(ShaLength::Len384)), + (ak::Sha512, None) => Ok(Self::Sha2(ShaLength::Len512)), + (_, None) => Err(ChecksumError::LengthRequired(kind.to_uppercase().into()).into()), + } + } + + pub fn to_tag(self) -> String { + use SizedAlgoKind::*; + match self { + Md5 => "MD5".into(), + Sm3 => "SM3".into(), + Sha1 => "SHA1".into(), + Blake3 => "BLAKE3".into(), + Sha2(len) => format!("SHA{}", len.as_usize()), + Sha3(len) => format!("SHA3-{}", len.as_usize()), + Blake2b(Some(byte_len)) => format!("BLAKE2b-{}", byte_len * 8), + Blake2b(None) => "BLAKE2b".into(), + Shake128(_) => "SHAKE128".into(), + Shake256(_) => "SHAKE256".into(), + Sysv | Bsd | Crc | Crc32b => panic!("Should not be used for tagging"), + } + } + + pub fn create_digest(&self) -> Box { + use ShaLength::*; + match self { + Self::Sysv => Box::new(SysV::new()), + Self::Bsd => Box::new(Bsd::new()), + Self::Crc => Box::new(Crc::new()), + Self::Crc32b => Box::new(CRC32B::new()), + Self::Md5 => Box::new(Md5::new()), + Self::Sm3 => Box::new(Sm3::new()), + Self::Sha1 => Box::new(Sha1::new()), + Self::Blake3 => Box::new(Blake3::new()), + Self::Sha2(Len224) => Box::new(Sha224::new()), + Self::Sha2(Len256) => Box::new(Sha256::new()), + Self::Sha2(Len384) => Box::new(Sha384::new()), + Self::Sha2(Len512) => Box::new(Sha512::new()), + Self::Sha3(Len224) => Box::new(Sha3_224::new()), + Self::Sha3(Len256) => Box::new(Sha3_256::new()), + Self::Sha3(Len384) => Box::new(Sha3_384::new()), + Self::Sha3(Len512) => Box::new(Sha3_512::new()), + Self::Blake2b(Some(byte_len)) => Box::new(Blake2b::with_output_bytes(*byte_len)), + Self::Blake2b(None) => Box::new(Blake2b::new()), + Self::Shake128(_) => Box::new(Shake128::new()), + Self::Shake256(_) => Box::new(Shake256::new()), + } + } + + pub fn bitlen(&self) -> usize { + use SizedAlgoKind::*; + match self { + Sysv => 512, + Bsd => 1024, + Crc => 256, + Crc32b => 32, + Md5 => 128, + Sm3 => 512, + Sha1 => 160, + Blake3 => 256, + Sha2(len) => len.as_usize(), + Sha3(len) => len.as_usize(), + Blake2b(len) => len.unwrap_or(512), + Shake128(len) => *len, + Shake256(len) => *len, + } + } + pub fn is_legacy(&self) -> bool { + use SizedAlgoKind::*; + matches!(self, Sysv | Bsd | Crc | Crc32b) + } +} + +#[derive(Debug, Error)] +pub enum ChecksumError { + #[error("the --raw option is not supported with multiple files")] + RawMultipleFiles, + #[error("the --ignore-missing option is meaningful only when verifying checksums")] + IgnoreNotCheck, + #[error("the --strict option is meaningful only when verifying checksums")] + StrictNotCheck, + #[error("the --quiet option is meaningful only when verifying checksums")] + QuietNotCheck, + + // --length sanitization errors + #[error("--length required for {}", .0.quote())] + LengthRequired(String), + #[error("invalid length: {}", .0.quote())] + InvalidLength(String), + #[error("maximum digest length for {} is 512 bits", .0.quote())] + LengthTooBigForBlake(String), + #[error("length is not a multiple of 8")] + LengthNotMultipleOf8, + #[error("digest length for {} must be 224, 256, 384, or 512", .0.quote())] + InvalidLengthForSha(String), + #[error("--algorithm={0} requires specifying --length 224, 256, 384, or 512")] + LengthRequiredForSha(String), + #[error("--length is only supported with --algorithm blake2b, sha2, or sha3")] + LengthOnlyForBlake2bSha2Sha3, + + #[error("the --binary and --text options are meaningless when verifying checksums")] + BinaryTextConflict, + #[error("--text mode is only supported with --untagged")] + TextWithoutUntagged, + #[error("--check is not supported with --algorithm={{bsd,sysv,crc,crc32b}}")] + AlgorithmNotSupportedWithCheck, + #[error("You cannot combine multiple hash algorithms!")] + CombineMultipleAlgorithms, + #[error("Needs an algorithm to hash with.\nUse --help for more information.")] + NeedAlgorithmToHash, + #[error("unknown algorithm: {0}: clap should have prevented this case")] + UnknownAlgorithm(String), + #[error("")] + Io(#[from] io::Error), +} + +impl UError for ChecksumError { + fn code(&self) -> i32 { + 1 + } +} + +pub fn digest_reader( + digest: &mut Box, + reader: &mut T, + binary: bool, +) -> io::Result<(DigestOutput, 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, binary); + let output_size = std::io::copy(reader, &mut digest_writer)? as usize; + digest_writer.finalize(); + + Ok((digest.result(), output_size)) +} + +/// Calculates the length of the digest. +pub fn calculate_blake2b_length(bit_length: usize) -> UResult> { + calculate_blake2b_length_str(bit_length.to_string().as_str()) +} + +/// Calculates the length of the digest. +pub fn calculate_blake2b_length_str(bit_length: &str) -> UResult> { + // Blake2b's length is parsed in an u64. + match bit_length.parse::() { + Ok(0) => Ok(None), + + // Error cases + Ok(n) if n > 512 => { + show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); + Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) + } + Err(e) if *e.kind() == IntErrorKind::PosOverflow => { + show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); + Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) + } + Err(_) => Err(ChecksumError::InvalidLength(bit_length.into()).into()), + + Ok(n) if n % 8 != 0 => { + show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); + Err(ChecksumError::LengthNotMultipleOf8.into()) + } + + // Valid cases + + // When length is 512, it is blake2b's default. So, don't show it + Ok(512) => Ok(None), + // Divide by 8, as our blake2b implementation expects bytes instead of bits. + Ok(n) => Ok(Some(n / 8)), + } +} + +pub fn validate_sha2_sha3_length(algo_name: AlgoKind, length: Option) -> UResult { + match length { + Some(224) => Ok(ShaLength::Len224), + Some(256) => Ok(ShaLength::Len256), + Some(384) => Ok(ShaLength::Len384), + Some(512) => Ok(ShaLength::Len512), + Some(len) => { + show_error!("{}", ChecksumError::InvalidLength(len.to_string())); + Err(ChecksumError::InvalidLengthForSha(algo_name.to_uppercase().into()).into()) + } + None => Err(ChecksumError::LengthRequiredForSha(algo_name.to_lowercase().into()).into()), + } +} + +pub fn sanitize_sha2_sha3_length_str(algo_kind: AlgoKind, length: &str) -> UResult { + // There is a difference in the errors sent when the length is not a number + // vs. its an invalid number. + // + // When inputting an invalid number, an extra error message it printed to + // remind of the accepted inputs. + let len = match length.parse::() { + Ok(l) => l, + // Note: Positive overflow while parsing counts as an invalid number, + // but a number still. + Err(e) if *e.kind() == IntErrorKind::PosOverflow => { + show_error!("{}", ChecksumError::InvalidLength(length.into())); + return Err(ChecksumError::InvalidLengthForSha(algo_kind.to_uppercase().into()).into()); + } + Err(_) => return Err(ChecksumError::InvalidLength(length.into()).into()), + }; + + if [224, 256, 384, 512].contains(&len) { + Ok(len) + } else { + show_error!("{}", ChecksumError::InvalidLength(length.into())); + Err(ChecksumError::InvalidLengthForSha(algo_kind.to_uppercase().into()).into()) + } +} + +pub fn unescape_filename(filename: &[u8]) -> (Vec, &'static str) { + let mut unescaped = Vec::with_capacity(filename.len()); + let mut byte_iter = filename.iter().peekable(); + loop { + let Some(byte) = byte_iter.next() else { + break; + }; + if *byte == b'\\' { + match byte_iter.next() { + Some(b'\\') => unescaped.push(b'\\'), + Some(b'n') => unescaped.push(b'\n'), + Some(b'r') => unescaped.push(b'\r'), + Some(x) => { + unescaped.push(b'\\'); + unescaped.push(*x); + } + _ => {} + } + } else { + unescaped.push(*byte); + } + } + let prefix = if unescaped == filename { "" } else { "\\" }; + (unescaped, prefix) +} + +pub fn escape_filename(filename: &OsStr) -> (String, &'static str) { + let original = filename.to_string_lossy(); + let escaped = original + .replace('\\', "\\\\") + .replace('\n', "\\n") + .replace('\r', "\\r"); + let prefix = if escaped == original { "" } else { "\\" }; + (escaped, prefix) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_unescape_filename() { + let (unescaped, prefix) = unescape_filename(b"test\\nfile.txt"); + assert_eq!(unescaped, b"test\nfile.txt"); + assert_eq!(prefix, "\\"); + let (unescaped, prefix) = unescape_filename(b"test\\nfile.txt"); + assert_eq!(unescaped, b"test\nfile.txt"); + assert_eq!(prefix, "\\"); + + let (unescaped, prefix) = unescape_filename(b"test\\rfile.txt"); + assert_eq!(unescaped, b"test\rfile.txt"); + assert_eq!(prefix, "\\"); + + let (unescaped, prefix) = unescape_filename(b"test\\\\file.txt"); + assert_eq!(unescaped, b"test\\file.txt"); + assert_eq!(prefix, "\\"); + } + + #[test] + fn test_escape_filename() { + let (escaped, prefix) = escape_filename(OsStr::new("testfile.txt")); + assert_eq!(escaped, "testfile.txt"); + assert_eq!(prefix, ""); + + let (escaped, prefix) = escape_filename(OsStr::new("test\nfile.txt")); + assert_eq!(escaped, "test\\nfile.txt"); + assert_eq!(prefix, "\\"); + + let (escaped, prefix) = escape_filename(OsStr::new("test\rfile.txt")); + assert_eq!(escaped, "test\\rfile.txt"); + assert_eq!(prefix, "\\"); + + let (escaped, prefix) = escape_filename(OsStr::new("test\\file.txt")); + assert_eq!(escaped, "test\\\\file.txt"); + assert_eq!(prefix, "\\"); + } + + #[test] + fn test_calculate_blake2b_length() { + assert_eq!(calculate_blake2b_length(0).unwrap(), None); + assert!(calculate_blake2b_length(10).is_err()); + assert!(calculate_blake2b_length(520).is_err()); + assert_eq!(calculate_blake2b_length(512).unwrap(), None); + assert_eq!(calculate_blake2b_length(256).unwrap(), Some(32)); + } +} diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum/validate.rs similarity index 56% rename from src/uucore/src/lib/features/checksum.rs rename to src/uucore/src/lib/features/checksum/validate.rs index 324dba7b3..ae18e6202 100644 --- a/src/uucore/src/lib/features/checksum.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -2,85 +2,71 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore anotherfile invalidchecksum JWZG FFFD xffname prefixfilename bytelen bitlen hexdigit rsplit -use data_encoding::BASE64; +// spell-checker:ignore rsplit hexdigit bitlen invalidchecksum inva idchecksum xffname + +use std::ffi::OsStr; +use std::fmt::Display; +use std::fs::File; +use std::io::{self, BufReader, Read, Write, stdin}; + use os_display::Quotable; -use std::{ - borrow::Cow, - ffi::OsStr, - fmt::Display, - fs::File, - io::{self, BufReader, Read, Write, stdin}, - num::IntErrorKind, - path::Path, - str, -}; +use crate::checksum::{AlgoKind, ChecksumError, SizedAlgoKind, digest_reader, unescape_filename}; +use crate::error::{FromIo, UError, UResult, USimpleError}; +use crate::quoting_style::{QuotingStyle, locale_aware_escape_name}; +use crate::sum::DigestOutput; use crate::{ - error::{FromIo, UError, UResult, USimpleError}, - os_str_as_bytes, os_str_from_bytes, - quoting_style::{QuotingStyle, locale_aware_escape_name}, - read_os_string_lines, show, show_error, show_warning_caps, - sum::{ - Blake2b, Blake3, Bsd, CRC32B, Crc, Digest, DigestWriter, Md5, Sha1, Sha3_224, Sha3_256, - Sha3_384, Sha3_512, Sha224, Sha256, Sha384, Sha512, Shake128, Shake256, Sm3, SysV, - }, - util_name, + os_str_as_bytes, os_str_from_bytes, read_os_string_lines, show, show_error, show_warning_caps, + translate, }; -use thiserror::Error; -pub const ALGORITHM_OPTIONS_SYSV: &str = "sysv"; -pub const ALGORITHM_OPTIONS_BSD: &str = "bsd"; -pub const ALGORITHM_OPTIONS_CRC: &str = "crc"; -pub const ALGORITHM_OPTIONS_CRC32B: &str = "crc32b"; -pub const ALGORITHM_OPTIONS_MD5: &str = "md5"; -pub const ALGORITHM_OPTIONS_SHA1: &str = "sha1"; -pub const ALGORITHM_OPTIONS_SHA2: &str = "sha2"; -pub const ALGORITHM_OPTIONS_SHA3: &str = "sha3"; +/// To what level should checksum validation print logging info. +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Default)] +pub enum ChecksumVerbose { + Status, + Quiet, + #[default] + Normal, + Warning, +} -pub const ALGORITHM_OPTIONS_SHA224: &str = "sha224"; -pub const ALGORITHM_OPTIONS_SHA256: &str = "sha256"; -pub const ALGORITHM_OPTIONS_SHA384: &str = "sha384"; -pub const ALGORITHM_OPTIONS_SHA512: &str = "sha512"; -pub const ALGORITHM_OPTIONS_BLAKE2B: &str = "blake2b"; -pub const ALGORITHM_OPTIONS_BLAKE3: &str = "blake3"; -pub const ALGORITHM_OPTIONS_SM3: &str = "sm3"; -pub const ALGORITHM_OPTIONS_SHAKE128: &str = "shake128"; -pub const ALGORITHM_OPTIONS_SHAKE256: &str = "shake256"; +impl ChecksumVerbose { + pub fn new(status: bool, quiet: bool, warn: bool) -> Self { + use ChecksumVerbose::*; -pub const SUPPORTED_ALGORITHMS: [&str; 17] = [ - ALGORITHM_OPTIONS_SYSV, - ALGORITHM_OPTIONS_BSD, - ALGORITHM_OPTIONS_CRC, - ALGORITHM_OPTIONS_CRC32B, - ALGORITHM_OPTIONS_MD5, - ALGORITHM_OPTIONS_SHA1, - ALGORITHM_OPTIONS_SHA2, - ALGORITHM_OPTIONS_SHA3, - ALGORITHM_OPTIONS_BLAKE2B, - ALGORITHM_OPTIONS_SM3, - // Extra algorithms that are not valid `cksum --algorithm` - ALGORITHM_OPTIONS_SHA224, - ALGORITHM_OPTIONS_SHA256, - ALGORITHM_OPTIONS_SHA384, - ALGORITHM_OPTIONS_SHA512, - ALGORITHM_OPTIONS_BLAKE3, - ALGORITHM_OPTIONS_SHAKE128, - ALGORITHM_OPTIONS_SHAKE256, -]; + // Assume only one of the three booleans will be enabled at once. + // This is ensured by clap's overriding arguments. + match (status, quiet, warn) { + (true, _, _) => Status, + (_, true, _) => Quiet, + (_, _, true) => Warning, + _ => Normal, + } + } -pub const LEGACY_ALGORITHMS: [&str; 4] = [ - ALGORITHM_OPTIONS_SYSV, - ALGORITHM_OPTIONS_BSD, - ALGORITHM_OPTIONS_CRC, - ALGORITHM_OPTIONS_CRC32B, -]; + #[inline] + pub fn over_status(self) -> bool { + self > Self::Status + } -pub struct HashAlgorithm { - pub name: &'static str, - pub create_fn: Box Box>, - pub bits: usize, + #[inline] + pub fn over_quiet(self) -> bool { + self > Self::Quiet + } + + #[inline] + pub fn at_least_warning(self) -> bool { + self >= Self::Warning + } +} + +/// This struct regroups CLI flags. +#[derive(Debug, Default, Clone, Copy)] +pub struct ChecksumValidateOptions { + pub ignore_missing: bool, + pub strict: bool, + pub verbose: ChecksumVerbose, } /// This structure holds the count of checksum test lines' outcomes. @@ -161,181 +147,45 @@ impl From for FileCheckError { } } -#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Default)] -pub enum ChecksumVerbose { - Status, - Quiet, - #[default] - Normal, - Warning, -} - -impl ChecksumVerbose { - pub fn new(status: bool, quiet: bool, warn: bool) -> Self { - use ChecksumVerbose::*; - - // Assume only one of the three booleans will be enabled at once. - // This is ensured by clap's overriding arguments. - match (status, quiet, warn) { - (true, _, _) => Status, - (_, true, _) => Quiet, - (_, _, true) => Warning, - _ => Normal, - } - } - - #[inline] - pub fn over_status(self) -> bool { - self > Self::Status - } - - #[inline] - pub fn over_quiet(self) -> bool { - self > Self::Quiet - } - - #[inline] - pub fn at_least_warning(self) -> bool { - self >= Self::Warning - } -} - -/// This struct regroups CLI flags. -#[derive(Debug, Default, Clone, Copy)] -pub struct ChecksumOptions { - pub binary: bool, - pub ignore_missing: bool, - pub strict: bool, - pub verbose: ChecksumVerbose, -} - -#[derive(Debug, Error)] -pub enum ChecksumError { - #[error("the --raw option is not supported with multiple files")] - RawMultipleFiles, - #[error("the --ignore-missing option is meaningful only when verifying checksums")] - IgnoreNotCheck, - #[error("the --strict option is meaningful only when verifying checksums")] - StrictNotCheck, - #[error("the --quiet option is meaningful only when verifying checksums")] - QuietNotCheck, - #[error("--length required for {}", .0.quote())] - LengthRequired(String), - #[error("invalid length: {}", .0.quote())] - InvalidLength(String), - #[error("digest length for {} must be 224, 256, 384, or 512", .0.quote())] - InvalidLengthForSha(String), - #[error("--algorithm={0} requires specifying --length 224, 256, 384, or 512")] - LengthRequiredForSha(String), - #[error("--length is only supported with --algorithm blake2b, sha2, or sha3")] - LengthOnlyForBlake2bSha2Sha3, - #[error("the --binary and --text options are meaningless when verifying checksums")] - BinaryTextConflict, - #[error("--text mode is only supported with --untagged")] - TextWithoutUntagged, - #[error("--check is not supported with --algorithm={{bsd,sysv,crc,crc32b}}")] - AlgorithmNotSupportedWithCheck, - #[error("You cannot combine multiple hash algorithms!")] - CombineMultipleAlgorithms, - #[error("Needs an algorithm to hash with.\nUse --help for more information.")] - NeedAlgorithmToHash, - #[error("unknown algorithm: {0}: clap should have prevented this case")] - UnknownAlgorithm(String), - #[error("")] - Io(#[from] io::Error), -} - -impl UError for ChecksumError { - fn code(&self) -> i32 { - 1 - } -} - -/// Creates a SHA3 hasher instance based on the specified bits argument. -/// -/// # Returns -/// -/// Returns a `UResult` with an `HashAlgorithm` or an `Err` if an unsupported -/// output size is provided. -pub fn create_sha3(bits: usize) -> UResult { - match bits { - 224 => Ok(HashAlgorithm { - name: "SHA3-224", - create_fn: Box::new(|| Box::new(Sha3_224::new())), - bits: 224, - }), - 256 => Ok(HashAlgorithm { - name: "SHA3-256", - create_fn: Box::new(|| Box::new(Sha3_256::new())), - bits: 256, - }), - 384 => Ok(HashAlgorithm { - name: "SHA3-384", - create_fn: Box::new(|| Box::new(Sha3_384::new())), - bits: 384, - }), - 512 => Ok(HashAlgorithm { - name: "SHA3-512", - create_fn: Box::new(|| Box::new(Sha3_512::new())), - bits: 512, - }), - - _ => Err(ChecksumError::InvalidLengthForSha("SHA3".into()).into()), - } -} - -pub fn create_sha2(bits: usize) -> UResult { - match bits { - 224 => Ok(HashAlgorithm { - name: "SHA224", - create_fn: Box::new(|| Box::new(Sha224::new())), - bits: 224, - }), - 256 => Ok(HashAlgorithm { - name: "SHA256", - create_fn: Box::new(|| Box::new(Sha256::new())), - bits: 256, - }), - 384 => Ok(HashAlgorithm { - name: "SHA384", - create_fn: Box::new(|| Box::new(Sha384::new())), - bits: 384, - }), - 512 => Ok(HashAlgorithm { - name: "SHA512", - create_fn: Box::new(|| Box::new(Sha512::new())), - bits: 512, - }), - - _ => Err(ChecksumError::InvalidLengthForSha("SHA2".into()).into()), - } -} - -#[allow(clippy::comparison_chain)] fn print_cksum_report(res: &ChecksumResult) { - if res.bad_format == 1 { - show_warning_caps!("{} line is improperly formatted", res.bad_format); - } else if res.bad_format > 1 { - show_warning_caps!("{} lines are improperly formatted", res.bad_format); + if res.bad_format > 0 { + show_warning_caps!( + "{}", + translate!("checksum-bad-format", "count" => res.bad_format) + ); } - if res.failed_cksum == 1 { - show_warning_caps!("{} computed checksum did NOT match", res.failed_cksum); - } else if res.failed_cksum > 1 { - show_warning_caps!("{} computed checksums did NOT match", res.failed_cksum); + if res.failed_cksum > 0 { + show_warning_caps!( + "{}", + translate!("checksum-failed-cksum", "count" => res.failed_cksum) + ); } - if res.failed_open_file == 1 { - show_warning_caps!("{} listed file could not be read", res.failed_open_file); - } else if res.failed_open_file > 1 { - show_warning_caps!("{} listed files could not be read", res.failed_open_file); + if res.failed_open_file > 0 { + show_warning_caps!( + "{}", + translate!("checksum-failed-open-file", "count" => res.failed_open_file) + ); } } /// Print a "no properly formatted lines" message in stderr #[inline] -fn log_no_properly_formatted(filename: String) { - show_error!("{filename}: no properly formatted checksum lines found"); +fn log_no_properly_formatted(filename: impl Display) { + show_error!( + "{}", + translate!("checksum-no-properly-formatted", "checksum_file" => filename) + ); +} + +/// Print a "no file was verified" message in stderr +#[inline] +fn log_no_file_verified(filename: impl Display) { + show_error!( + "{}", + translate!("checksum-no-file-verified", "checksum_file" => filename) + ); } /// Represents the different outcomes that can happen to a file @@ -395,104 +245,6 @@ fn print_file_report( } } -pub fn detect_algo(algo: &str, length: Option) -> UResult { - match algo { - ALGORITHM_OPTIONS_SYSV => Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_SYSV, - create_fn: Box::new(|| Box::new(SysV::new())), - bits: 512, - }), - ALGORITHM_OPTIONS_BSD => Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_BSD, - create_fn: Box::new(|| Box::new(Bsd::new())), - bits: 1024, - }), - ALGORITHM_OPTIONS_CRC => Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_CRC, - create_fn: Box::new(|| Box::new(Crc::new())), - bits: 256, - }), - ALGORITHM_OPTIONS_CRC32B => Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_CRC32B, - create_fn: Box::new(|| Box::new(CRC32B::new())), - bits: 32, - }), - ALGORITHM_OPTIONS_MD5 | "md5sum" => Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_MD5, - create_fn: Box::new(|| Box::new(Md5::new())), - bits: 128, - }), - ALGORITHM_OPTIONS_SHA1 | "sha1sum" => Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_SHA1, - create_fn: Box::new(|| Box::new(Sha1::new())), - bits: 160, - }), - ALGORITHM_OPTIONS_SHA224 | "sha224sum" => Ok(create_sha2(224)?), - ALGORITHM_OPTIONS_SHA256 | "sha256sum" => Ok(create_sha2(256)?), - ALGORITHM_OPTIONS_SHA384 | "sha384sum" => Ok(create_sha2(384)?), - ALGORITHM_OPTIONS_SHA512 | "sha512sum" => Ok(create_sha2(512)?), - ALGORITHM_OPTIONS_BLAKE2B | "b2sum" => { - // Set default length to 512 if None - let bits = length.unwrap_or(512); - if bits == 512 { - Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_BLAKE2B, - create_fn: Box::new(move || Box::new(Blake2b::new())), - bits: 512, - }) - } else { - Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_BLAKE2B, - create_fn: Box::new(move || Box::new(Blake2b::with_output_bytes(bits))), - bits, - }) - } - } - ALGORITHM_OPTIONS_BLAKE3 | "b3sum" => Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_BLAKE3, - create_fn: Box::new(|| Box::new(Blake3::new())), - bits: 256, - }), - ALGORITHM_OPTIONS_SM3 => Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_SM3, - create_fn: Box::new(|| Box::new(Sm3::new())), - bits: 512, - }), - algo @ (ALGORITHM_OPTIONS_SHAKE128 | "shake128sum") => { - let bits = length.ok_or(ChecksumError::LengthRequired(algo.to_ascii_uppercase()))?; - Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_SHAKE128, - create_fn: Box::new(|| Box::new(Shake128::new())), - bits, - }) - } - algo @ (ALGORITHM_OPTIONS_SHAKE256 | "shake256sum") => { - let bits = length.ok_or(ChecksumError::LengthRequired(algo.to_ascii_uppercase()))?; - Ok(HashAlgorithm { - name: ALGORITHM_OPTIONS_SHAKE256, - create_fn: Box::new(|| Box::new(Shake256::new())), - bits, - }) - } - algo @ ALGORITHM_OPTIONS_SHA2 => { - let bits = validate_sha2_sha3_length(algo, length)?; - create_sha2(bits) - } - algo @ ALGORITHM_OPTIONS_SHA3 => { - let bits = validate_sha2_sha3_length(algo, length)?; - create_sha3(bits) - } - - // TODO: `hashsum` specific, to remove once hashsum is removed. - algo @ ("sha3-224" | "sha3-256" | "sha3-384" | "sha3-512") => { - let bits: usize = algo.strip_prefix("sha3-").unwrap().parse().unwrap(); - create_sha3(bits) - } - - algo => Err(ChecksumError::UnknownAlgorithm(algo.into()).into()), - } -} - #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum LineFormat { AlgoBased, @@ -557,27 +309,7 @@ impl LineFormat { SubCase::OpenSSL => ByteSliceExt::rsplit_once(after_paren, b")= ")?, }; - fn is_valid_checksum(checksum: &[u8]) -> bool { - if checksum.is_empty() { - return false; - } - - let mut parts = checksum.splitn(2, |&b| b == b'='); - let main = parts.next().unwrap(); // Always exists since checksum isn't empty - let padding = parts.next().unwrap_or_default(); // Empty if no '=' - - main.iter() - .all(|&b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/') - && !main.is_empty() - && padding.len() <= 2 - && padding.iter().all(|&b| b == b'=') - } - if !is_valid_checksum(checksum) { - return None; - } - // SAFETY: we just validated the contents of checksum, we can unsafely make a - // String from it - let checksum_utf8 = unsafe { String::from_utf8_unchecked(checksum.to_vec()) }; + let checksum_utf8 = Self::validate_checksum_format(checksum)?; Some(LineInfo { algo_name: Some(algo_utf8), @@ -597,12 +329,8 @@ impl LineFormat { fn parse_untagged(line: &[u8]) -> Option { let space_idx = line.iter().position(|&b| b == b' ')?; let checksum = &line[..space_idx]; - if !checksum.iter().all(|&b| b.is_ascii_hexdigit()) || checksum.is_empty() { - return None; - } - // SAFETY: we just validated the contents of checksum, we can unsafely make a - // String from it - let checksum_utf8 = unsafe { String::from_utf8_unchecked(checksum.to_vec()) }; + + let checksum_utf8 = Self::validate_checksum_format(checksum)?; let rest = &line[space_idx..]; let filename = rest @@ -649,6 +377,47 @@ impl LineFormat { format: Self::SingleSpace, }) } + + /// Ensure that the given checksum is syntactically valid (that it is either + /// hexadecimal or base64 encoded). + fn validate_checksum_format(checksum: &[u8]) -> Option { + if checksum.is_empty() { + return None; + } + + let mut is_base64 = false; + + for index in 0..checksum.len() { + match checksum[index..] { + // ASCII alphanumeric + [b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9', ..] => (), + // Base64 special character + [b'+' | b'/', ..] => is_base64 = true, + // Base64 end of string padding + [b'='] | [b'=', b'='] | [b'=', b'=', b'='] => { + is_base64 = true; + break; + } + // Any other character means the checksum is wrong + _ => return None, + } + } + + // If base64 characters were encountered, make sure the checksum has a + // length multiple of 4. + // + // This check is not enough because it may allow base64-encoded + // checksums that are fully alphanumeric. Another check happens later + // when we are provided with a length hint to detect ambiguous + // base64-encoded checksums. + if is_base64 && checksum.len() % 4 != 0 { + return None; + } + + // SAFETY: we just validated the contents of checksum, we can unsafely make a + // String from it + Some(unsafe { String::from_utf8_unchecked(checksum.to_vec()) }) + } } // Helper trait for byte slice operations @@ -712,62 +481,60 @@ impl LineInfo { } } -fn get_filename_for_output(filename: &OsStr, input_is_stdin: bool) -> String { - if input_is_stdin { - "standard input" - } else { - filename.to_str().unwrap() - } - .maybe_quote() - .to_string() -} - -/// Extract the expected digest from the checksum string -fn get_expected_digest_as_hex_string( - line_info: &LineInfo, - len_hint: Option, -) -> Option> { - let ck = &line_info.checksum; - - let against_hint = |len| len_hint.is_none_or(|l| l == len); - - if ck.len() % 2 != 0 { - // If the length of the digest is not a multiple of 2, then it - // must be improperly formatted (1 hex digit is 2 characters) +/// Extract the expected digest from the checksum string and decode it +fn get_raw_expected_digest(checksum: &str, byte_len_hint: Option) -> Option> { + // If the length of the digest is not a multiple of 2, then it must be + // improperly formatted (1 byte is 2 hex digits, and base64 strings should + // always be a multiple of 4). + if checksum.len() % 2 != 0 { return None; } - // If the digest can be decoded as hexadecimal AND its length matches the - // one expected (in case it's given), just go with it. - if ck.as_bytes().iter().all(u8::is_ascii_hexdigit) && against_hint(ck.len()) { - return Some(Cow::Borrowed(ck)); + let checks_hint = |len| byte_len_hint.is_none_or(|hint| hint == len); + + // If the length of the string matches the one to be expected (in case it's + // given) AND the digest can be decoded as hexadecimal, just go with it. + if checks_hint(checksum.len() / 2) { + if let Ok(raw_ck) = hex::decode(checksum) { + return Some(raw_ck); + } } - // If hexadecimal digest fails for any reason, interpret the digest as base 64. - BASE64 - .decode(ck.as_bytes()) // Decode the string as encoded base64 - .map(hex::encode) // Encode it back as hexadecimal - .map(Cow::::Owned) + // If the checksum cannot be decoded as hexadecimal, interpret it as Base64 + // instead. + + // But first, verify the encoded checksum length, which should be a + // multiple of 4. + // + // It is important to check it before trying to decode, because the + // forgiving mode of decoding will ignore if padding characters '=' are + // MISSING, but to match GNU's behavior, we must reject it. + if checksum.len() % 4 != 0 { + return None; + } + + // Perform the decoding and be FORGIVING about it, to allow for checksums + // with INVALID padding to still be decoded. This is enforced by + // `test_untagged_base64_matching_tag` in `test_cksum.rs` + + base64_simd::forgiving_decode_to_vec(checksum.as_bytes()) .ok() - .and_then(|s| { - // Check the digest length - if against_hint(s.len()) { Some(s) } else { None } - }) + .filter(|raw| checks_hint(raw.len())) } /// Returns a reader that reads from the specified file, or from stdin if `filename_to_check` is "-". fn get_file_to_check( filename: &OsStr, - opts: ChecksumOptions, + opts: ChecksumValidateOptions, ) -> Result, LineCheckError> { - let filename_bytes = os_str_as_bytes(filename).expect("UTF-8 error"); + let filename_bytes = os_str_as_bytes(filename).map_err(|e| LineCheckError::UError(e.into()))?; if filename == "-" { - Ok(Box::new(stdin())) // Use stdin if "-" is specified in the checksum file + Ok(Box::new(io::stdin())) // Use stdin if "-" is specified in the checksum file } else { let failed_open = || { print_file_report( - std::io::stdout(), + io::stdout(), filename_bytes, FileChecksumResult::CantOpen, "", @@ -817,17 +584,18 @@ fn get_input_file(filename: &OsStr) -> UResult> { match File::open(filename) { Ok(f) => { if f.metadata()?.is_dir() { - Err( - io::Error::other(format!("{}: Is a directory", filename.to_string_lossy())) - .into(), + Err(io::Error::other( + translate!("error-is-a-directory", "file" => filename.to_string_lossy()), ) + .into()) } else { Ok(Box::new(f)) } } Err(_) => Err(io::Error::other(format!( - "{}: No such file or directory", - filename.to_string_lossy() + "{}: {}", + filename.to_string_lossy(), + translate!("error-file-not-found") )) .into()), } @@ -836,11 +604,14 @@ fn get_input_file(filename: &OsStr) -> UResult> { /// Gets the algorithm name and length from the `LineInfo` if the algo-based format is matched. fn identify_algo_name_and_length( line_info: &LineInfo, - algo_name_input: Option<&str>, + algo_name_input: Option, last_algo: &mut Option, -) -> Result<(String, Option), LineCheckError> { +) -> Result<(AlgoKind, Option), LineCheckError> { let algo_from_line = line_info.algo_name.clone().unwrap_or_default(); - let line_algo = algo_from_line.to_lowercase(); + let Ok(line_algo) = AlgoKind::from_cksum(algo_from_line.to_lowercase()) else { + // Unknown algorithm + return Err(LineCheckError::ImproperlyFormatted); + }; *last_algo = Some(algo_from_line); // check if we are called with XXXsum (example: md5sum) but we detected a @@ -848,31 +619,21 @@ fn identify_algo_name_and_length( // // Also handle the case cksum -s sm3 but the file contains other formats if let Some(algo_name_input) = algo_name_input { - match (algo_name_input, line_algo.as_str()) { + match (algo_name_input, line_algo) { (l, r) if l == r => (), // Edge case for SHA2, which matches SHA(224|256|384|512) ( - ALGORITHM_OPTIONS_SHA2, - ALGORITHM_OPTIONS_SHA224 - | ALGORITHM_OPTIONS_SHA256 - | ALGORITHM_OPTIONS_SHA384 - | ALGORITHM_OPTIONS_SHA512, + AlgoKind::Sha2, + AlgoKind::Sha224 | AlgoKind::Sha256 | AlgoKind::Sha384 | AlgoKind::Sha512, ) => (), _ => return Err(LineCheckError::ImproperlyFormatted), } } - if !SUPPORTED_ALGORITHMS.contains(&line_algo.as_str()) { - // Not supported algo, leave early - return Err(LineCheckError::ImproperlyFormatted); - } - let bytes = if let Some(bitlen) = line_info.algo_bit_len { - match line_algo.as_str() { - ALGORITHM_OPTIONS_BLAKE2B if bitlen % 8 == 0 => Some(bitlen / 8), - ALGORITHM_OPTIONS_SHA2 | ALGORITHM_OPTIONS_SHA3 - if [224, 256, 384, 512].contains(&bitlen) => - { + match line_algo { + AlgoKind::Blake2b if bitlen % 8 == 0 => Some(bitlen / 8), + AlgoKind::Sha2 | AlgoKind::Sha3 if [224, 256, 384, 512].contains(&bitlen) => { Some(bitlen) } // Either @@ -885,7 +646,7 @@ fn identify_algo_name_and_length( // the given length is wrong because it's not a multiple of 8. _ => return Err(LineCheckError::ImproperlyFormatted), } - } else if line_algo == ALGORITHM_OPTIONS_BLAKE2B { + } else if line_algo == AlgoKind::Blake2b { // Default length with BLAKE2b, Some(64) } else { @@ -899,9 +660,9 @@ fn identify_algo_name_and_length( /// the expected one. fn compute_and_check_digest_from_file( filename: &[u8], - expected_checksum: &str, - mut algo: HashAlgorithm, - opts: ChecksumOptions, + expected_checksum: &[u8], + algo: SizedAlgoKind, + opts: ChecksumValidateOptions, ) -> Result<(), LineCheckError> { let (filename_to_check_unescaped, prefix) = unescape_filename(filename); let real_filename_to_check = os_str_from_bytes(&filename_to_check_unescaped)?; @@ -911,13 +672,19 @@ fn compute_and_check_digest_from_file( let mut file_reader = BufReader::new(file_to_check); // Read the file and calculate the checksum - let create_fn = &mut algo.create_fn; - let mut digest = create_fn(); + let mut digest = algo.create_digest(); + + // TODO: improve function signature to use ReadingMode instead of binary bool + // Set binary to false because --binary is not supported with --check let (calculated_checksum, _) = - digest_reader(&mut digest, &mut file_reader, opts.binary, algo.bits).unwrap(); + digest_reader(&mut digest, &mut file_reader, /* binary */ false).unwrap(); // Do the checksum validation - let checksum_correct = expected_checksum == calculated_checksum; + let checksum_correct = match calculated_checksum { + DigestOutput::Vec(data) => data == expected_checksum, + DigestOutput::Crc(n) => n.to_be_bytes() == expected_checksum, + DigestOutput::U16(n) => n.to_be_bytes() == expected_checksum, + }; print_file_report( std::io::stdout(), filename, @@ -936,26 +703,26 @@ fn compute_and_check_digest_from_file( /// Check a digest checksum with non-algo based pre-treatment. fn process_algo_based_line( line_info: &LineInfo, - cli_algo_name: Option<&str>, - opts: ChecksumOptions, + cli_algo_kind: Option, + opts: ChecksumValidateOptions, last_algo: &mut Option, ) -> Result<(), LineCheckError> { let filename_to_check = line_info.filename.as_slice(); - let (algo_name, algo_byte_len) = - identify_algo_name_and_length(line_info, cli_algo_name, last_algo)?; + let (algo_kind, algo_byte_len) = + identify_algo_name_and_length(line_info, cli_algo_kind, last_algo)?; // If the digest bitlen is known, we can check the format of the expected // checksum with it. - let digest_char_length_hint = match (algo_name.as_str(), algo_byte_len) { - (ALGORITHM_OPTIONS_BLAKE2B, Some(bytelen)) => Some(bytelen * 2), + let digest_char_length_hint = match (algo_kind, algo_byte_len) { + (AlgoKind::Blake2b, Some(byte_len)) => Some(byte_len), _ => None, }; - let expected_checksum = get_expected_digest_as_hex_string(line_info, digest_char_length_hint) + let expected_checksum = get_raw_expected_digest(&line_info.checksum, digest_char_length_hint) .ok_or(LineCheckError::ImproperlyFormatted)?; - let algo = detect_algo(&algo_name, algo_byte_len)?; + let algo = SizedAlgoKind::from_unsized(algo_kind, algo_byte_len)?; compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts) } @@ -964,9 +731,9 @@ fn process_algo_based_line( fn process_non_algo_based_line( line_number: usize, line_info: &LineInfo, - cli_algo_name: &str, + cli_algo_kind: AlgoKind, cli_algo_length: Option, - opts: ChecksumOptions, + opts: ChecksumValidateOptions, ) -> Result<(), LineCheckError> { let mut filename_to_check = line_info.filename.as_slice(); if filename_to_check.starts_with(b"*") @@ -976,30 +743,22 @@ fn process_non_algo_based_line( // Remove the leading asterisk if present - only for the first line filename_to_check = &filename_to_check[1..]; } - let expected_checksum = get_expected_digest_as_hex_string(line_info, None) + let expected_checksum = get_raw_expected_digest(&line_info.checksum, None) .ok_or(LineCheckError::ImproperlyFormatted)?; // When a specific algorithm name is input, use it and use the provided // bits except when dealing with blake2b, sha2 and sha3, where we will // detect the length. - let (algo_name, algo_byte_len) = match cli_algo_name { - ALGORITHM_OPTIONS_BLAKE2B => { - // division by 2 converts the length of the Blake2b checksum from - // hexadecimal characters to bytes, as each byte is represented by - // two hexadecimal characters. - ( - ALGORITHM_OPTIONS_BLAKE2B.to_string(), - Some(expected_checksum.len() / 2), - ) + let (algo_kind, algo_byte_len) = match cli_algo_kind { + AlgoKind::Blake2b => (AlgoKind::Blake2b, Some(expected_checksum.len())), + algo @ (AlgoKind::Sha2 | AlgoKind::Sha3) => { + // multiplication by 8 to get the number of bits + (algo, Some(expected_checksum.len() * 8)) } - algo @ (ALGORITHM_OPTIONS_SHA2 | ALGORITHM_OPTIONS_SHA3) => { - // multiplication by 4 to get the number of bits - (algo.to_string(), Some(expected_checksum.len() * 4)) - } - _ => (cli_algo_name.to_lowercase(), cli_algo_length), + _ => (cli_algo_kind, cli_algo_length), }; - let algo = detect_algo(&algo_name, algo_byte_len)?; + let algo = SizedAlgoKind::from_unsized(algo_kind, algo_byte_len)?; compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts) } @@ -1013,9 +772,9 @@ fn process_non_algo_based_line( fn process_checksum_line( line: &OsStr, i: usize, - cli_algo_name: Option<&str>, + cli_algo_name: Option, cli_algo_length: Option, - opts: ChecksumOptions, + opts: ChecksumValidateOptions, cached_line_format: &mut Option, last_algo: &mut Option, ) -> Result<(), LineCheckError> { @@ -1046,9 +805,9 @@ fn process_checksum_line( fn process_checksum_file( filename_input: &OsStr, - cli_algo_name: Option<&str>, + cli_algo_kind: Option, cli_algo_length: Option, - opts: ChecksumOptions, + opts: ChecksumValidateOptions, ) -> Result<(), FileCheckError> { let mut res = ChecksumResult::default(); @@ -1083,7 +842,7 @@ fn process_checksum_file( let line_result = process_checksum_line( line, i, - cli_algo_name, + cli_algo_kind, cli_algo_length, opts, &mut cached_line_format, @@ -1107,18 +866,16 @@ fn process_checksum_file( res.bad_format += 1; if opts.verbose.at_least_warning() { - let algo = if let Some(algo_name_input) = cli_algo_name { - Cow::Owned(algo_name_input.to_uppercase()) + let algo = if let Some(algo_name_input) = cli_algo_kind { + algo_name_input.to_uppercase() } else if let Some(algo) = &last_algo { - Cow::Borrowed(algo.as_str()) + algo.as_str() } else { - Cow::Borrowed("Unknown algorithm") + "Unknown algorithm" }; - eprintln!( - "{}: {}: {}: improperly formatted {algo} checksum line", - util_name(), - filename_input.maybe_quote(), - i + 1, + show_error!( + "{}", + translate!("checksum-error-algo-bad-format", "file" => filename_input.maybe_quote(), "line" => i + 1, "algo" => algo) ); } } @@ -1128,11 +885,19 @@ fn process_checksum_file( } } + let filename_display = || { + if input_is_stdin { + "standard input".maybe_quote() + } else { + filename_input.maybe_quote() + } + }; + // not a single line correctly formatted found // return an error if res.total_properly_formatted() == 0 { if opts.verbose.over_status() { - log_no_properly_formatted(get_filename_for_output(filename_input, input_is_stdin)); + log_no_properly_formatted(filename_display()); } return Err(FileCheckError::Failed); } @@ -1146,11 +911,7 @@ fn process_checksum_file( // we have only bad format // and we had ignore-missing if opts.verbose.over_status() { - eprintln!( - "{}: {}: no file was verified", - util_name(), - filename_input.maybe_quote(), - ); + log_no_file_verified(filename_display()); } return Err(FileCheckError::Failed); } @@ -1176,9 +937,9 @@ fn process_checksum_file( /// Do the checksum validation (can be strict or not) pub fn perform_checksum_validation<'a, I>( files: I, - algo_name_input: Option<&str>, + algo_kind: Option, length_input: Option, - opts: ChecksumOptions, + opts: ChecksumValidateOptions, ) -> UResult<()> where I: Iterator, @@ -1188,7 +949,7 @@ where // if cksum has several input files, it will print the result for each file for filename_input in files { use FileCheckError::*; - match process_checksum_file(filename_input, algo_name_input, length_input, opts) { + match process_checksum_file(filename_input, algo_kind, length_input, opts) { Err(UError(e)) => return Err(e), Err(Failed | CantOpenChecksumFile) => failed = true, Ok(_) => (), @@ -1202,293 +963,11 @@ where } } -pub fn digest_reader( - digest: &mut Box, - reader: &mut T, - binary: bool, - 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, binary); - 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.div_ceil(8)]; - digest.hash_finalize(&mut bytes); - Ok((hex::encode(bytes), output_size)) - } -} - -/// Calculates the length of the digest. -pub fn calculate_blake2b_length(length: usize) -> UResult> { - calculate_blake2b_length_str(length.to_string().as_str()) -} - -/// Calculates the length of the digest. -pub fn calculate_blake2b_length_str(length: &str) -> UResult> { - match length.parse() { - Ok(0) => Ok(None), - Ok(n) if n % 8 != 0 => { - show_error!("{}", ChecksumError::InvalidLength(length.into())); - Err(io::Error::new(io::ErrorKind::InvalidInput, "length is not a multiple of 8").into()) - } - Ok(n) if n > 512 => { - show_error!("{}", ChecksumError::InvalidLength(length.into())); - Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!( - "maximum digest length for {} is 512 bits", - "BLAKE2b".quote() - ), - ) - .into()) - } - Ok(n) => { - // Divide by 8, as our blake2b implementation expects bytes instead of bits. - if n == 512 { - // When length is 512, it is blake2b's default. - // So, don't show it - Ok(None) - } else { - Ok(Some(n / 8)) - } - } - Err(_) => Err(ChecksumError::InvalidLength(length.into()).into()), - } -} - -pub fn validate_sha2_sha3_length(algo_name: &str, length: Option) -> UResult { - match length { - Some(len @ (224 | 256 | 384 | 512)) => Ok(len), - Some(len) => { - show_error!("{}", ChecksumError::InvalidLength(len.to_string())); - Err(ChecksumError::InvalidLengthForSha(algo_name.to_ascii_uppercase()).into()) - } - None => Err(ChecksumError::LengthRequiredForSha(algo_name.into()).into()), - } -} - -pub fn sanitize_sha2_sha3_length_str(algo_name: &str, length: &str) -> UResult { - // There is a difference in the errors sent when the length is not a number - // vs. its an invalid number. - // - // When inputting an invalid number, an extra error message it printed to - // remind of the accepted inputs. - let len = match length.parse::() { - Ok(l) => l, - // Note: Positive overflow while parsing counts as an invalid number, - // but a number still. - Err(e) if *e.kind() == IntErrorKind::PosOverflow => { - show_error!("{}", ChecksumError::InvalidLength(length.into())); - return Err(ChecksumError::InvalidLengthForSha(algo_name.to_ascii_uppercase()).into()); - } - Err(_) => return Err(ChecksumError::InvalidLength(length.into()).into()), - }; - - if [224, 256, 384, 512].contains(&len) { - Ok(len) - } else { - show_error!("{}", ChecksumError::InvalidLength(length.into())); - Err(ChecksumError::InvalidLengthForSha(algo_name.to_ascii_uppercase()).into()) - } -} - -pub fn unescape_filename(filename: &[u8]) -> (Vec, &'static str) { - let mut unescaped = Vec::with_capacity(filename.len()); - let mut byte_iter = filename.iter().peekable(); - loop { - let Some(byte) = byte_iter.next() else { - break; - }; - if *byte == b'\\' { - match byte_iter.next() { - Some(b'\\') => unescaped.push(b'\\'), - Some(b'n') => unescaped.push(b'\n'), - Some(b'r') => unescaped.push(b'\r'), - Some(x) => { - unescaped.push(b'\\'); - unescaped.push(*x); - } - _ => {} - } - } else { - unescaped.push(*byte); - } - } - let prefix = if unescaped == filename { "" } else { "\\" }; - (unescaped, prefix) -} - -pub fn escape_filename(filename: &Path) -> (String, &'static str) { - let original = filename.as_os_str().to_string_lossy(); - let escaped = original - .replace('\\', "\\\\") - .replace('\n', "\\n") - .replace('\r', "\\r"); - let prefix = if escaped == original { "" } else { "\\" }; - (escaped, prefix) -} - #[cfg(test)] mod tests { - use super::*; use std::ffi::OsString; - #[test] - fn test_unescape_filename() { - let (unescaped, prefix) = unescape_filename(b"test\\nfile.txt"); - assert_eq!(unescaped, b"test\nfile.txt"); - assert_eq!(prefix, "\\"); - let (unescaped, prefix) = unescape_filename(b"test\\nfile.txt"); - assert_eq!(unescaped, b"test\nfile.txt"); - assert_eq!(prefix, "\\"); - - let (unescaped, prefix) = unescape_filename(b"test\\rfile.txt"); - assert_eq!(unescaped, b"test\rfile.txt"); - assert_eq!(prefix, "\\"); - - let (unescaped, prefix) = unescape_filename(b"test\\\\file.txt"); - assert_eq!(unescaped, b"test\\file.txt"); - assert_eq!(prefix, "\\"); - } - - #[test] - fn test_escape_filename() { - let (escaped, prefix) = escape_filename(Path::new("testfile.txt")); - assert_eq!(escaped, "testfile.txt"); - assert_eq!(prefix, ""); - - let (escaped, prefix) = escape_filename(Path::new("test\nfile.txt")); - assert_eq!(escaped, "test\\nfile.txt"); - assert_eq!(prefix, "\\"); - - let (escaped, prefix) = escape_filename(Path::new("test\rfile.txt")); - assert_eq!(escaped, "test\\rfile.txt"); - assert_eq!(prefix, "\\"); - - let (escaped, prefix) = escape_filename(Path::new("test\\file.txt")); - assert_eq!(escaped, "test\\\\file.txt"); - assert_eq!(prefix, "\\"); - } - - #[test] - fn test_calculate_blake2b_length() { - assert_eq!(calculate_blake2b_length(0).unwrap(), None); - assert!(calculate_blake2b_length(10).is_err()); - assert!(calculate_blake2b_length(520).is_err()); - assert_eq!(calculate_blake2b_length(512).unwrap(), None); - assert_eq!(calculate_blake2b_length(256).unwrap(), Some(32)); - } - - #[test] - fn test_detect_algo() { - assert_eq!( - detect_algo(ALGORITHM_OPTIONS_SYSV, None).unwrap().name, - ALGORITHM_OPTIONS_SYSV - ); - assert_eq!( - detect_algo(ALGORITHM_OPTIONS_BSD, None).unwrap().name, - ALGORITHM_OPTIONS_BSD - ); - assert_eq!( - detect_algo(ALGORITHM_OPTIONS_CRC, None).unwrap().name, - ALGORITHM_OPTIONS_CRC - ); - assert_eq!( - detect_algo(ALGORITHM_OPTIONS_MD5, None).unwrap().name, - ALGORITHM_OPTIONS_MD5 - ); - assert_eq!( - detect_algo(ALGORITHM_OPTIONS_SHA1, None).unwrap().name, - ALGORITHM_OPTIONS_SHA1 - ); - assert_eq!( - detect_algo(ALGORITHM_OPTIONS_SHA224, None).unwrap().name, - ALGORITHM_OPTIONS_SHA224.to_ascii_uppercase() - ); - assert_eq!( - detect_algo(ALGORITHM_OPTIONS_SHA256, None).unwrap().name, - ALGORITHM_OPTIONS_SHA256.to_ascii_uppercase() - ); - assert_eq!( - detect_algo(ALGORITHM_OPTIONS_SHA384, None).unwrap().name, - ALGORITHM_OPTIONS_SHA384.to_ascii_uppercase() - ); - assert_eq!( - detect_algo(ALGORITHM_OPTIONS_SHA512, None).unwrap().name, - ALGORITHM_OPTIONS_SHA512.to_ascii_uppercase() - ); - assert_eq!( - detect_algo(ALGORITHM_OPTIONS_BLAKE2B, None).unwrap().name, - ALGORITHM_OPTIONS_BLAKE2B - ); - assert_eq!( - detect_algo(ALGORITHM_OPTIONS_BLAKE3, None).unwrap().name, - ALGORITHM_OPTIONS_BLAKE3 - ); - assert_eq!( - detect_algo(ALGORITHM_OPTIONS_SM3, None).unwrap().name, - ALGORITHM_OPTIONS_SM3 - ); - assert_eq!( - detect_algo(ALGORITHM_OPTIONS_SHAKE128, Some(128)) - .unwrap() - .name, - ALGORITHM_OPTIONS_SHAKE128 - ); - assert_eq!( - detect_algo(ALGORITHM_OPTIONS_SHAKE256, Some(256)) - .unwrap() - .name, - ALGORITHM_OPTIONS_SHAKE256 - ); - - // Older versions of checksum used to detect the "sha3" prefix, but not - // anymore. - assert!(detect_algo("sha3_224", Some(224)).is_err()); - assert!(detect_algo("sha3_256", Some(256)).is_err()); - assert!(detect_algo("sha3_384", Some(384)).is_err()); - assert!(detect_algo("sha3_512", Some(512)).is_err()); - - let sha3_224 = detect_algo("sha3", Some(224)).unwrap(); - assert_eq!(sha3_224.name, "SHA3-224"); - assert_eq!(sha3_224.bits, 224); - let sha3_256 = detect_algo("sha3", Some(256)).unwrap(); - assert_eq!(sha3_256.name, "SHA3-256"); - assert_eq!(sha3_256.bits, 256); - let sha3_384 = detect_algo("sha3", Some(384)).unwrap(); - assert_eq!(sha3_384.name, "SHA3-384"); - assert_eq!(sha3_384.bits, 384); - let sha3_512 = detect_algo("sha3", Some(512)).unwrap(); - assert_eq!(sha3_512.name, "SHA3-512"); - assert_eq!(sha3_512.bits, 512); - - assert!(detect_algo("sha3", None).is_err()); - - assert_eq!(detect_algo("sha2", Some(224)).unwrap().name, "SHA224"); - assert_eq!(detect_algo("sha2", Some(256)).unwrap().name, "SHA256"); - assert_eq!(detect_algo("sha2", Some(384)).unwrap().name, "SHA384"); - assert_eq!(detect_algo("sha2", Some(512)).unwrap().name, "SHA512"); - - assert!(detect_algo("sha2", None).is_err()); - } + use super::*; #[test] fn test_algo_based_parser() { @@ -1585,7 +1064,13 @@ mod tests { b"b064a020db8018f18ff5ae367d01b212 ", Some((b"b064a020db8018f18ff5ae367d01b212", b" ")), ), - (b"invalidchecksum test", None), + // base64 checksums are accepted + ( + b"b21lbGV0dGUgZHUgZnJvbWFnZQ== ", + Some((b"b21lbGV0dGUgZHUgZnJvbWFnZQ==", b" ")), + ), + // Invalid checksums fail + (b"inva|idchecksum test", None), ]; for (input, expected) in test_cases { @@ -1705,33 +1190,30 @@ mod tests { #[test] fn test_get_expected_digest() { - let line = OsString::from("SHA256 (empty) = 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="); - let mut cached_line_format = None; - let line_info = LineInfo::parse(&line, &mut cached_line_format).unwrap(); + let ck = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=".to_owned(); - let result = get_expected_digest_as_hex_string(&line_info, None); + let result = get_raw_expected_digest(&ck, None); assert_eq!( result.unwrap(), - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + hex::decode(b"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + .unwrap() ); } #[test] fn test_get_expected_checksum_invalid() { // The line misses a '=' at the end to be valid base64 - let line = OsString::from("SHA256 (empty) = 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU"); - let mut cached_line_format = None; - let line_info = LineInfo::parse(&line, &mut cached_line_format).unwrap(); + let ck = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU".to_owned(); - let result = get_expected_digest_as_hex_string(&line_info, None); + let result = get_raw_expected_digest(&ck, None); assert!(result.is_none()); } #[test] fn test_print_file_report() { - let opts = ChecksumOptions::default(); + let opts = ChecksumValidateOptions::default(); let cases: &[(&[u8], FileChecksumResult, &str, &[u8])] = &[ (b"filename", FileChecksumResult::Ok, "", b"filename: OK\n"), diff --git a/src/uucore/src/lib/features/encoding.rs b/src/uucore/src/lib/features/encoding.rs index 6c6261c2c..2f7caae2b 100644 --- a/src/uucore/src/lib/features/encoding.rs +++ b/src/uucore/src/lib/features/encoding.rs @@ -214,6 +214,11 @@ impl EncodingWrapper { } } +pub struct PadResult { + pub chunk: Vec, + pub had_invalid_tail: bool, +} + pub trait SupportsFastDecodeAndEncode { /// Returns the list of characters used by this encoding fn alphabet(&self) -> &'static [u8]; @@ -245,6 +250,19 @@ pub trait SupportsFastDecodeAndEncode { /// /// The decoding performed by `fast_decode` depends on this number being correct. fn valid_decoding_multiple(&self) -> usize; + + /// Whether the decoder can flush partial chunks (multiples of `valid_decoding_multiple`) + /// before seeing the full input. Defaults to `false` for encodings that must consume the + /// entire input (e.g. base58). + fn supports_partial_decode(&self) -> bool { + false + } + + /// Gives encoding-specific logic a chance to pad a trailing, non-empty remainder + /// before the final decode attempt. The default implementation opts out. + fn pad_remainder(&self, _remainder: &[u8]) -> Option { + None + } } impl SupportsFastDecodeAndEncode for Base58Wrapper { @@ -504,3 +522,80 @@ impl SupportsFastDecodeAndEncode for EncodingWrapper { self.unpadded_multiple } } + +pub struct Base32Wrapper { + inner: EncodingWrapper, +} + +impl Base32Wrapper { + pub fn new( + encoding: Encoding, + valid_decoding_multiple: usize, + unpadded_multiple: usize, + alphabet: &'static [u8], + ) -> Self { + Self { + inner: EncodingWrapper::new( + encoding, + valid_decoding_multiple, + unpadded_multiple, + alphabet, + ), + } + } +} + +impl SupportsFastDecodeAndEncode for Base32Wrapper { + fn alphabet(&self) -> &'static [u8] { + self.inner.alphabet() + } + + fn decode_into_vec(&self, input: &[u8], output: &mut Vec) -> UResult<()> { + self.inner.decode_into_vec(input, output) + } + + fn encode_to_vec_deque(&self, input: &[u8], output: &mut VecDeque) -> UResult<()> { + self.inner.encode_to_vec_deque(input, output) + } + + fn unpadded_multiple(&self) -> usize { + self.inner.unpadded_multiple() + } + + fn valid_decoding_multiple(&self) -> usize { + self.inner.valid_decoding_multiple() + } + + fn pad_remainder(&self, remainder: &[u8]) -> Option { + if remainder.is_empty() || remainder.contains(&b'=') { + return None; + } + + const VALID_REMAINDERS: [usize; 4] = [2, 4, 5, 7]; + + let mut len = remainder.len(); + let mut trimmed = false; + + while len > 0 && !VALID_REMAINDERS.contains(&len) { + len -= 1; + trimmed = true; + } + + if len == 0 { + return None; + } + + let mut padded = remainder[..len].to_vec(); + let missing = self.valid_decoding_multiple() - padded.len(); + padded.extend(std::iter::repeat_n(b'=', missing)); + + Some(PadResult { + chunk: padded, + had_invalid_tail: trimmed, + }) + } + + fn supports_partial_decode(&self) -> bool { + true + } +} diff --git a/src/uucore/src/lib/features/format/human.rs b/src/uucore/src/lib/features/format/human.rs index 3c80e0b19..7777103b9 100644 --- a/src/uucore/src/lib/features/format/human.rs +++ b/src/uucore/src/lib/features/format/human.rs @@ -9,7 +9,7 @@ //! //! Format sizes like gnulibs human_readable() would -use number_prefix::NumberPrefix; +use unit_prefix::NumberPrefix; #[derive(Copy, Clone, PartialEq)] pub enum SizeFormat { diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index f8d3c0f96..16de054a3 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -123,6 +123,7 @@ impl FileInformation { not(target_os = "openbsd"), not(target_os = "illumos"), not(target_os = "solaris"), + not(target_os = "cygwin"), not(target_arch = "aarch64"), not(target_arch = "riscv64"), not(target_arch = "loongarch64"), @@ -140,6 +141,7 @@ impl FileInformation { target_os = "openbsd", target_os = "illumos", target_os = "solaris", + target_os = "cygwin", target_arch = "aarch64", target_arch = "riscv64", target_arch = "loongarch64", diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index 4be4d66cf..78dfcceb2 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -7,9 +7,9 @@ // spell-checker:ignore DATETIME getmntinfo subsecond (fs) cifs smbfs -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] const LINUX_MTAB: &str = "/etc/mtab"; -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] const LINUX_MOUNTINFO: &str = "/proc/self/mountinfo"; #[cfg(all(unix, not(any(target_os = "aix", target_os = "redox"))))] static MOUNT_OPT_BIND: &str = "bind"; @@ -94,7 +94,8 @@ pub use libc::statfs as StatFs; target_os = "dragonfly", target_os = "illumos", target_os = "solaris", - target_os = "redox" + target_os = "redox", + target_os = "cygwin", ))] pub use libc::statvfs as StatFs; @@ -112,7 +113,8 @@ pub use libc::statfs as statfs_fn; target_os = "illumos", target_os = "solaris", target_os = "dragonfly", - target_os = "redox" + target_os = "redox", + target_os = "cygwin", ))] pub use libc::statvfs as statfs_fn; @@ -189,7 +191,7 @@ pub struct MountInfo { pub dummy: bool, } -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] fn replace_special_chars(s: &[u8]) -> Vec { use bstr::ByteSlice; @@ -205,7 +207,7 @@ fn replace_special_chars(s: &[u8]) -> Vec { } impl MountInfo { - #[cfg(any(target_os = "linux", target_os = "android"))] + #[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] fn new(file_name: &str, raw: &[&[u8]]) -> Option { use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; @@ -459,9 +461,9 @@ use crate::error::UResult; target_os = "windows" ))] use crate::error::USimpleError; -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] use std::fs::File; -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] use std::io::{BufRead, BufReader}; #[cfg(any( target_vendor = "apple", @@ -481,7 +483,7 @@ use std::slice; /// Read file system list. pub fn read_fs_list() -> UResult> { - #[cfg(any(target_os = "linux", target_os = "android"))] + #[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] { let (file_name, f) = File::open(LINUX_MOUNTINFO) .map(|f| (LINUX_MOUNTINFO, f)) @@ -722,6 +724,7 @@ impl FsMeta for StatFs { not(target_os = "solaris"), not(target_os = "redox"), not(target_arch = "s390x"), + not(target_os = "cygwin"), target_pointer_width = "64" ))] return self.f_bsize; @@ -730,6 +733,7 @@ impl FsMeta for StatFs { not(target_os = "freebsd"), not(target_os = "netbsd"), not(target_os = "redox"), + not(target_os = "cygwin"), any( target_arch = "s390x", target_vendor = "apple", @@ -747,6 +751,7 @@ impl FsMeta for StatFs { target_os = "illumos", target_os = "solaris", target_os = "redox", + target_os = "cygwin", all(target_os = "android", target_pointer_width = "64"), ))] return self.f_bsize.try_into().unwrap(); diff --git a/src/uucore/src/lib/features/hardware.rs b/src/uucore/src/lib/features/hardware.rs new file mode 100644 index 000000000..e0325ed2f --- /dev/null +++ b/src/uucore/src/lib/features/hardware.rs @@ -0,0 +1,433 @@ +// 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. + +//! CPU hardware capability detection for performance-sensitive utilities +//! +//! This module provides a unified interface for detecting CPU features and +//! respecting environment-based SIMD policies (e.g., GLIBC_TUNABLES). +//! +//! # Use Cases +//! +//! - `cksum --debug`: Report hardware acceleration capabilities +//! - `wc --debug`: Report SIMD usage and GLIBC_TUNABLES restrictions +//! - Runtime decisions: Enable/disable SIMD paths based on environment +//! +//! # Examples +//! +//! ```no_run +//! use uucore::hardware::{CpuFeatures, simd_policy}; +//! +//! // Simple hardware detection +//! let features = CpuFeatures::detect(); +//! if features.has_avx2() { +//! println!("AVX2 is available"); +//! } +//! +//! // Check SIMD policy (respects GLIBC_TUNABLES) +//! let policy = simd_policy(); +//! if policy.allows_simd() { +//! // Use SIMD-accelerated path +//! } else { +//! // Fall back to scalar implementation +//! } +//! ``` + +use std::env; +use std::sync::OnceLock; + +/// CPU hardware features that affect performance +/// +/// Provides platform-specific CPU feature detection with caching. +/// Detection is performed once and cached for the lifetime of the process. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CpuFeatures { + /// AVX-512 support (x86/x86_64 only) + avx512: bool, + /// AVX2 support (x86/x86_64 only) + avx2: bool, + /// PCLMULQDQ support for CRC acceleration (x86/x86_64 only) + pclmul: bool, + /// VMULL support for CRC acceleration (ARM only) + vmull: bool, + /// SSE2 support (x86/x86_64 only) + sse2: bool, + /// ARM ASIMD/NEON support (aarch64 only) + asimd: bool, +} + +impl CpuFeatures { + /// Detect available CPU features (cached after first call) + /// + /// This function uses a singleton pattern to ensure feature detection + /// happens only once per process. Thread-safe. + /// + /// # Examples + /// + /// ```no_run + /// use uucore::hardware::CpuFeatures; + /// + /// let features = CpuFeatures::detect(); + /// println!("AVX2: {}", features.has_avx2()); + /// ``` + pub fn detect() -> Self { + static FEATURES: OnceLock = OnceLock::new(); + *FEATURES.get_or_init(Self::detect_impl) + } + + fn detect_impl() -> Self { + Self { + avx512: detect_avx512(), + avx2: detect_avx2(), + pclmul: detect_pclmul(), + vmull: detect_vmull(), + sse2: detect_sse2(), + asimd: detect_asimd(), + } + } + + /// Check if AVX-512 is available (x86/x86_64 only) + pub fn has_avx512(&self) -> bool { + self.avx512 + } + + /// Check if AVX2 is available (x86/x86_64 only) + pub fn has_avx2(&self) -> bool { + self.avx2 + } + + /// Check if PCLMULQDQ is available (x86/x86_64 only) + pub fn has_pclmul(&self) -> bool { + self.pclmul + } + + /// Check if VMULL is available (ARM only) + pub fn has_vmull(&self) -> bool { + self.vmull + } + + /// Check if SSE2 is available (x86/x86_64 only) + pub fn has_sse2(&self) -> bool { + self.sse2 + } + + /// Check if ARM ASIMD/NEON is available (aarch64 only) + pub fn has_asimd(&self) -> bool { + self.asimd + } + + /// Get list of available features as strings + /// + /// Returns uppercase feature names (e.g., "AVX2", "SSE2", "ASIMD") + pub fn available_features(&self) -> Vec<&'static str> { + let mut features = Vec::new(); + if self.avx512 { + features.push("AVX512"); + } + if self.avx2 { + features.push("AVX2"); + } + if self.pclmul { + features.push("PCLMUL"); + } + if self.vmull { + features.push("VMULL"); + } + if self.sse2 { + features.push("SSE2"); + } + if self.asimd { + features.push("ASIMD"); + } + features + } +} + +/// SIMD policy based on environment variables +/// +/// Respects GLIBC_TUNABLES environment variable to disable specific CPU features. +/// This is used by GNU utilities to allow users to disable hardware acceleration. +#[derive(Debug, Clone)] +pub struct SimdPolicy { + /// Features disabled via GLIBC_TUNABLES (e.g., ["AVX2", "AVX512F"]) + disabled_by_env: Vec, + /// Hardware features actually available + hardware_features: CpuFeatures, +} + +impl SimdPolicy { + /// Create a new SIMD policy by checking environment and hardware + fn new() -> Self { + let tunables = env::var("GLIBC_TUNABLES").unwrap_or_default(); + let disabled_by_env = parse_disabled_features(&tunables); + let hardware_features = CpuFeatures::detect(); + + Self { + disabled_by_env, + hardware_features, + } + } + + /// Check if SIMD operations are allowed + /// + /// Returns `false` if any features are disabled via GLIBC_TUNABLES, + /// regardless of what's available in hardware. + /// + /// # Examples + /// + /// ```no_run + /// use uucore::hardware::simd_policy; + /// + /// let policy = simd_policy(); + /// if policy.allows_simd() { + /// // Use SIMD-accelerated bytecount + /// } else { + /// // Use scalar fallback + /// } + /// ``` + pub fn allows_simd(&self) -> bool { + self.disabled_by_env.is_empty() + } + + /// Get list of features disabled by environment + pub fn disabled_features(&self) -> &[String] { + &self.disabled_by_env + } + + /// Get available hardware features + pub fn hardware_features(&self) -> &CpuFeatures { + &self.hardware_features + } + + /// Get list of features that are both available and not disabled + pub fn enabled_features(&self) -> Vec<&'static str> { + if !self.allows_simd() { + return Vec::new(); + } + self.hardware_features.available_features() + } +} + +/// Get the global SIMD policy (cached) +/// +/// This checks both hardware capabilities and the GLIBC_TUNABLES environment +/// variable. The result is cached for the lifetime of the process. +/// +/// # Examples +/// +/// ```no_run +/// use uucore::hardware::simd_policy; +/// +/// let policy = simd_policy(); +/// if policy.allows_simd() { +/// println!("SIMD is enabled"); +/// } else { +/// println!("SIMD disabled by: {:?}", policy.disabled_features()); +/// } +/// ``` +pub fn simd_policy() -> &'static SimdPolicy { + static POLICY: OnceLock = OnceLock::new(); + POLICY.get_or_init(SimdPolicy::new) +} + +// Platform-specific feature detection + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +fn detect_avx512() -> bool { + if cfg!(target_os = "android") { + false + } else { + std::arch::is_x86_feature_detected!("avx512f") + && std::arch::is_x86_feature_detected!("avx512bw") + } +} + +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +fn detect_avx512() -> bool { + false +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +fn detect_avx2() -> bool { + if cfg!(target_os = "android") { + false + } else { + std::arch::is_x86_feature_detected!("avx2") + } +} + +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +fn detect_avx2() -> bool { + false +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +fn detect_pclmul() -> bool { + if cfg!(target_os = "android") { + false + } else { + std::arch::is_x86_feature_detected!("pclmulqdq") + } +} + +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +fn detect_pclmul() -> bool { + false +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +fn detect_sse2() -> bool { + if cfg!(target_os = "android") { + false + } else { + std::arch::is_x86_feature_detected!("sse2") + } +} + +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +fn detect_sse2() -> bool { + false +} + +#[cfg(all(target_arch = "aarch64", target_endian = "little"))] +fn detect_asimd() -> bool { + if cfg!(target_os = "android") { + false + } else { + std::arch::is_aarch64_feature_detected!("asimd") + } +} + +#[cfg(not(all(target_arch = "aarch64", target_endian = "little")))] +fn detect_asimd() -> bool { + false +} + +#[cfg(target_arch = "aarch64")] +fn detect_vmull() -> bool { + // VMULL is part of ARM NEON/ASIMD + // For now, we use ASIMD as a proxy + detect_asimd() +} + +#[cfg(not(target_arch = "aarch64"))] +fn detect_vmull() -> bool { + false +} + +// GLIBC_TUNABLES parsing + +/// Parse GLIBC_TUNABLES environment variable for disabled features +/// +/// Format: `glibc.cpu.hwcaps=-AVX2,-AVX512F` +/// Multiple tunable sections can be separated by colons. +fn parse_disabled_features(tunables: &str) -> Vec { + if tunables.is_empty() { + return Vec::new(); + } + + let mut disabled = Vec::new(); + + // GLIBC_TUNABLES format: "tunable1=value1:tunable2=value2" + for entry in tunables.split(':') { + let entry = entry.trim(); + let Some((name, raw_value)) = entry.split_once('=') else { + continue; + }; + + // We only care about glibc.cpu.hwcaps + if name.trim() != "glibc.cpu.hwcaps" { + continue; + } + + // Parse comma-separated features, disabled ones start with '-' + for token in raw_value.split(',') { + let token = token.trim(); + if let Some(feature) = token.strip_prefix('-') { + let feature = feature.trim().to_ascii_uppercase(); + if !feature.is_empty() { + disabled.push(feature); + } + } + } + } + + disabled +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cpu_features_detection() { + let features = CpuFeatures::detect(); + // Just verify it doesn't panic and returns consistent results + let features2 = CpuFeatures::detect(); + assert_eq!(features, features2); + } + + #[test] + fn test_available_features() { + let features = CpuFeatures::detect(); + let available = features.available_features(); + // Should return a list (may be empty on some platforms) + assert!(available.iter().all(|s| !s.is_empty())); + } + + #[test] + fn test_parse_disabled_features_empty() { + assert_eq!(parse_disabled_features(""), Vec::::new()); + } + + #[test] + fn test_parse_disabled_features_single() { + let result = parse_disabled_features("glibc.cpu.hwcaps=-AVX2"); + assert_eq!(result, vec!["AVX2"]); + } + + #[test] + fn test_parse_disabled_features_multiple() { + let result = parse_disabled_features("glibc.cpu.hwcaps=-AVX2,-AVX512F"); + assert_eq!(result, vec!["AVX2", "AVX512F"]); + } + + #[test] + fn test_parse_disabled_features_mixed() { + let result = parse_disabled_features("glibc.cpu.hwcaps=-AVX2,SSE2,-AVX512F"); + // Only features with '-' prefix are disabled + assert_eq!(result, vec!["AVX2", "AVX512F"]); + } + + #[test] + fn test_parse_disabled_features_with_other_tunables() { + let result = + parse_disabled_features("glibc.malloc.check=1:glibc.cpu.hwcaps=-AVX2:other=value"); + assert_eq!(result, vec!["AVX2"]); + } + + #[test] + fn test_parse_disabled_features_case_insensitive() { + let result = parse_disabled_features("glibc.cpu.hwcaps=-avx2,-Avx512f"); + // Should normalize to uppercase + assert_eq!(result, vec!["AVX2", "AVX512F"]); + } + + #[test] + fn test_simd_policy() { + let policy = simd_policy(); + // Just verify it works + let _ = policy.allows_simd(); + let _ = policy.disabled_features(); + let _ = policy.enabled_features(); + } + + #[test] + fn test_simd_policy_caching() { + let policy1 = simd_policy(); + let policy2 = simd_policy(); + // Should be same instance (pointer equality) + assert!(std::ptr::eq(policy1, policy2)); + } +} diff --git a/src/uucore/src/lib/features/mode.rs b/src/uucore/src/lib/features/mode.rs index 50ed8c97c..d562f1fe0 100644 --- a/src/uucore/src/lib/features/mode.rs +++ b/src/uucore/src/lib/features/mode.rs @@ -7,7 +7,7 @@ // spell-checker:ignore (vars) fperm srwx -use libc::{S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR, mode_t, umask}; +use libc::umask; pub fn parse_numeric(fperm: u32, mut mode: &str, considering_dir: bool) -> Result { let (op, pos) = parse_op(mode).map_or_else(|_| (None, 0), |(op, pos)| (Some(op), pos)); @@ -137,23 +137,36 @@ fn parse_change(mode: &str, fperm: u32, considering_dir: bool) -> (u32, usize) { (srwx, pos) } -#[allow(clippy::unnecessary_cast)] -pub fn parse_mode(mode: &str) -> Result { - #[cfg(all( - not(target_os = "freebsd"), - not(target_vendor = "apple"), - not(target_os = "android") - ))] - let fperm = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; - #[cfg(any(target_os = "freebsd", target_vendor = "apple", target_os = "android"))] - let fperm = (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) as u32; +/// Modify a file mode based on a user-supplied string. +/// Supports comma-separated mode strings like "ug+rwX,o+rX" (same as chmod). +pub fn parse_chmod( + current_mode: u32, + mode_string: &str, + considering_dir: bool, + umask: u32, +) -> Result { + let mut new_mode: u32 = current_mode; - let result = if mode.chars().any(|c| c.is_ascii_digit()) { - parse_numeric(fperm as u32, mode, true) - } else { - parse_symbolic(fperm as u32, mode, get_umask(), true) - }; - result.map(|mode| mode as mode_t) + // Split by commas and process each mode part sequentially + for mode_part in mode_string.split(',') { + let mode_part = mode_part.trim(); + if mode_part.is_empty() { + continue; + } + + new_mode = if mode_part.chars().any(|c| c.is_ascii_digit()) { + parse_numeric(new_mode, mode_part, considering_dir)? + } else { + parse_symbolic(new_mode, mode_part, umask, considering_dir)? + }; + } + + Ok(new_mode) +} + +/// Takes a user-supplied string and tries to parse to u32 mode bitmask. +pub fn parse(mode_string: &str, considering_dir: bool, umask: u32) -> Result { + parse_chmod(0, mode_string, considering_dir, umask) } pub fn get_umask() -> u32 { @@ -183,23 +196,142 @@ pub fn get_umask() -> u32 { } #[cfg(test)] -mod test { +mod tests { + + use super::parse; + use super::parse_chmod; #[test] - fn symbolic_modes() { - assert_eq!(super::parse_mode("u+x").unwrap(), 0o766); - assert_eq!( - super::parse_mode("+x").unwrap(), - if crate::os::is_wsl_1() { 0o776 } else { 0o777 } - ); - assert_eq!(super::parse_mode("a-w").unwrap(), 0o444); - assert_eq!(super::parse_mode("g-r").unwrap(), 0o626); + fn test_chmod_symbolic_modes() { + assert_eq!(parse_chmod(0o666, "u+x", false, 0).unwrap(), 0o766); + assert_eq!(parse_chmod(0o666, "+x", false, 0).unwrap(), 0o777); + assert_eq!(parse_chmod(0o666, "a-w", false, 0).unwrap(), 0o444); + assert_eq!(parse_chmod(0o666, "g-r", false, 0).unwrap(), 0o626); } #[test] - fn numeric_modes() { - assert_eq!(super::parse_mode("644").unwrap(), 0o644); - assert_eq!(super::parse_mode("+100").unwrap(), 0o766); - assert_eq!(super::parse_mode("-4").unwrap(), 0o662); + fn test_chmod_numeric_modes() { + assert_eq!(parse_chmod(0o666, "644", false, 0).unwrap(), 0o644); + assert_eq!(parse_chmod(0o666, "+100", false, 0).unwrap(), 0o766); + assert_eq!(parse_chmod(0o666, "-4", false, 0).unwrap(), 0o662); + } + + #[test] + fn test_parse_numeric_mode() { + // Simple numeric mode + assert_eq!(parse("644", false, 0).unwrap(), 0o644); + assert_eq!(parse("755", false, 0).unwrap(), 0o755); + assert_eq!(parse("777", false, 0).unwrap(), 0o777); + assert_eq!(parse("600", false, 0).unwrap(), 0o600); + } + + #[test] + fn test_parse_numeric_mode_with_operator() { + // Numeric mode with + operator + assert_eq!(parse("+100", false, 0).unwrap(), 0o100); + assert_eq!(parse("+644", false, 0).unwrap(), 0o644); + + // Numeric mode with - operator (starting from 0, so nothing to remove) + assert_eq!(parse("-4", false, 0).unwrap(), 0); + // But if we first set a mode, then remove bits + assert_eq!(parse("644,-4", false, 0).unwrap(), 0o640); + } + + #[test] + fn test_parse_symbolic_mode() { + // Simple symbolic modes + assert_eq!(parse("u+x", false, 0).unwrap(), 0o100); + assert_eq!(parse("g+w", false, 0).unwrap(), 0o020); + assert_eq!(parse("o+r", false, 0).unwrap(), 0o004); + assert_eq!(parse("a+x", false, 0).unwrap(), 0o111); + } + + #[test] + fn test_parse_symbolic_mode_multiple_permissions() { + // Multiple permissions in one mode + assert_eq!(parse("u+rw", false, 0).unwrap(), 0o600); + assert_eq!(parse("ug+rwx", false, 0).unwrap(), 0o770); + assert_eq!(parse("a+rwx", false, 0).unwrap(), 0o777); + } + + #[test] + fn test_parse_comma_separated_modes() { + // Comma-separated mode strings (as mentioned in the doc comment) + assert_eq!(parse("ug+rwX,o+rX", false, 0).unwrap(), 0o664); + assert_eq!(parse("u+rwx,g+rx,o+r", false, 0).unwrap(), 0o754); + assert_eq!(parse("u+w,g+w,o+w", false, 0).unwrap(), 0o222); + } + + #[test] + fn test_parse_comma_separated_with_spaces() { + // Comma-separated with spaces (should be trimmed) + assert_eq!(parse("u+rw, g+rw, o+r", false, 0).unwrap(), 0o664); + assert_eq!(parse(" u+x , g+x ", false, 0).unwrap(), 0o110); + } + + #[test] + fn test_parse_mixed_numeric_and_symbolic() { + // Mix of numeric and symbolic modes + assert_eq!(parse("644,u+x", false, 0).unwrap(), 0o744); + assert_eq!(parse("u+rw,755", false, 0).unwrap(), 0o755); + } + + #[test] + fn test_parse_empty_string() { + // Empty string should return 0 + assert_eq!(parse("", false, 0).unwrap(), 0); + assert_eq!(parse(" ", false, 0).unwrap(), 0); + assert_eq!(parse(",,", false, 0).unwrap(), 0); + } + + #[test] + fn test_parse_with_umask() { + // Test with umask (affects symbolic modes when no level is specified) + let umask = 0o022; + assert_eq!(parse("+w", false, umask).unwrap(), 0o200); + // The umask should be respected for symbolic modes without explicit level + } + + #[test] + fn test_parse_considering_dir() { + // Test directory vs file mode differences + // For directories, X (capital X) should add execute permission + assert_eq!(parse("a+X", true, 0).unwrap(), 0o111); + // For files without execute, X should not add execute + assert_eq!(parse("a+X", false, 0).unwrap(), 0o000); + + // Numeric modes for directories preserve setuid/setgid bits + assert_eq!(parse("755", true, 0).unwrap(), 0o755); + } + + #[test] + fn test_parse_invalid_modes() { + // Invalid numeric mode (too large) + assert!(parse("10000", false, 0).is_err()); + + // Invalid operator + assert!(parse("u*rw", false, 0).is_err()); + + // Invalid symbolic mode + assert!(parse("invalid", false, 0).is_err()); + } + + #[test] + fn test_parse_complex_combinations() { + // Complex real-world examples + assert_eq!(parse("u=rwx,g=rx,o=r", false, 0).unwrap(), 0o754); + // To test removal, we need to first set permissions, then remove them + assert_eq!(parse("644,a-w", false, 0).unwrap(), 0o444); + assert_eq!(parse("644,g-r", false, 0).unwrap(), 0o604); + } + + #[test] + fn test_parse_sequential_application() { + // Test that comma-separated modes are applied sequentially + // First set to 644, then add execute for user + assert_eq!(parse("644,u+x", false, 0).unwrap(), 0o744); + + // First add user write, then set to 755 (should override) + assert_eq!(parse("u+w,755", false, 0).unwrap(), 0o755); } } diff --git a/src/uucore/src/lib/features/parser/mod.rs b/src/uucore/src/lib/features/parser/mod.rs index 800fe6e8c..d2fc27721 100644 --- a/src/uucore/src/lib/features/parser/mod.rs +++ b/src/uucore/src/lib/features/parser/mod.rs @@ -4,8 +4,13 @@ // file that was distributed with this source code. // spell-checker:ignore extendedbigdecimal +#[cfg(any(feature = "parser", feature = "parser-num"))] pub mod num_parser; +#[cfg(any(feature = "parser", feature = "parser-glob"))] pub mod parse_glob; +#[cfg(any(feature = "parser", feature = "parser-size"))] pub mod parse_size; +#[cfg(any(feature = "parser", feature = "parser-num"))] pub mod parse_time; +#[cfg(any(feature = "parser", feature = "parser-num"))] pub mod shortcut_value_parser; diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index 4e7fe81c9..0bccb2173 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -3,8 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (vars/api) fcntl setrlimit setitimer rubout pollable sysconf -// spell-checker:ignore (vars/signals) ABRT ALRM CHLD SEGV SIGABRT SIGALRM SIGBUS SIGCHLD SIGCONT SIGDANGER SIGEMT SIGFPE SIGHUP SIGILL SIGINFO SIGINT SIGIO SIGIOT SIGKILL SIGMIGRATE SIGMSG SIGPIPE SIGPRE SIGPROF SIGPWR SIGQUIT SIGSEGV SIGSTOP SIGSYS SIGTALRM SIGTERM SIGTRAP SIGTSTP SIGTHR SIGTTIN SIGTTOU SIGURG SIGUSR SIGVIRT SIGVTALRM SIGWINCH SIGXCPU SIGXFSZ STKFLT PWR THR TSTP TTIN TTOU VIRT VTALRM XCPU XFSZ SIGCLD SIGPOLL SIGWAITING SIGAIOCANCEL SIGLWP SIGFREEZE SIGTHAW SIGCANCEL SIGLOST SIGXRES SIGJVM SIGRTMIN SIGRT SIGRTMAX TALRM AIOCANCEL XRES RTMIN RTMAX +// spell-checker:ignore (vars/api) fcntl setrlimit setitimer rubout pollable sysconf pgrp +// spell-checker:ignore (vars/signals) ABRT ALRM CHLD SEGV SIGABRT SIGALRM SIGBUS SIGCHLD SIGCONT SIGDANGER SIGEMT SIGFPE SIGHUP SIGILL SIGINFO SIGINT SIGIO SIGIOT SIGKILL SIGMIGRATE SIGMSG SIGPIPE SIGPRE SIGPROF SIGPWR SIGQUIT SIGSEGV SIGSTOP SIGSYS SIGTALRM SIGTERM SIGTRAP SIGTSTP SIGTHR SIGTTIN SIGTTOU SIGURG SIGUSR SIGVIRT SIGVTALRM SIGWINCH SIGXCPU SIGXFSZ STKFLT PWR THR TSTP TTIN TTOU VIRT VTALRM XCPU XFSZ SIGCLD SIGPOLL SIGWAITING SIGAIOCANCEL SIGLWP SIGFREEZE SIGTHAW SIGCANCEL SIGLOST SIGXRES SIGJVM SIGRTMIN SIGRT SIGRTMAX TALRM AIOCANCEL XRES RTMIN RTMAX LTOSTOP //! This module provides a way to handle signals in a platform-independent way. //! It provides a way to convert signal names to their corresponding values and vice versa. @@ -346,6 +346,49 @@ pub static ALL_SIGNALS: [&str; 37] = [ "VIRT", "TALRM", ]; +/* + The following signals are defined in Cygwin + https://cygwin.com/cgit/newlib-cygwin/tree/winsup/cygwin/include/cygwin/signal.h + + SIGHUP 1 hangup + SIGINT 2 interrupt + SIGQUIT 3 quit + SIGILL 4 illegal instruction (not reset when caught) + SIGTRAP 5 trace trap (not reset when caught) + SIGABRT 6 used by abort + SIGEMT 7 EMT instruction + SIGFPE 8 floating point exception + SIGKILL 9 kill (cannot be caught or ignored) + SIGBUS 10 bus error + SIGSEGV 11 segmentation violation + SIGSYS 12 bad argument to system call + SIGPIPE 13 write on a pipe with no one to read it + SIGALRM 14 alarm clock + SIGTERM 15 software termination signal from kill + SIGURG 16 urgent condition on IO channel + SIGSTOP 17 sendable stop signal not from tty + SIGTSTP 18 stop signal from tty + SIGCONT 19 continue a stopped process + SIGCHLD 20 to parent on child stop or exit + SIGTTIN 21 to readers pgrp upon background tty read + SIGTTOU 22 like TTIN for output if (tp->t_local<OSTOP) + SIGIO 23 input/output possible signal + SIGXCPU 24 exceeded CPU time limit + SIGXFSZ 25 exceeded file size limit + SIGVTALRM 26 virtual time alarm + SIGPROF 27 profiling time alarm + SIGWINCH 28 window changed + SIGLOST 29 resource lost (eg, record-lock lost) + SIGUSR1 30 user defined signal 1 + SIGUSR2 31 user defined signal 2 +*/ +#[cfg(target_os = "cygwin")] +pub static ALL_SIGNALS: [&str; 32] = [ + "EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "EMT", "FPE", "KILL", "BUS", "SEGV", + "SYS", "PIPE", "ALRM", "TERM", "URG", "STOP", "TSTP", "CONT", "CHLD", "TTIN", "TTOU", "IO", + "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "PWR", "USR1", "USR2", +]; + /// Returns the signal number for a given signal name or value. pub fn signal_by_name_or_value(signal_name_or_value: &str) -> Option { let signal_name_upcase = signal_name_or_value.to_uppercase(); diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index e517a03fc..66fb752ab 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -12,12 +12,52 @@ //! [`DigestWriter`] struct provides a wrapper around [`Digest`] that //! implements the [`Write`] trait, for use in situations where calling //! [`write`] would be useful. -use std::io::Write; -use hex::encode; +use std::io::{self, Write}; + +use data_encoding::BASE64; + #[cfg(windows)] use memchr::memmem; +use crate::error::{UResult, USimpleError}; + +/// Represents the output of a checksum computation. +#[derive(Debug)] +pub enum DigestOutput { + /// Varying-size output + Vec(Vec), + /// Legacy output for Crc and Crc32B modes + Crc(u32), + /// Legacy output for Sysv and BSD modes + U16(u16), +} + +impl DigestOutput { + pub fn write_raw(&self, mut w: impl std::io::Write) -> io::Result<()> { + match self { + Self::Vec(buf) => w.write_all(buf), + // For legacy outputs, print them in big endian + Self::Crc(n) => w.write_all(&n.to_be_bytes()), + Self::U16(n) => w.write_all(&n.to_be_bytes()), + } + } + + pub fn to_hex(&self) -> UResult { + match self { + Self::Vec(buf) => Ok(hex::encode(buf)), + _ => Err(USimpleError::new(1, "Legacy output cannot be encoded")), + } + } + + pub fn to_base64(&self) -> UResult { + match self { + Self::Vec(buf) => Ok(BASE64.encode(buf)), + _ => Err(USimpleError::new(1, "Legacy output cannot be encoded")), + } + } +} + pub trait Digest { fn new() -> Self where @@ -29,10 +69,11 @@ pub trait Digest { fn output_bytes(&self) -> usize { self.output_bits().div_ceil(8) } - fn result_str(&mut self) -> String { + + fn result(&mut self) -> DigestOutput { let mut buf: Vec = vec![0; self.output_bytes()]; self.hash_finalize(&mut buf); - encode(buf) + DigestOutput::Vec(buf) } } @@ -167,10 +208,12 @@ impl Digest for Crc { out.copy_from_slice(&self.digest.finalize().to_ne_bytes()); } - fn result_str(&mut self) -> String { + fn result(&mut self) -> DigestOutput { let mut out: [u8; 8] = [0; 8]; self.hash_finalize(&mut out); - u64::from_ne_bytes(out).to_string() + + let x = u64::from_ne_bytes(out); + DigestOutput::Crc((x & (u32::MAX as u64)) as u32) } fn reset(&mut self) { @@ -214,10 +257,10 @@ impl Digest for CRC32B { 32 } - fn result_str(&mut self) -> String { + fn result(&mut self) -> DigestOutput { let mut out = [0; 4]; self.hash_finalize(&mut out); - format!("{}", u32::from_be_bytes(out)) + DigestOutput::Crc(u32::from_be_bytes(out)) } } @@ -240,10 +283,10 @@ impl Digest for Bsd { out.copy_from_slice(&self.state.to_ne_bytes()); } - fn result_str(&mut self) -> String { - let mut _out: Vec = vec![0; 2]; + fn result(&mut self) -> DigestOutput { + let mut _out = [0; 2]; self.hash_finalize(&mut _out); - format!("{}", self.state) + DigestOutput::U16(self.state) } fn reset(&mut self) { @@ -275,10 +318,10 @@ impl Digest for SysV { out.copy_from_slice(&(self.state as u16).to_ne_bytes()); } - fn result_str(&mut self) -> String { - let mut _out: Vec = vec![0; 2]; + fn result(&mut self) -> DigestOutput { + let mut _out = [0; 2]; self.hash_finalize(&mut _out); - format!("{}", self.state) + DigestOutput::U16((self.state & (u16::MAX as u32)) as u16) } fn reset(&mut self) { @@ -292,7 +335,7 @@ impl Digest for SysV { // Implements the Digest trait for sha2 / sha3 algorithms with fixed output macro_rules! impl_digest_common { - ($algo_type: ty, $size: expr) => { + ($algo_type: ty, $size: literal) => { impl Digest for $algo_type { fn new() -> Self { Self(Default::default()) @@ -319,7 +362,7 @@ macro_rules! impl_digest_common { // Implements the Digest trait for sha2 / sha3 algorithms with variable output macro_rules! impl_digest_shake { - ($algo_type: ty) => { + ($algo_type: ty, $output_bits: literal) => { impl Digest for $algo_type { fn new() -> Self { Self(Default::default()) @@ -338,7 +381,13 @@ macro_rules! impl_digest_shake { } fn output_bits(&self) -> usize { - 0 + $output_bits + } + + fn result(&mut self) -> DigestOutput { + let mut bytes = vec![0; self.output_bits().div_ceil(8)]; + self.hash_finalize(&mut bytes); + DigestOutput::Vec(bytes) } } }; @@ -368,8 +417,8 @@ impl_digest_common!(Sha3_512, 512); pub struct Shake128(sha3::Shake128); pub struct Shake256(sha3::Shake256); -impl_digest_shake!(Shake128); -impl_digest_shake!(Shake256); +impl_digest_shake!(Shake128, 256); +impl_digest_shake!(Shake256, 512); /// A struct that writes to a digest. /// @@ -501,14 +550,14 @@ mod tests { writer_crlf.write_all(b"\r").unwrap(); writer_crlf.write_all(b"\n").unwrap(); writer_crlf.finalize(); - let result_crlf = digest.result_str(); + let result_crlf = digest.result(); // We expect "\r\n" to be replaced with "\n" in text mode on Windows. let mut digest = Box::new(Md5::new()) as Box; let mut writer_lf = DigestWriter::new(&mut digest, false); writer_lf.write_all(b"\n").unwrap(); writer_lf.finalize(); - let result_lf = digest.result_str(); + let result_lf = digest.result(); assert_eq!(result_crlf, result_lf); } diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 000bd23fd..40632ae98 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -54,11 +54,18 @@ pub use crate::features::fast_inc; pub use crate::features::format; #[cfg(feature = "fs")] pub use crate::features::fs; +#[cfg(feature = "hardware")] +pub use crate::features::hardware; #[cfg(feature = "i18n-common")] pub use crate::features::i18n; #[cfg(feature = "lines")] pub use crate::features::lines; -#[cfg(feature = "parser")] +#[cfg(any( + feature = "parser", + feature = "parser-num", + feature = "parser-size", + feature = "parser-glob" +))] pub use crate::features::parser; #[cfg(feature = "quoting-style")] pub use crate::features::quoting_style; @@ -321,7 +328,10 @@ pub fn set_utility_is_second_arg() { // args_os() can be expensive to call, it copies all of argv before iterating. // So if we want only the first arg or so it's overkill. We cache it. +#[cfg(windows)] static ARGV: LazyLock> = LazyLock::new(|| wild::args_os().collect()); +#[cfg(not(windows))] +static ARGV: LazyLock> = LazyLock::new(|| std::env::args_os().collect()); static UTIL_NAME: LazyLock = LazyLock::new(|| { let base_index = usize::from(get_utility_is_second_arg()); diff --git a/tests/benches/factor/Cargo.toml b/tests/benches/factor/Cargo.toml deleted file mode 100644 index 30d4cc8b1..000000000 --- a/tests/benches/factor/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "uu_factor_benches" -version = "0.0.0" -authors = ["nicoo "] -description = "Benchmarks for the uu_factor integer factorization tool" -edition.workspace = true -homepage.workspace = true -license.workspace = true -publish = false - -[dev-dependencies] -array-init = "2.0.0" -criterion = "0.6.0" -rand = "0.9.1" -rand_chacha = "0.9.0" -num-prime = "0.4.4" - -[[bench]] -name = "table" -harness = false diff --git a/tests/benches/factor/benches/table.rs b/tests/benches/factor/benches/table.rs deleted file mode 100644 index 504b7e8e3..000000000 --- a/tests/benches/factor/benches/table.rs +++ /dev/null @@ -1,64 +0,0 @@ -// This file is part of the uutils coreutils package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -// spell-checker:ignore funcs - -use array_init::array_init; -use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; - -fn table(c: &mut Criterion) { - #[cfg(target_os = "linux")] - check_personality(); - - const INPUT_SIZE: usize = 128; - - let inputs = { - // Deterministic RNG; use an explicitly-named RNG to guarantee stability - use rand::{RngCore, SeedableRng}; - use rand_chacha::ChaCha8Rng; - const SEED: u64 = 0xdead_bebe_ea75_cafe; // spell-checker:disable-line - let mut rng = ChaCha8Rng::seed_from_u64(SEED); - - std::iter::repeat_with(move || array_init::<_, _, INPUT_SIZE>(|_| rng.next_u64())) - }; - - let mut group = c.benchmark_group("table"); - group.throughput(Throughput::Elements(INPUT_SIZE as _)); - for a in inputs.take(10) { - let a_str = format!("{a:?}"); - group.bench_with_input(BenchmarkId::new("factor", &a_str), &a, |b, &a| { - b.iter(|| { - for n in a { - let _r = num_prime::nt_funcs::factors(n, None); - } - }); - }); - } - group.finish(); -} - -#[cfg(target_os = "linux")] -fn check_personality() { - use std::fs; - const ADDR_NO_RANDOMIZE: u64 = 0x0040000; - const PERSONALITY_PATH: &str = "/proc/self/personality"; - - let p_string = fs::read_to_string(PERSONALITY_PATH) - .unwrap_or_else(|_| panic!("Couldn't read '{PERSONALITY_PATH}'")) - .strip_suffix('\n') - .unwrap() - .to_owned(); - - let personality = u64::from_str_radix(&p_string, 16) - .unwrap_or_else(|_| panic!("Expected a hex value for personality, got '{p_string:?}'")); - if personality & ADDR_NO_RANDOMIZE == 0 { - eprintln!( - "WARNING: Benchmarking with ASLR enabled (personality is {personality:x}), results might not be reproducible." - ); - } -} - -criterion_group!(benches, table); -criterion_main!(benches); diff --git a/tests/by-util/test_basenc.rs b/tests/by-util/test_basenc.rs index f02de772b..a3c92b885 100644 --- a/tests/by-util/test_basenc.rs +++ b/tests/by-util/test_basenc.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. // spell-checker: ignore (encodings) lsbf msbf +// spell-checker: ignore autopad MFRGG MFRGGZDF abcdeabc baddecode CPNMUO use uutests::{at_and_ucmd, new_ucmd}; @@ -112,6 +113,63 @@ fn test_base32hex_decode() { .stdout_only("nice>base?"); } +#[test] +fn test_base32_autopad_short_quantum() { + new_ucmd!() + .args(&["--base32", "--decode"]) + .pipe_in("MFRGG") + .succeeds() + .stdout_only("abc"); +} + +#[test] +fn test_base32_autopad_multiline_stream() { + new_ucmd!() + .args(&["--base32", "--decode"]) + .pipe_in("MFRGGZDF\nMFRGG") + .succeeds() + .stdout_only("abcdeabc"); +} + +#[test] +fn test_base32_baddecode_keeps_prefix() { + new_ucmd!() + .args(&["--base32", "--decode"]) + .pipe_in("MFRGGZDF=") + .fails() + .stdout_is("abcde") + .stderr_is("basenc: error: invalid input\n"); +} + +#[test] +fn test_base32hex_autopad_short_quantum() { + new_ucmd!() + .args(&["--base32hex", "--decode"]) + .pipe_in("C5H66") + .succeeds() + .stdout_only("abc"); +} + +#[test] +fn test_base32hex_rejects_trailing_garbage() { + new_ucmd!() + .args(&["--base32hex", "-d"]) + .pipe_in("VNC0FKD5W") + .fails() + .stdout_is_bytes(b"\xFD\xD8\x07\xD1\xA5") + .stderr_is("basenc: error: invalid input\n"); +} + +#[test] +fn test_base32hex_truncated_block_keeps_prefix() { + new_ucmd!() + .args(&["--base32hex", "-d"]) + .pipe_in("CPNMUO") + .fails() + .stdout_is_bytes(b"foo") + .stderr_is("basenc: error: invalid input\n"); +} + #[test] fn test_base16() { new_ucmd!() diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 1378aab00..e4d4b0284 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -2,6 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore (words) dirfd subdirs openat FDCWD use std::fs::{OpenOptions, Permissions, metadata, set_permissions}; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; @@ -1280,6 +1281,51 @@ fn test_chmod_non_utf8_paths() { ); } +#[cfg(all(target_os = "linux", feature = "chmod"))] +#[test] +#[ignore = "covered by util/check-safe-traversal.sh"] +fn test_chmod_recursive_uses_dirfd_for_subdirs() { + use std::process::Command; + use uutests::get_tests_binary; + + // strace is required; fail fast if it is missing or not runnable + let output = Command::new("strace") + .arg("-V") + .output() + .expect("strace not found; install strace to run this test"); + assert!( + output.status.success(), + "strace -V failed; ensure strace is installed and usable" + ); + + let (at, _ucmd) = at_and_ucmd!(); + at.mkdir("x"); + at.mkdir("x/y"); + at.mkdir("x/y/z"); + + let log_path = at.plus_as_string("strace.log"); + + let status = Command::new("strace") + .arg("-e") + .arg("openat") + .arg("-o") + .arg(&log_path) + .arg(get_tests_binary!()) + .args(["chmod", "-R", "+x", "x"]) + .current_dir(&at.subdir) + .status() + .expect("failed to run strace"); + assert!(status.success(), "strace run failed"); + + let log = at.read("strace.log"); + + // Regression guard: ensure recursion uses dirfd-relative openat instead of AT_FDCWD with a multi-component path + assert!( + !log.contains("openat(AT_FDCWD, \"x/y"), + "chmod recursed using AT_FDCWD with a multi-component path; expected dirfd-relative openat" + ); +} + #[test] fn test_chmod_colored_output() { // Test colored help message diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index d966e4b1f..d4685d619 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -10,9 +10,8 @@ use uutests::util::TestScenario; use uutests::util::log_info; use uutests::util_name; -const ALGOS: [&str; 12] = [ - "sysv", "bsd", "crc", "crc32b", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", - "blake2b", "sm3", +const ALGOS: [&str; 11] = [ + "sysv", "bsd", "crc", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "blake2b", "sm3", ]; const SHA_LENGTHS: [u32; 4] = [224, 256, 384, 512]; @@ -774,14 +773,31 @@ fn test_blake2b_length() { #[test] fn test_blake2b_length_greater_than_512() { - new_ucmd!() - .arg("--length=1024") - .arg("--algorithm=blake2b") - .arg("lorem_ipsum.txt") - .arg("alice_in_wonderland.txt") - .fails_with_code(1) - .no_stdout() - .stderr_is_fixture("length_larger_than_512.expected"); + for l in ["513", "1024", "73786976294838206464"] { + new_ucmd!() + .arg("--algorithm=blake2b") + .arg("--length") + .arg(l) + .arg("lorem_ipsum.txt") + .fails_with_code(1) + .no_stdout() + .stderr_contains(format!("invalid length: '{l}'")) + .stderr_contains("maximum digest length for 'BLAKE2b' is 512 bits"); + } +} + +#[test] +fn test_blake2b_length_nan() { + for l in ["foo", "512x", "x512", "0xff"] { + new_ucmd!() + .arg("--algorithm=blake2b") + .arg("--length") + .arg(l) + .arg("lorem_ipsum.txt") + .fails_with_code(1) + .no_stdout() + .stderr_contains(format!("invalid length: '{l}'")); + } } #[test] @@ -2302,6 +2318,119 @@ mod gnu_cksum_base64 { } } +/// This module reimplements the cksum-base64-untagged.sh GNU test. +mod gnu_cksum_base64_untagged { + use super::*; + + macro_rules! decl_sha_test { + ($id:ident, $algo:literal, $len:expr) => { + mod $id { + use super::*; + + #[test] + fn check_length_guess() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.write("inp", "test input\n"); + + let compute = ts + .ucmd() + .arg("-a") + .arg($algo) + .arg("-l") + .arg(stringify!($len)) + .arg("--base64") + .arg("--untagged") + .arg("inp") + .succeeds(); + + at.write_bytes("check", compute.stdout()); + + ts.ucmd() + .arg("-a") + .arg($algo) + .arg("--check") + .arg("check") + .succeeds() + .stdout_only("inp: OK\n"); + + at.write("check", " inp"); + + ts.ucmd() + .arg("-a") + .arg($algo) + .arg("check") + .fails() + .stderr_contains(concat!( + "--algorithm=", + $algo, + " requires specifying --length" + )); + } + } + }; + } + + decl_sha_test!(sha2_224, "sha2", 224); + decl_sha_test!(sha2_256, "sha2", 256); + decl_sha_test!(sha2_384, "sha2", 384); + decl_sha_test!(sha2_512, "sha2", 512); + decl_sha_test!(sha3_224, "sha3", 224); + decl_sha_test!(sha3_256, "sha3", 256); + decl_sha_test!(sha3_384, "sha3", 384); + decl_sha_test!(sha3_512, "sha3", 512); + + macro_rules! decl_blake_test { + ($id:ident, $len:expr) => { + mod $id { + use super::*; + + #[test] + fn check_length_guess() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.write("inp", "test input\n"); + + let compute = ts + .ucmd() + .arg("-a") + .arg("blake2b") + .arg("-l") + .arg(stringify!($len)) + .arg("--base64") + .arg("--untagged") + .arg("inp") + .succeeds(); + + at.write_bytes("check", compute.stdout()); + + ts.ucmd() + .arg("-a") + .arg("blake2b") + .arg("--check") + .arg("check") + .succeeds() + .stdout_only("inp: OK\n"); + } + } + }; + } + + decl_blake_test!(blake2b_8, 8); + decl_blake_test!(blake2b_216, 216); + decl_blake_test!(blake2b_224, 224); + decl_blake_test!(blake2b_232, 232); + decl_blake_test!(blake2b_248, 248); + decl_blake_test!(blake2b_256, 256); + decl_blake_test!(blake2b_264, 264); + decl_blake_test!(blake2b_376, 376); + decl_blake_test!(blake2b_384, 384); + decl_blake_test!(blake2b_392, 392); + decl_blake_test!(blake2b_504, 504); + decl_blake_test!(blake2b_512, 512); +} /// This module reimplements the cksum-c.sh GNU test. mod gnu_cksum_c { use super::*; @@ -2329,6 +2458,71 @@ mod gnu_cksum_c { scene } + fn make_scene_with_comment() -> TestScenario { + let scene = make_scene(); + + scene + .fixtures + .append("CHECKSUMS", "# Very important comment\n"); + + scene + } + + fn make_scene_with_invalid_line() -> TestScenario { + let scene = make_scene_with_comment(); + + scene.fixtures.append("CHECKSUMS", "invalid_line\n"); + + scene + } + + #[test] + fn test_tagged_invalid_length() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.write( + "sha2-bad-length.sum", + "SHA2-128 (/dev/null) = 38b060a751ac96384cd9327eb1b1e36a", + ); + + ucmd.arg("--check") + .arg("sha2-bad-length.sum") + .fails() + .stderr_contains("sha2-bad-length.sum: no properly formatted checksum lines found"); + } + + #[test] + #[cfg_attr(not(unix), ignore = "/dev/null is only available on UNIX")] + fn test_untagged_base64_matching_tag() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.write("tag-prefix.sum", "SHA1+++++++++++++++++++++++= /dev/null"); + + ucmd.arg("--check") + .arg("-a") + .arg("sha1") + .arg("tag-prefix.sum") + .fails() + .stderr_contains("WARNING: 1 computed checksum did NOT match"); + } + + #[test] + #[cfg_attr(windows, ignore = "Awkward filename is not supported on windows")] + fn test_awkward_filename() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + let awkward_file = "abc (f) = abc"; + + at.touch(awkward_file); + + let result = ts.ucmd().arg("-a").arg("sha1").arg(awkward_file).succeeds(); + + at.write_bytes("tag-awkward.sum", result.stdout()); + + ts.ucmd().arg("-c").arg("tag-awkward.sum").succeeds(); + } + #[test] #[ignore = "todo"] fn test_signed_checksums() { @@ -2380,16 +2574,6 @@ mod gnu_cksum_c { .no_output(); } - fn make_scene_with_comment() -> TestScenario { - let scene = make_scene(); - - scene - .fixtures - .append("CHECKSUMS", "# Very important comment\n"); - - scene - } - #[test] fn test_status_with_comment() { let scene = make_scene_with_comment(); @@ -2403,14 +2587,6 @@ mod gnu_cksum_c { .no_output(); } - fn make_scene_with_invalid_line() -> TestScenario { - let scene = make_scene_with_comment(); - - scene.fixtures.append("CHECKSUMS", "invalid_line\n"); - - scene - } - #[test] fn test_check_strict() { let scene = make_scene_with_invalid_line(); @@ -2579,6 +2755,20 @@ mod gnu_cksum_c { .stderr_contains("CHECKSUMS-missing: no file was verified"); } + #[test] + fn test_ignore_missing_stdin() { + let scene = make_scene_with_checksum_missing(); + + scene + .ucmd() + .arg("--ignore-missing") + .arg("--check") + .pipe_in_fixture("CHECKSUMS-missing") + .fails() + .no_stdout() + .stderr_contains("'standard input': no file was verified"); + } + #[test] fn test_status_and_warn() { let scene = make_scene_with_checksum_missing(); @@ -2746,3 +2936,113 @@ mod format_mix { .stderr_contains("cksum: WARNING: 1 line is improperly formatted"); } } + +#[cfg(not(target_os = "android"))] +mod debug_flag { + use super::*; + + #[test] + fn test_debug_flag() { + // Test with default CRC algorithm - should output CPU feature detection + new_ucmd!() + .arg("--debug") + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture("crc_single_file.expected") + .stderr_contains("avx512") + .stderr_contains("avx2") + .stderr_contains("pclmul"); + + // Test with MD5 algorithm - CPU detection should be same regardless of algorithm + new_ucmd!() + .arg("--debug") + .arg("-a") + .arg("md5") + .arg("lorem_ipsum.txt") + .succeeds() + .stdout_is_fixture("md5_single_file.expected") + .stderr_contains("avx512") + .stderr_contains("avx2") + .stderr_contains("pclmul"); + + // Test with stdin - CPU detection should appear once + new_ucmd!() + .arg("--debug") + .pipe_in("test") + .succeeds() + .stderr_contains("avx512") + .stderr_contains("avx2") + .stderr_contains("pclmul"); + + // Test with multiple files - CPU detection should appear once, not per file + new_ucmd!() + .arg("--debug") + .arg("lorem_ipsum.txt") + .arg("alice_in_wonderland.txt") + .succeeds() + .stdout_is_fixture("crc_multiple_files.expected") + .stderr_str_check(|stderr| { + // Verify CPU detection happens only once by checking the count of each feature line + let avx512_count = stderr + .lines() + .filter(|line| line.contains("avx512")) + .count(); + let avx2_count = stderr.lines().filter(|line| line.contains("avx2")).count(); + let pclmul_count = stderr + .lines() + .filter(|line| line.contains("pclmul")) + .count(); + + avx512_count == 1 && avx2_count == 1 && pclmul_count == 1 + }); + } + + #[test] + fn test_debug_with_algorithms() { + // Test with SHA256 - CPU detection should be same regardless of algorithm + new_ucmd!() + .arg("--debug") + .arg("-a") + .arg("sha256") + .arg("lorem_ipsum.txt") + .succeeds() + .stderr_contains("avx512") + .stderr_contains("avx2") + .stderr_contains("pclmul"); + + // Test with BLAKE2b default length + new_ucmd!() + .arg("--debug") + .arg("-a") + .arg("blake2b") + .arg("lorem_ipsum.txt") + .succeeds() + .stderr_contains("avx512") + .stderr_contains("avx2") + .stderr_contains("pclmul"); + + // Test with BLAKE2b custom length + new_ucmd!() + .arg("--debug") + .arg("-a") + .arg("blake2b") + .arg("--length") + .arg("256") + .arg("lorem_ipsum.txt") + .succeeds() + .stderr_contains("avx512") + .stderr_contains("avx2") + .stderr_contains("pclmul"); + + // Test with SHA1 + new_ucmd!() + .arg("--debug") + .arg("-a") + .arg("sha1") + .arg("lorem_ipsum.txt") + .succeeds() + .stderr_contains("avx512") + .stderr_contains("avx2") + .stderr_contains("pclmul"); + } +} diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 27f822845..c6f0d1c77 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -69,7 +69,7 @@ static TEST_NONEXISTENT_FILE: &str = "nonexistent_file.txt"; use uutests::util::compare_xattrs; /// Assert that mode, ownership, and permissions of two metadata objects match. -#[cfg(all(not(windows), not(target_os = "freebsd")))] +#[cfg(all(not(windows), not(target_os = "freebsd"), not(target_os = "openbsd")))] macro_rules! assert_metadata_eq { ($m1:expr, $m2:expr) => {{ assert_eq!($m1.mode(), $m2.mode(), "mode is different"); @@ -142,6 +142,41 @@ fn test_cp_duplicate_folder() { assert!(at.dir_exists(format!("{TEST_COPY_TO_FOLDER}/{TEST_COPY_FROM_FOLDER}").as_str())); } +#[test] +fn test_cp_duplicate_directories_merge() { + let (at, mut ucmd) = at_and_ucmd!(); + + // Source directory 1 + at.mkdir_all("src_dir/subdir"); + at.write("src_dir/subdir/file1.txt", "content1"); + at.write("src_dir/subdir/file2.txt", "content2"); + + // Source directory 2 + at.mkdir_all("src_dir2/subdir"); + at.write("src_dir2/subdir/file1.txt", "content3"); + + // Destination + at.mkdir("dest"); + + // Perform merge copy + ucmd.arg("-r") + .arg("src_dir/subdir") + .arg("src_dir2/subdir") + .arg("dest") + .succeeds(); + + // Verify directory exists + assert!(at.dir_exists("dest/subdir")); + + // file1.txt should be overwritten by src_dir2/subdir/file1.txt + assert!(at.file_exists("dest/subdir/file1.txt")); + assert_eq!(at.read("dest/subdir/file1.txt"), "content3"); + + // file2.txt should remain from first copy + assert!(at.file_exists("dest/subdir/file2.txt")); + assert_eq!(at.read("dest/subdir/file2.txt"), "content2"); +} + #[test] fn test_cp_duplicate_files_normalized_path() { let (at, mut ucmd) = at_and_ucmd!(); @@ -1553,7 +1588,7 @@ fn test_cp_parents_with_permissions_copy_file() { .arg(dir) .succeeds(); - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] { let p1_metadata = at.metadata("p1"); let p2_metadata = at.metadata("p1/p2"); @@ -1596,7 +1631,7 @@ fn test_cp_parents_with_permissions_copy_dir() { .arg(dir1) .succeeds(); - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] { let p1_metadata = at.metadata("p1"); let p2_metadata = at.metadata("p1/p2"); @@ -1641,7 +1676,7 @@ fn test_cp_preserve_no_args() { .arg("--preserve") .succeeds(); - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] { // Assert that the mode, ownership, and timestamps are preserved // NOTICE: the ownership is not modified on the src file, because that requires root permissions @@ -1669,7 +1704,7 @@ fn test_cp_preserve_no_args_before_opts() { .arg(dst_file) .succeeds(); - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] { // Assert that the mode, ownership, and timestamps are preserved // NOTICE: the ownership is not modified on the src file, because that requires root permissions @@ -1695,7 +1730,7 @@ fn test_cp_preserve_all() { // Copy ucmd.arg(src_file).arg(dst_file).arg(argument).succeeds(); - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] { // Assert that the mode, ownership, and timestamps are preserved // NOTICE: the ownership is not modified on the src file, because that requires root permissions @@ -3028,7 +3063,7 @@ fn test_copy_through_dangling_symlink_no_dereference_permissions() { assert!(at.symlink_exists("d2"), "symlink wasn't created"); // `-p` means `--preserve=mode,ownership,timestamps` - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] { let metadata1 = at.symlink_metadata("dangle"); let metadata2 = at.symlink_metadata("d2"); @@ -3749,7 +3784,7 @@ fn test_preserve_hardlink_attributes_in_directory() { // // A hard link should have the same inode as the target file. at.file_exists("dest/src/link"); - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] assert_eq!( at.metadata("dest/src/f").ino(), at.metadata("dest/src/link").ino() @@ -3765,7 +3800,7 @@ fn test_hard_link_file() { ucmd.args(&["-f", "--link", "src", "dest"]) .succeeds() .no_output(); - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] assert_eq!(at.metadata("src").ino(), at.metadata("dest").ino()); } @@ -4069,7 +4104,7 @@ fn test_cp_dest_no_permissions() { } #[test] -#[cfg(all(unix, not(target_os = "freebsd")))] +#[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] fn test_cp_attributes_only() { let (at, mut ucmd) = at_and_ucmd!(); let a = "file_a"; @@ -6537,7 +6572,7 @@ fn test_cp_preserve_selinux() { selinux_perm_dest ); - #[cfg(all(unix, not(target_os = "freebsd")))] + #[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] { // Assert that the mode, ownership, and timestamps are preserved // NOTICE: the ownership is not modified on the src file, because that requires root permissions diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 224626b21..bc97cb28f 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -5,17 +5,16 @@ // spell-checker:ignore (paths) atim sublink subwords azerty azeaze xcwww azeaz amaz azea qzerty tazerty tsublink testfile1 testfile2 filelist fpath testdir testfile // spell-checker:ignore selfref ELOOP smallfile + #[cfg(not(windows))] use regex::Regex; -use uutests::at_and_ucmd; -use uutests::new_ucmd; #[cfg(not(target_os = "windows"))] use uutests::unwrap_or_return; use uutests::util::TestScenario; #[cfg(not(target_os = "windows"))] use uutests::util::expected_result; -use uutests::util_name; +use uutests::{at_and_ucmd, new_ucmd, util_name}; #[cfg(not(target_os = "openbsd"))] const SUB_DIR: &str = "subdir/deeper"; @@ -1399,6 +1398,17 @@ fn test_du_files0_from_stdin_with_invalid_zero_length_file_names() { .stderr_contains("-:2: invalid zero-length file name"); } +#[test] +fn test_du_files0_from_stdin_with_stdin_as_input() { + new_ucmd!() + .arg("--files0-from=-") + .pipe_in("-") + .fails_with_code(1) + .stderr_is( + "du: when reading file names from standard input, no file name of '-' allowed\n", + ); +} + #[test] fn test_du_files0_from_dir() { let ts = TestScenario::new(util_name!()); diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index db8e0e793..68e7e03b5 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (words) bamf chdir rlimit prlimit COMSPEC cout cerr FFFD winsize xpixel ypixel +// spell-checker:ignore (words) bamf chdir rlimit prlimit COMSPEC cout cerr FFFD winsize xpixel ypixel Secho #![allow(clippy::missing_errors_doc)] #[cfg(unix)] @@ -1801,3 +1801,64 @@ fn test_shebang_error() { .fails() .stderr_contains("use -[v]S to pass options in shebang lines"); } + +#[test] +#[cfg(not(target_os = "windows"))] +fn test_braced_variable_with_default_value() { + new_ucmd!() + .arg("-Secho ${UNSET_VAR_UNLIKELY_12345:fallback}") + .succeeds() + .stdout_is("fallback\n"); +} + +#[test] +#[cfg(not(target_os = "windows"))] +fn test_braced_variable_with_default_when_set() { + new_ucmd!() + .env("TEST_VAR_12345", "actual") + .arg("-Secho ${TEST_VAR_12345:fallback}") + .succeeds() + .stdout_is("actual\n"); +} + +#[test] +#[cfg(not(target_os = "windows"))] +fn test_simple_braced_variable() { + new_ucmd!() + .env("TEST_VAR_12345", "value") + .arg("-Secho ${TEST_VAR_12345}") + .succeeds() + .stdout_is("value\n"); +} + +#[test] +fn test_braced_variable_error_missing_closing_brace() { + new_ucmd!() + .arg("-Secho ${FOO") + .fails_with_code(125) + .stderr_contains("Missing closing brace"); +} + +#[test] +fn test_braced_variable_error_missing_closing_brace_after_default() { + new_ucmd!() + .arg("-Secho ${FOO:-value") + .fails_with_code(125) + .stderr_contains("Missing closing brace after default value"); +} + +#[test] +fn test_braced_variable_error_starts_with_digit() { + new_ucmd!() + .arg("-Secho ${1FOO}") + .fails_with_code(125) + .stderr_contains("Unexpected character: '1'"); +} + +#[test] +fn test_braced_variable_error_unexpected_character() { + new_ucmd!() + .arg("-Secho ${FOO?}") + .fails_with_code(125) + .stderr_contains("Unexpected character: '?'"); +} diff --git a/tests/by-util/test_fold.rs b/tests/by-util/test_fold.rs index 4a2d381fa..9497044c9 100644 --- a/tests/by-util/test_fold.rs +++ b/tests/by-util/test_fold.rs @@ -2,6 +2,8 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore fullwidth + use uutests::new_ucmd; #[test] @@ -41,6 +43,24 @@ fn test_default_wrap_with_newlines() { .stdout_is_fixture("lorem_ipsum_new_line_80_column.expected"); } +#[test] +fn test_wide_characters_in_column_mode() { + new_ucmd!() + .args(&["-w", "5"]) + .pipe_in("\u{B250}\u{B250}\u{B250}\n") + .succeeds() + .stdout_is("\u{B250}\u{B250}\n\u{B250}\n"); +} + +#[test] +fn test_wide_characters_with_characters_option() { + new_ucmd!() + .args(&["--characters", "-w", "5"]) + .pipe_in("\u{B250}\u{B250}\u{B250}\n") + .succeeds() + .stdout_is("\u{B250}\u{B250}\u{B250}\n"); +} + #[test] fn test_should_preserve_empty_line_without_final_newline() { new_ucmd!() @@ -579,3 +599,36 @@ fn test_all_tab_advances_at_non_utf8_character() { .succeeds() .stdout_is_fixture_bytes("non_utf8_tab_stops_w16.expected"); } + +#[test] +fn test_combining_characters_nfc() { + // e acute NFC form (single character) + let e_acute_nfc = "\u{00E9}"; // é as single character + new_ucmd!() + .arg("-w2") + .pipe_in(format!("{e_acute_nfc}{e_acute_nfc}{e_acute_nfc}")) + .succeeds() + .stdout_is(format!("{e_acute_nfc}{e_acute_nfc}\n{e_acute_nfc}")); +} + +#[test] +fn test_combining_characters_nfd() { + // e acute NFD form (base + combining acute) + let e_acute_nfd = "e\u{0301}"; // e + combining acute accent + new_ucmd!() + .arg("-w2") + .pipe_in(format!("{e_acute_nfd}{e_acute_nfd}{e_acute_nfd}")) + .succeeds() + .stdout_is(format!("{e_acute_nfd}{e_acute_nfd}\n{e_acute_nfd}")); +} + +#[test] +fn test_fullwidth_characters() { + // e fullwidth (takes 2 columns) + let e_fullwidth = "\u{FF45}"; // e + new_ucmd!() + .arg("-w2") + .pipe_in(format!("{e_fullwidth}{e_fullwidth}")) + .succeeds() + .stdout_is(format!("{e_fullwidth}\n{e_fullwidth}")); +} diff --git a/tests/by-util/test_hashsum.rs b/tests/by-util/test_hashsum.rs index b2eb96879..beaf994e1 100644 --- a/tests/by-util/test_hashsum.rs +++ b/tests/by-util/test_hashsum.rs @@ -107,17 +107,12 @@ macro_rules! test_digest { at.write("a", "file1\n"); at.write("c", "file3\n"); - #[cfg(unix)] - let file_not_found_str = "No such file or directory"; - #[cfg(not(unix))] - let file_not_found_str = "The system cannot find the file specified"; - ts.ucmd() .args(&[DIGEST_ARG, BITS_ARG, "a", "b", "c"]) .fails() .stdout_contains("a\n") .stdout_contains("c\n") - .stderr_contains(format!("b: {file_not_found_str}")); + .stderr_contains("b: No such file or directory"); } } )*) @@ -1097,11 +1092,11 @@ fn test_sha256_stdin_binary() { ); } +// This test is currently disabled on windows #[test] +#[cfg_attr(windows, ignore = "Discussion is in #9168")] fn test_check_sha256_binary() { - let ts = TestScenario::new(util_name!()); - - ts.ucmd() + new_ucmd!() .args(&[ "--sha256", "--bits=256", diff --git a/tests/by-util/test_hostname.rs b/tests/by-util/test_hostname.rs index e58c236a5..e4f30c3e5 100644 --- a/tests/by-util/test_hostname.rs +++ b/tests/by-util/test_hostname.rs @@ -15,7 +15,7 @@ fn test_hostname() { } // FixME: fails for "MacOS" => "failed to lookup address information" -#[cfg(not(target_os = "macos"))] +#[cfg(not(any(target_os = "macos")))] #[test] fn test_hostname_ip() { let result = new_ucmd!().arg("-i").succeeds(); diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index f4c7b2dc1..2a2e7d670 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -243,6 +243,75 @@ fn test_install_mode_symbolic() { assert_eq!(0o100_003_u32, PermissionsExt::mode(&permissions)); } +#[test] +fn test_install_mode_comma_separated() { + let (at, mut ucmd) = at_and_ucmd!(); + let dir = "target_dir"; + let file = "source_file"; + // Test comma-separated mode like chmod: ug+rwX,o+rX + let mode_arg = "--mode=ug+rwX,o+rX"; + + at.touch(file); + at.mkdir(dir); + ucmd.arg(file).arg(dir).arg(mode_arg).succeeds().no_stderr(); + + let dest_file = &format!("{dir}/{file}"); + assert!(at.file_exists(file)); + assert!(at.file_exists(dest_file)); + let permissions = at.metadata(dest_file).permissions(); + // ug+rwX: For files, X only adds execute if file already has execute (it doesn't here, starting at 0) + // So this adds rw to user and group = 0o660 + // o+rX: For files, X doesn't add execute, so this adds r to others = 0o004 + // Total: 0o664 for file (0o100_664) + assert_eq!(0o100_664_u32, PermissionsExt::mode(&permissions)); +} + +#[test] +fn test_install_mode_comma_separated_directory() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let dir = "test_dir"; + // Test comma-separated mode for directory creation: ug+rwX,o+rX + let mode_arg = "--mode=ug+rwX,o+rX"; + + scene + .ucmd() + .arg("-d") + .arg(dir) + .arg(mode_arg) + .succeeds() + .no_stderr(); + + assert!(at.dir_exists(dir)); + let permissions = at.metadata(dir).permissions(); + // ug+rwX sets user and group to rwx (0o770), o+rX sets others to r-x (0o005) + // Total: 0o775 for directory (0o040_775) + assert_eq!(0o040_775_u32, PermissionsExt::mode(&permissions)); +} + +#[test] +fn test_install_mode_symbolic_ignore_umask() { + let (at, mut ucmd) = at_and_ucmd!(); + let dir = "target_dir"; + let file = "source_file"; + let mode_arg = "--mode=+w"; + + at.touch(file); + at.mkdir(dir); + ucmd.arg(file) + .arg(dir) + .arg(mode_arg) + .umask(0o022) + .succeeds() + .no_stderr(); + + let dest_file = &format!("{dir}/{file}"); + assert!(at.file_exists(file)); + assert!(at.file_exists(dest_file)); + let permissions = at.metadata(dest_file).permissions(); + assert_eq!(0o100_222_u32, PermissionsExt::mode(&permissions)); +} + #[test] fn test_install_mode_failing() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index d5a7bbfbb..bc103a629 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -934,3 +934,17 @@ fn test_ln_non_utf8_paths() { let symlink_path = at.plus(symlink_name); assert!(symlink_path.is_symlink()); } + +#[test] +fn test_ln_hard_link_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("dir"); + + scene + .ucmd() + .args(&["dir", "dir_link"]) + .fails() + .stderr_contains("hard link not allowed for directory"); +} diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index ef7591b8a..38729d306 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -6663,3 +6663,18 @@ fn test_f_with_long_format() { // Long format should still work (contains permissions, etc.) assert!(result.contains("-rw")); } + +#[test] +#[cfg(target_os = "linux")] +fn test_ls_proc_self_fd_no_errors() { + // Regression test: ReadDir must stay alive until metadata() is called + // to prevent "cannot access '/proc/self/fd/3'" errors. + let scene = TestScenario::new(util_name!()); + + scene + .ucmd() + .arg("-l") + .arg("/proc/self/fd") + .succeeds() + .stderr_does_not_contain("cannot access"); +} diff --git a/tests/by-util/test_mkfifo.rs b/tests/by-util/test_mkfifo.rs index 707adf71c..ac0b78b3a 100644 --- a/tests/by-util/test_mkfifo.rs +++ b/tests/by-util/test_mkfifo.rs @@ -99,6 +99,8 @@ fn test_create_fifo_with_mode_and_umask() { test_fifo_creation("u-r,g-w,o+x", 0o022, "p-w-r--rwx"); // spell-checker:disable-line test_fifo_creation("a=rwx,o-w", 0o022, "prwxrwxr-x"); // spell-checker:disable-line test_fifo_creation("=rwx,o-w", 0o022, "prwxr-xr-x"); // spell-checker:disable-line + test_fifo_creation("ug+rw,o+r", 0o022, "prw-rw-rw-"); // spell-checker:disable-line + test_fifo_creation("u=rwx,g=rx,o=", 0o022, "prwxr-x---"); // spell-checker:disable-line } #[test] @@ -124,6 +126,32 @@ fn test_create_fifo_with_umask() { test_fifo_creation(0o777, "p---------"); // spell-checker:disable-line } +#[test] +fn test_create_fifo_permission_denied() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let no_exec_dir = "owner_no_exec_dir"; + let named_pipe = "owner_no_exec_dir/mkfifo_err"; + + at.mkdir(no_exec_dir); + at.set_mode(no_exec_dir, 0o644); + + let err_msg = format!( + "mkfifo: cannot create fifo '{named_pipe}': File exists +mkfifo: cannot set permissions on '{named_pipe}': Permission denied (os error 13) +" + ); + + scene + .ucmd() + .arg(named_pipe) + .arg("-m") + .arg("666") + .fails() + .stderr_is(err_msg.as_str()); +} + #[test] #[cfg(feature = "feat_selinux")] fn test_mkfifo_selinux() { diff --git a/tests/by-util/test_mknod.rs b/tests/by-util/test_mknod.rs index 34136b828..5d2b08aec 100644 --- a/tests/by-util/test_mknod.rs +++ b/tests/by-util/test_mknod.rs @@ -154,6 +154,22 @@ fn test_mknod_mode_permissions() { } } +#[test] +fn test_mknod_mode_comma_separated() { + let ts = TestScenario::new(util_name!()); + ts.ucmd() + .arg("-m") + .arg("u=rwx,g=rx,o=") + .arg("test_file") + .arg("p") + .succeeds(); + assert!(ts.fixtures.is_fifo("test_file")); + assert_eq!( + ts.fixtures.metadata("test_file").permissions().mode() & 0o777, + 0o750 + ); +} + #[test] #[cfg(feature = "feat_selinux")] fn test_mknod_selinux() { diff --git a/tests/by-util/test_more.rs b/tests/by-util/test_more.rs index a46648a8b..2bf130a18 100644 --- a/tests/by-util/test_more.rs +++ b/tests/by-util/test_more.rs @@ -3,144 +3,253 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use std::io::IsTerminal; - +#[cfg(unix)] +use nix::unistd::{read, write}; +#[cfg(unix)] +use std::fs::File; +#[cfg(unix)] +use std::fs::{Permissions, set_permissions}; +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStrExt; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +#[cfg(unix)] +use uutests::util::pty_path; +#[cfg(unix)] use uutests::{at_and_ucmd, new_ucmd}; +#[cfg(unix)] +fn run_more_with_pty( + args: &[&str], + file: &str, + content: &str, +) -> (uutests::util::UChild, std::os::fd::OwnedFd, String) { + let (path, controller, _replica) = pty_path(); + let (at, mut ucmd) = at_and_ucmd!(); + at.write(file, content); + + let mut child = ucmd + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .args(args) + .arg(file) + .run_no_wait(); + + child.delay(100); + let mut output = vec![0u8; 1024]; + let n = read(&controller, &mut output).unwrap(); + let output_str = String::from_utf8_lossy(&output[..n]).to_string(); + + (child, controller, output_str) +} + +#[cfg(unix)] +fn quit_more(controller: &std::os::fd::OwnedFd, mut child: uutests::util::UChild) { + write(controller, b"q").unwrap(); + child.delay(50); +} + #[cfg(unix)] #[test] fn test_no_arg() { - if std::io::stdout().is_terminal() { - new_ucmd!() - .terminal_simulation(true) - .fails() - .stderr_contains("more: bad usage"); - } + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .fails() + .stderr_contains("more: bad usage"); } #[test] +#[cfg(unix)] fn test_valid_arg() { - if std::io::stdout().is_terminal() { - let args_list: Vec<&[&str]> = vec![ - &["-c"], - &["--clean-print"], - &["-p"], - &["--print-over"], - &["-s"], - &["--squeeze"], - &["-u"], - &["--plain"], - &["-n", "10"], - &["--lines", "0"], - &["--number", "0"], - &["-F", "10"], - &["--from-line", "0"], - &["-P", "something"], - &["--pattern", "-1"], - ]; - for args in args_list { - test_alive(args); - } + let args_list: Vec<&[&str]> = vec![ + &["-c"], + &["--clean-print"], + &["-p"], + &["--print-over"], + &["-s"], + &["--squeeze"], + &["-u"], + &["--plain"], + &["-n", "10"], + &["--lines", "0"], + &["--number", "0"], + &["-F", "10"], + &["--from-line", "0"], + &["-P", "something"], + &["--pattern", "-1"], + ]; + for args in args_list { + test_alive(args); } } +#[cfg(unix)] fn test_alive(args: &[&str]) { let (at, mut ucmd) = at_and_ucmd!(); + let (path, controller, _replica) = pty_path(); let content = "test content"; let file = "test_file"; at.write(file, content); - let mut cmd = ucmd.args(args).arg(file).run_no_wait(); + let mut child = ucmd + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .args(args) + .arg(file) + .run_no_wait(); // wait for more to start and display the file - while cmd.is_alive() && !cmd.stdout_all().contains(content) { - cmd.delay(50); - } + child.delay(100); - assert!(cmd.is_alive(), "Command should still be alive"); + assert!(child.is_alive(), "Command should still be alive"); // cleanup - cmd.kill(); + write(&controller, b"q").unwrap(); + child.delay(50); } #[test] +#[cfg(unix)] fn test_invalid_arg() { - if std::io::stdout().is_terminal() { - new_ucmd!().arg("--invalid").fails(); + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg("--invalid") + .fails(); - new_ucmd!().arg("--lines").arg("-10").fails(); - new_ucmd!().arg("--number").arg("-10").fails(); + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg("--lines") + .arg("-10") + .fails(); - new_ucmd!().arg("--from-line").arg("-10").fails(); - } + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg("--from-line") + .arg("-10") + .fails(); } #[test] +#[cfg(unix)] fn test_file_arg() { - // Run the test only if there's a valid terminal, else do nothing - // Maybe we could capture the error, i.e. "Device not found" in that case - // but I am leaving this for later - if std::io::stdout().is_terminal() { - // Directory as argument - new_ucmd!() - .arg(".") - .succeeds() - .stderr_contains("'.' is a directory."); + // Directory as argument + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg(".") + .succeeds() + .stderr_contains("'.' is a directory."); - // Single argument errors - let (at, mut ucmd) = at_and_ucmd!(); - at.mkdir_all("folder"); - ucmd.arg("folder") - .succeeds() - .stderr_contains("is a directory"); + // Single argument errors + let (path, _controller, _replica) = pty_path(); + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir_all("folder"); + ucmd.set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg("folder") + .succeeds() + .stderr_contains("is a directory"); - new_ucmd!() - .arg("nonexistent_file") - .succeeds() - .stderr_contains("No such file or directory"); + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg("nonexistent_file") + .succeeds() + .stderr_contains("No such file or directory"); - // Multiple nonexistent files - new_ucmd!() - .arg("file2") - .arg("file3") - .succeeds() - .stderr_contains("file2") - .stderr_contains("file3"); - } + // Multiple nonexistent files + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg("file2") + .arg("file3") + .succeeds() + .stderr_contains("file2") + .stderr_contains("file3"); } #[test] -#[cfg(target_family = "unix")] +#[cfg(unix)] fn test_invalid_file_perms() { - if std::io::stdout().is_terminal() { - use std::fs::{Permissions, set_permissions}; - use std::os::unix::fs::PermissionsExt; - - let (at, mut ucmd) = at_and_ucmd!(); - let permissions = Permissions::from_mode(0o244); - at.make_file("invalid-perms.txt"); - set_permissions(at.plus("invalid-perms.txt"), permissions).unwrap(); - ucmd.arg("invalid-perms.txt") - .succeeds() - .stderr_contains("permission denied"); - } + let (path, _controller, _replica) = pty_path(); + let (at, mut ucmd) = at_and_ucmd!(); + let permissions = Permissions::from_mode(0o244); + at.make_file("invalid-perms.txt"); + set_permissions(at.plus("invalid-perms.txt"), permissions).unwrap(); + ucmd.set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg("invalid-perms.txt") + .succeeds() + .stderr_contains("permission denied"); } #[test] #[cfg(target_os = "linux")] fn test_more_non_utf8_paths() { - use std::os::unix::ffi::OsStrExt; - if std::io::stdout().is_terminal() { - let (at, mut ucmd) = at_and_ucmd!(); - let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); - // Create test file with normal name first - at.write( - &file_name.to_string_lossy(), - "test content for non-UTF-8 file", - ); + let (path, _controller, _replica) = pty_path(); + let (at, mut ucmd) = at_and_ucmd!(); + let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); + // Create test file with normal name first + at.write( + &file_name.to_string_lossy(), + "test content for non-UTF-8 file", + ); - // Test that more can handle non-UTF-8 filenames without crashing - ucmd.arg(file_name).succeeds(); - } + // Test that more can handle non-UTF-8 filenames without crashing + ucmd.set_stdin(File::open(&path).unwrap()) + .set_stdout(File::create(&path).unwrap()) + .arg(file_name) + .succeeds(); +} + +#[test] +#[cfg(unix)] +fn test_basic_display() { + let (child, controller, output) = run_more_with_pty(&[], "test.txt", "line1\nline2\nline3\n"); + assert!(output.contains("line1")); + quit_more(&controller, child); +} + +#[test] +#[cfg(unix)] +fn test_squeeze_blank_lines() { + let (child, controller, output) = + run_more_with_pty(&["-s"], "test.txt", "line1\n\n\n\nline2\n"); + assert!(output.contains("line1")); + quit_more(&controller, child); +} + +#[test] +#[cfg(unix)] +fn test_pattern_search() { + let (child, controller, output) = run_more_with_pty( + &["-P", "target"], + "test.txt", + "foo\nbar\nbaz\ntarget\nend\n", + ); + assert!(output.contains("target")); + assert!(!output.contains("foo")); + quit_more(&controller, child); +} + +#[test] +#[cfg(unix)] +fn test_from_line_option() { + let (child, controller, output) = + run_more_with_pty(&["-F", "2"], "test.txt", "line1\nline2\nline3\nline4\n"); + assert!(output.contains("line2")); + assert!(!output.contains("line1")); + quit_more(&controller, child); } diff --git a/tests/by-util/test_nl.rs b/tests/by-util/test_nl.rs index 953d7bcb3..ab430b20b 100644 --- a/tests/by-util/test_nl.rs +++ b/tests/by-util/test_nl.rs @@ -800,3 +800,104 @@ fn test_file_with_non_utf8_content() { String::from_utf8_lossy(invalid_utf8) )); } + +// Regression tests for issue #9132: repeated flags should use last value +#[test] +fn test_repeated_body_numbering_flag() { + // -ba -bt should use -bt (t=nonempty) + new_ucmd!() + .args(&["-ba", "-bt"]) + .pipe_in("a\n\nb\n\nc") + .succeeds() + .stdout_is(" 1\ta\n \n 2\tb\n \n 3\tc\n"); +} + +#[test] +fn test_repeated_header_numbering_flag() { + // -ha -ht should use -ht (number only nonempty lines in header) + new_ucmd!() + .args(&["-ha", "-ht"]) + .pipe_in("\\:\\:\\:\na\nb\n\nc") + .succeeds() + .stdout_is("\n 1\ta\n 2\tb\n \n 3\tc\n"); +} + +#[test] +fn test_repeated_footer_numbering_flag() { + // -fa -ft should use -ft (t=nonempty in footer) + new_ucmd!() + .args(&["-fa", "-ft"]) + .pipe_in("\\:\na\nb\n\nc") + .succeeds() + .stdout_is("\n 1\ta\n 2\tb\n \n 3\tc\n"); +} + +#[test] +fn test_repeated_number_format_flag() { + // -n ln -n rn should use -n rn (rn=right aligned) + new_ucmd!() + .args(&["-n", "ln", "-n", "rn"]) + .pipe_in("a\nb\nc") + .succeeds() + .stdout_is(" 1\ta\n 2\tb\n 3\tc\n"); +} + +#[test] +fn test_repeated_number_separator_flag() { + // -s ':' -s '|' should use -s '|' + new_ucmd!() + .args(&["-s", ":", "-s", "|"]) + .pipe_in("a\nb\nc") + .succeeds() + .stdout_is(" 1|a\n 2|b\n 3|c\n"); +} + +#[test] +fn test_repeated_number_width_flag() { + // -w 3 -w 8 should use -w 8 + new_ucmd!() + .args(&["-w", "3", "-w", "8"]) + .pipe_in("a\nb\nc") + .succeeds() + .stdout_is(" 1\ta\n 2\tb\n 3\tc\n"); +} + +#[test] +fn test_repeated_line_increment_flag() { + // -i 1 -i 5 should use -i 5 + new_ucmd!() + .args(&["-i", "1", "-i", "5"]) + .pipe_in("a\nb\nc") + .succeeds() + .stdout_is(" 1\ta\n 6\tb\n 11\tc\n"); +} + +#[test] +fn test_repeated_starting_line_number_flag() { + // -v 1 -v 10 should use -v 10 + new_ucmd!() + .args(&["-v", "1", "-v", "10"]) + .pipe_in("a\nb\nc") + .succeeds() + .stdout_is(" 10\ta\n 11\tb\n 12\tc\n"); +} + +#[test] +fn test_repeated_join_blank_lines_flag() { + // -l 1 -l 2 should use -l 2 + new_ucmd!() + .args(&["-l", "1", "-l", "2", "-ba"]) + .pipe_in("a\n\n\nb") + .succeeds() + .stdout_is(" 1\ta\n \n 2\t\n 3\tb\n"); +} + +#[test] +fn test_repeated_section_delimiter_flag() { + // -d ':' -d '|' should use -d '|' + new_ucmd!() + .args(&["-d", ":", "-d", "|"]) + .pipe_in("|:|:|:\na\nb\nc") + .succeeds() + .stdout_is("\n a\n b\n c\n"); +} diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index 673073694..d947833f7 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.rs @@ -241,8 +241,7 @@ fn test_should_report_invalid_empty_number_on_blank_stdin() { #[test] fn test_suffixes() { - // TODO add support for ronna (R) and quetta (Q) - let valid_suffixes = ['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' /*'R' , 'Q'*/]; + let valid_suffixes = ['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q', 'k']; for c in ('A'..='Z').chain('a'..='z') { let args = ["--from=si", "--to=si", &format!("1{c}")]; @@ -264,12 +263,12 @@ fn test_suffixes() { #[test] fn test_should_report_invalid_suffix_on_nan() { - // GNU numfmt reports this one as “invalid number” + // GNU numfmt reports this one as "invalid number" new_ucmd!() .args(&["--from=auto"]) .pipe_in("NaN") .fails() - .stderr_is("numfmt: invalid suffix in input: 'NaN'\n"); + .stderr_is("numfmt: invalid number: 'NaN'\n"); } #[test] @@ -700,7 +699,7 @@ fn test_invalid_stdin_number_with_warn_returns_status_0() { .pipe_in("4Q") .succeeds() .stdout_is("4Q\n") - .stderr_is("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_is("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] @@ -718,7 +717,7 @@ fn test_invalid_stdin_number_with_abort_returns_status_2() { .args(&["--invalid=abort"]) .pipe_in("4Q") .fails_with_code(2) - .stderr_only("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_only("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] @@ -728,7 +727,7 @@ fn test_invalid_stdin_number_with_fail_returns_status_2() { .pipe_in("4Q") .fails_with_code(2) .stdout_is("4Q\n") - .stderr_is("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_is("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] @@ -737,7 +736,7 @@ fn test_invalid_arg_number_with_warn_returns_status_0() { .args(&["--invalid=warn", "4Q"]) .succeeds() .stdout_is("4Q\n") - .stderr_is("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_is("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] @@ -753,7 +752,7 @@ fn test_invalid_arg_number_with_abort_returns_status_2() { new_ucmd!() .args(&["--invalid=abort", "4Q"]) .fails_with_code(2) - .stderr_only("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_only("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] @@ -762,7 +761,7 @@ fn test_invalid_arg_number_with_fail_returns_status_2() { .args(&["--invalid=fail", "4Q"]) .fails_with_code(2) .stdout_is("4Q\n") - .stderr_is("numfmt: invalid suffix in input: '4Q'\n"); + .stderr_is("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n"); } #[test] diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index d5b747948..fea019e3a 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -3,10 +3,12 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore abcdefghijklmnopqrstuvwxyz Anone fdbb littl +// spell-checker:ignore abcdefghijklmnopqrstuvwxyz Anone fdbb littl bfloat #[cfg(unix)] use std::io::Read; +#[cfg(target_os = "linux")] +use std::path::Path; use unindent::unindent; use uutests::util::TestScenario; @@ -19,6 +21,27 @@ static ALPHA_OUT: &str = " 0000033 "; +fn erange_message() -> String { + let err = std::io::Error::from_raw_os_error(libc::ERANGE); + let msg = err.to_string(); + msg.split(" (os error").next().unwrap_or(&msg).to_string() +} + +fn run_skip_across_inputs(files: &[(&str, &str)], skip: u64, expected: &str) { + let (at, mut ucmd) = at_and_ucmd!(); + for (name, contents) in files { + at.write(name, contents); + } + + ucmd.arg("-c").arg("-j").arg(skip.to_string()).arg("-An"); + + for (name, _) in files { + ucmd.arg(name); + } + + ucmd.succeeds().stdout_only(expected); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); @@ -174,6 +197,32 @@ fn test_hex32() { .stdout_only(expected_output); } +// Regression: 16-bit IEEE half should print with canonical precision (no spurious digits) +#[test] +fn test_float16_compact() { + let input: [u8; 4] = [0x3c, 0x00, 0x3c, 0x00]; // two times 1.0 in big-endian half + new_ucmd!() + .arg("--endian=big") + .arg("-An") + .arg("-tfH") + .run_piped_stdin(&input[..]) + .success() + .stdout_only(" 1 1\n"); +} + +// Regression: 16-bit bfloat should print with canonical precision (no spurious digits) +#[test] +fn test_bfloat16_compact() { + let input: [u8; 4] = [0x3f, 0x80, 0x3f, 0x80]; // two times 1.0 in big-endian bfloat16 + new_ucmd!() + .arg("--endian=big") + .arg("-An") + .arg("-tfB") + .run_piped_stdin(&input[..]) + .success() + .stdout_only(" 1 1\n"); +} + #[test] fn test_f16() { let input: [u8; 14] = [ @@ -187,7 +236,7 @@ fn test_f16() { ]; // 0x8400 -6.104e-5 let expected_output = unindent( " - 0000000 1.0000000 0 -0 inf + 0000000 1 0 -0 inf 0000010 -inf NaN -6.1035156e-5 0000016 ", @@ -214,7 +263,7 @@ fn test_fh() { ]; // 0x8400 -6.1035156e-5 let expected_output = unindent( " - 0000000 1.0000000 0 -0 inf + 0000000 1 0 -0 inf 0000010 -inf NaN -6.1035156e-5 0000016 ", @@ -241,7 +290,7 @@ fn test_fb() { ]; // -6.1035156e-5 let expected_output = unindent( " - 0000000 1.0000000 0 -0 inf + 0000000 1 0 -0 inf 0000010 -inf NaN -6.1035156e-5 0000016 ", @@ -368,22 +417,29 @@ fn test_invalid_width() { #[test] fn test_zero_width() { - let input: [u8; 4] = [0x00, 0x00, 0x00, 0x00]; - let expected_output = unindent( - " - 0000000 000000 - 0000002 000000 - 0000004 - ", - ); - new_ucmd!() .arg("-w0") - .arg("-v") - .run_piped_stdin(&input[..]) - .success() - .stderr_is_bytes("od: warning: invalid width 0; using 2 instead\n".as_bytes()) - .stdout_is(expected_output); + .arg("-An") + .fails_with_code(1) + .stderr_only("od: invalid -w argument '0'\n"); +} + +#[test] +fn test_negative_width_argument() { + new_ucmd!() + .arg("-w-1") + .arg("-An") + .fails_with_code(1) + .stderr_only("od: invalid -w argument '-1'\n"); +} + +#[test] +fn test_non_numeric_width_argument() { + new_ucmd!() + .arg("-ww") + .arg("-An") + .fails_with_code(1) + .stderr_only("od: invalid -w argument 'w'\n"); } #[test] @@ -402,6 +458,42 @@ fn test_width_without_value() { .stdout_only(expected_output); } +#[test] +fn test_very_wide_ascii_output() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("data-a", "x"); + ucmd.arg("-a") + .arg("-w65537") + .arg("-An") + .arg("data-a") + .succeeds() + .stdout_only(" x\n"); +} + +#[test] +fn test_very_wide_char_output() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("data-c", "x"); + ucmd.arg("-c") + .arg("-w65537") + .arg("-An") + .arg("data-c") + .succeeds() + .stdout_only(" x\n"); +} + +#[test] +fn test_very_wide_hex_byte_output() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write_bytes("data-x", &[0x42]); + ucmd.arg("-tx1") + .arg("-w65537") + .arg("-An") + .arg("data-x") + .succeeds() + .stdout_only(" 42\n"); +} + #[test] fn test_suppress_duplicates() { let input: [u8; 41] = [ @@ -606,6 +698,53 @@ fn test_invalid_offset() { new_ucmd!().arg("-Ab").fails(); } +#[test] +fn test_invalid_traditional_offsets_are_filenames() { + let cases = [("++0", "++0"), ("+-0", "+-0"), ("+ 0", "'+ 0'")]; + + for (input, display) in cases { + new_ucmd!() + .arg(input) + .fails_with_code(1) + .stderr_only(format!("od: {display}: No such file or directory\n")); + } + + new_ucmd!() + .arg("--") + .arg("-0") + .fails_with_code(1) + .stderr_only("od: -0: No such file or directory\n"); +} + +#[test] +fn test_traditional_offset_overflow_diagnosed() { + let erange = erange_message(); + let long_octal = "7".repeat(255); + let long_decimal = format!("{}.", "9".repeat(254)); + let long_hex = format!("0x{}", "f".repeat(253)); + + new_ucmd!() + .arg("-") + .arg(&long_octal) + .pipe_in(Vec::::new()) + .fails_with_code(1) + .stderr_only(format!("od: {long_octal}: {erange}\n")); + + new_ucmd!() + .arg("-") + .arg(&long_decimal) + .pipe_in(Vec::::new()) + .fails_with_code(1) + .stderr_only(format!("od: {long_decimal}: {erange}\n")); + + new_ucmd!() + .arg("-") + .arg(&long_hex) + .pipe_in(Vec::::new()) + .fails_with_code(1) + .stderr_only(format!("od: {long_hex}: {erange}\n")); +} + #[test] fn test_empty_offset() { new_ucmd!() @@ -670,6 +809,59 @@ fn test_skip_bytes_hex() { )); } +#[test] +fn test_skip_bytes_consumes_single_input() { + run_skip_across_inputs(&[("g", "a")], 1, ""); +} + +#[test] +fn test_skip_bytes_consumes_two_inputs() { + run_skip_across_inputs(&[("g", "a"), ("h", "b")], 2, ""); +} + +#[test] +fn test_skip_bytes_consumes_three_inputs() { + run_skip_across_inputs(&[("g", "a"), ("h", "b"), ("i", "c")], 3, ""); +} + +#[test] +fn test_skip_bytes_prints_after_consuming_multiple_inputs() { + run_skip_across_inputs( + &[("g", "a"), ("h", "b"), ("i", "c"), ("j", "d")], + 3, + " d\n", + ); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_skip_bytes_proc_file_without_seeking() { + let proc_path = Path::new("/proc/version"); + if !proc_path.exists() { + return; + } + + let Ok(contents) = std::fs::read(proc_path) else { + return; + }; + + if contents.is_empty() { + return; + } + + let (at, mut ucmd) = at_and_ucmd!(); + at.write("after", "e"); + + ucmd.arg("-An") + .arg("-c") + .arg("-j") + .arg(contents.len().to_string()) + .arg(proc_path) + .arg("after") + .succeeds() + .stdout_only(" e\n"); +} + #[test] fn test_skip_bytes_error() { let input = "12345"; @@ -778,6 +970,24 @@ fn test_stdin_offset() { )); } +#[test] +fn test_traditional_decimal_dot_offset() { + new_ucmd!() + .arg("+1.") + .pipe_in("a") + .succeeds() + .stdout_only("0000001\n"); +} + +#[test] +fn test_traditional_dot_block_offset() { + new_ucmd!() + .arg("+1.b") + .pipe_in(vec![b'a'; 512]) + .succeeds() + .stdout_only("0001000\n"); +} + #[test] fn test_file_offset() { new_ucmd!() diff --git a/tests/by-util/test_pr.rs b/tests/by-util/test_pr.rs index 0d1c4bc4b..1fa91dab2 100644 --- a/tests/by-util/test_pr.rs +++ b/tests/by-util/test_pr.rs @@ -558,6 +558,49 @@ fn test_value_for_number_lines() { new_ucmd!().args(&["-n", "foo5.txt", "test.log"]).fails(); } +#[test] +fn test_header_formatting_with_custom_date_format() { + // This test verifies that the header is properly formatted with: + // - Date/time on the left + // - Filename centered + // - "Page X" on the right + // This matches GNU pr behavior for the time-style test + + let test_file_path = "test_one_page.log"; + + // Set a specific date format like in the GNU test + let output = new_ucmd!() + .args(&["-D", "+%Y-%m-%d %H:%M:%S %z (%Z)", test_file_path]) + .succeeds() + .stdout_move_str(); + + // Extract the header line (3rd line of output) + let lines: Vec<&str> = output.lines().collect(); + assert!( + lines.len() >= 5, + "Output should have at least 5 lines for header" + ); + + let header_line = lines[2]; + + // The header should be 72 characters wide (default page width) + assert_eq!(header_line.chars().count(), 72); + + // Check that it contains the expected parts + assert!(header_line.contains(test_file_path)); + assert!(header_line.contains("Page 1")); + + // Verify the filename is roughly centered + let filename_pos = header_line.find(test_file_path).unwrap(); + let page_pos = header_line.find("Page 1").unwrap(); + + // Filename should be somewhere in the middle third of the line + assert!(filename_pos > 24 && filename_pos < 48); + + // Page should be right-aligned (near the end) + assert!(page_pos >= 60); +} + #[test] fn test_help() { new_ucmd!().arg("--help").succeeds(); diff --git a/tests/by-util/test_ptx.rs b/tests/by-util/test_ptx.rs index 3ff36a1c6..464dcf6ae 100644 --- a/tests/by-util/test_ptx.rs +++ b/tests/by-util/test_ptx.rs @@ -256,3 +256,11 @@ fn test_utf8() { .succeeds() .stdout_only("\\xx {}{it’s}{disabled}{}{}\n\\xx {}{}{it’s}{ disabled}{}\n"); } + +#[test] +fn test_gnu_mode_dumb_format() { + // Test GNU mode (dumb format) - the default mode without -G flag + new_ucmd!().pipe_in("a b").succeeds().stdout_only( + " a b\n a b\n", + ); +} diff --git a/tests/by-util/test_readlink.rs b/tests/by-util/test_readlink.rs index 848800cc3..e21459526 100644 --- a/tests/by-util/test_readlink.rs +++ b/tests/by-util/test_readlink.rs @@ -2,11 +2,11 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// // spell-checker:ignore regfile -use uutests::new_ucmd; -use uutests::path_concat; + use uutests::util::{TestScenario, get_root_path}; -use uutests::{at_and_ucmd, util_name}; +use uutests::{at_and_ucmd, new_ucmd, path_concat, util_name}; static GIBBERISH: &str = "supercalifragilisticexpialidocious"; @@ -15,6 +15,14 @@ static NOT_A_DIRECTORY: &str = "Not a directory"; #[cfg(windows)] static NOT_A_DIRECTORY: &str = "The directory name is invalid."; +#[test] +fn test_no_args() { + new_ucmd!() + .fails_with_code(1) + .no_stdout() + .stderr_contains("readlink: missing operand"); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); diff --git a/tests/by-util/test_seq.rs b/tests/by-util/test_seq.rs index f82a6228f..d5dd526aa 100644 --- a/tests/by-util/test_seq.rs +++ b/tests/by-util/test_seq.rs @@ -10,6 +10,27 @@ fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); } +#[test] +#[cfg(unix)] +fn test_broken_pipe_still_exits_success() { + use std::process::Stdio; + + let mut child = new_ucmd!() + // Use an infinite sequence so a burst of output happens immediately after spawn. + // With small output the process can finish before stdout is closed and the Broken pipe never occurs. + .args(&["inf"]) + .set_stdout(Stdio::piped()) + .run_no_wait(); + + // Trigger a Broken pipe by writing to a pipe whose reader closed first. + child.close_stdout(); + let result = child.wait().unwrap(); + + result + .code_is(0) + .stderr_contains("write error: Broken pipe"); +} + #[test] fn test_no_args() { new_ucmd!() diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 3a4cc1a86..8bce9d69c 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -1002,32 +1002,31 @@ fn test_compress_merge() { #[test] #[cfg(not(target_os = "android"))] fn test_compress_fail() { + let result = new_ucmd!() + .args(&[ + "ext_sort.txt", + "-n", + "--compress-program", + "nonexistent-program", + "-S", + "10", + ]) + .succeeds(); + #[cfg(not(windows))] - new_ucmd!() - .args(&[ - "ext_sort.txt", - "-n", - "--compress-program", - "nonexistent-program", - "-S", - "10", - ]) - .fails() - .stderr_only("sort: couldn't execute compress program: errno 2\n"); - // With coverage, it fails with a different error: - // "thread 'main' panicked at 'called `Option::unwrap()` on ... - // So, don't check the output + result.stderr_contains( + "sort: could not run compress program 'nonexistent-program': No such file or directory", + ); + #[cfg(windows)] - new_ucmd!() - .args(&[ - "ext_sort.txt", - "-n", - "--compress-program", - "nonexistent-program", - "-S", - "10", - ]) - .fails(); + result.stderr_contains("could not run compress program"); + + // Check that it still produces correct sorted output to stdout + let expected = new_ucmd!() + .args(&["ext_sort.txt", "-n"]) + .succeeds() + .stdout_move_str(); + assert_eq!(result.stdout_str(), expected); } #[test] diff --git a/tests/by-util/test_stdbuf.rs b/tests/by-util/test_stdbuf.rs index 8c3fef587..a19900db3 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -87,6 +87,17 @@ fn test_stdbuf_no_buffer_option_fails() { .stderr_contains("the following required arguments were not provided:"); } +#[cfg(not(target_os = "windows"))] +#[test] +fn test_stdbuf_no_command_fails_with_125() { + // Test that missing command fails with exit code 125 (stdbuf error) + // This verifies proper error handling without unwrap panic + new_ucmd!() + .args(&["-o1"]) + .fails_with_code(125) + .stderr_contains("the following required arguments were not provided:"); +} + // Disabled on x86_64-unknown-linux-musl because the cross-rs Docker image for this target // does not provide musl-compiled system utilities (like tail), leading to dynamic linker errors // when preloading musl-compiled libstdbuf.so into glibc-compiled binaries. Same thing for FreeBSD. diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index 8f4aec5bd..f68de5daf 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -2,9 +2,18 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore parenb parmrk ixany iuclc onlcr ofdel icanon noflsh econl igpar ispeed ospeed +// spell-checker:ignore parenb parmrk ixany iuclc onlcr icanon noflsh econl igpar ispeed ospeed NCCS nonhex gstty -use uutests::new_ucmd; +use uutests::util::{expected_result, pty_path}; +use uutests::{at_and_ts, new_ucmd, unwrap_or_return}; + +/// Normalize stderr by replacing the full binary path with just the utility name +/// This allows comparison between GNU (which shows "stty" or "gstty") and ours (which shows full path) +fn normalize_stderr(stderr: &str) -> String { + // Replace patterns like "Try 'gstty --help'" or "Try '/path/to/stty --help'" with "Try 'stty --help'" + let re = regex::Regex::new(r"Try '[^']*(?:g)?stty --help'").unwrap(); + re.replace_all(stderr, "Try 'stty --help'").to_string() +} #[test] fn test_invalid_arg() { @@ -12,31 +21,41 @@ fn test_invalid_arg() { } #[test] -#[ignore = "Fails because cargo test does not run in a tty"] -fn runs() { - new_ucmd!().succeeds(); +#[cfg(unix)] +fn test_basic() { + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .args(&["--file", &path]) + .succeeds() + .stdout_contains("speed"); } #[test] -#[ignore = "Fails because cargo test does not run in a tty"] -fn print_all() { - let res = new_ucmd!().args(&["--all"]).succeeds(); +#[cfg(unix)] +fn test_all_flag() { + let (path, _controller, _replica) = pty_path(); + let result = new_ucmd!().args(&["--all", "--file", &path]).succeeds(); - // Random selection of flags to check for - for flag in [ - "parenb", "parmrk", "ixany", "onlcr", "ofdel", "icanon", "noflsh", - ] { - res.stdout_contains(flag); + for flag in ["parenb", "parmrk", "ixany", "onlcr", "icanon", "noflsh"] { + result.stdout_contains(flag); } } #[test] -#[ignore = "Fails because cargo test does not run in a tty"] -fn sane_settings() { - new_ucmd!().args(&["intr", "^A"]).succeeds(); - new_ucmd!().succeeds().stdout_contains("intr = ^A"); +#[cfg(unix)] +fn test_sane() { + let (path, _controller, _replica) = pty_path(); + new_ucmd!() - .args(&["sane"]) + .args(&["--file", &path, "intr", "^A"]) + .succeeds(); + new_ucmd!() + .args(&["--file", &path]) + .succeeds() + .stdout_contains("intr = ^A"); + new_ucmd!().args(&["--file", &path, "sane"]).succeeds(); + new_ucmd!() + .args(&["--file", &path]) .succeeds() .stdout_str_check(|s| !s.contains("intr = ^A")); } @@ -183,6 +202,24 @@ fn invalid_baud_setting() { .args(&["ospeed", "995"]) .fails() .stderr_contains("invalid ospeed '995'"); + + for speed in &[ + "9599..", "9600..", "9600.5.", "9600.50.", "9600.0.", "++9600", "0x2580", "96E2", "9600,0", + "9600.0 ", + ] { + new_ucmd!().args(&["ispeed", speed]).fails(); + } +} + +#[test] +#[cfg(unix)] +fn valid_baud_formats() { + let (path, _controller, _replica) = pty_path(); + for speed in &[" +9600", "9600.49", "9600.50", "9599.51", " 9600."] { + new_ucmd!() + .args(&["--file", &path, "ispeed", speed]) + .succeeds(); + } } #[test] @@ -320,3 +357,172 @@ fn non_negatable_combo() { .fails() .stderr_contains("invalid argument '-ek'"); } + +// Tests for saved state parsing and restoration +#[test] +#[cfg(unix)] +fn test_save_and_restore() { + let (path, _controller, _replica) = pty_path(); + let saved = new_ucmd!() + .args(&["--save", "--file", &path]) + .succeeds() + .stdout_move_str(); + + let saved = saved.trim(); + assert!(saved.contains(':')); + + new_ucmd!().args(&["--file", &path, saved]).succeeds(); +} + +#[test] +#[cfg(unix)] +fn test_save_with_g_flag() { + let (path, _controller, _replica) = pty_path(); + let saved = new_ucmd!() + .args(&["-g", "--file", &path]) + .succeeds() + .stdout_move_str(); + + let saved = saved.trim(); + assert!(saved.contains(':')); + + new_ucmd!().args(&["--file", &path, saved]).succeeds(); +} + +#[test] +#[cfg(unix)] +fn test_save_restore_after_change() { + let (path, _controller, _replica) = pty_path(); + let saved = new_ucmd!() + .args(&["--save", "--file", &path]) + .succeeds() + .stdout_move_str(); + + let saved = saved.trim(); + + new_ucmd!() + .args(&["--file", &path, "intr", "^A"]) + .succeeds(); + + new_ucmd!().args(&["--file", &path, saved]).succeeds(); + + new_ucmd!() + .args(&["--file", &path]) + .succeeds() + .stdout_str_check(|s| !s.contains("intr = ^A")); +} + +// These tests both validate what we expect each input to return and their error codes +// and also use the GNU coreutils results to validate our results match expectations +#[test] +#[cfg(unix)] +fn test_saved_state_valid_formats() { + let (path, _controller, _replica) = pty_path(); + let (_at, ts) = at_and_ts!(); + + // Generate valid saved state from the actual terminal + let saved = unwrap_or_return!(expected_result(&ts, &["-g", "--file", &path])).stdout_move_str(); + let saved = saved.trim(); + + let result = ts.ucmd().args(&["--file", &path, saved]).run(); + + result.success().no_stderr(); + + let exp_result = unwrap_or_return!(expected_result(&ts, &["--file", &path, saved])); + let normalized_stderr = normalize_stderr(result.stderr_str()); + result + .stdout_is(exp_result.stdout_str()) + .code_is(exp_result.code()); + assert_eq!(normalized_stderr, exp_result.stderr_str()); +} + +#[test] +#[cfg(unix)] +fn test_saved_state_invalid_formats() { + let (path, _controller, _replica) = pty_path(); + let (_at, ts) = at_and_ts!(); + + let num_cc = nix::libc::NCCS; + + // Build test strings with platform-specific counts + let cc_zeros = vec!["0"; num_cc].join(":"); + let cc_with_invalid = if num_cc > 0 { + let mut parts = vec!["1c"; num_cc]; + parts[0] = "100"; // First control char > 255 + parts.join(":") + } else { + String::new() + }; + let cc_with_space = if num_cc > 0 { + let mut parts = vec!["1c"; num_cc]; + parts[0] = "1c "; // Space in hex + parts.join(":") + } else { + String::new() + }; + let cc_with_nonhex = if num_cc > 0 { + let mut parts = vec!["1c"; num_cc]; + parts[0] = "xyz"; // Non-hex + parts.join(":") + } else { + String::new() + }; + let cc_with_empty = if num_cc > 0 { + let mut parts = vec!["1c"; num_cc]; + parts[0] = ""; // Empty + parts.join(":") + } else { + String::new() + }; + + // Cannot test single value since it would be interpreted as baud rate + let invalid_states = vec![ + "500:5:4bf".to_string(), // fewer than expected parts + "500:5:4bf:8a3b".to_string(), // only 4 parts + format!("500:5:{}:8a3b:{}", cc_zeros, "extra"), // too many parts + format!("500::4bf:8a3b:{}", cc_zeros), // empty hex value in flags + format!("500:5:4bf:8a3b:{}", cc_with_empty), // empty hex value in cc + format!("500:5:4bf:8a3b:{}", cc_with_nonhex), // non-hex characters + format!("500:5:4bf:8a3b:{}", cc_with_space), // space in hex value + format!("500:5:4bf:8a3b:{}", cc_with_invalid), // control char > 255 + ]; + + for state in &invalid_states { + let result = ts.ucmd().args(&["--file", &path, state]).run(); + + result.failure().stderr_contains("invalid argument"); + + let exp_result = unwrap_or_return!(expected_result(&ts, &["--file", &path, state])); + let normalized_stderr = normalize_stderr(result.stderr_str()); + let exp_normalized_stderr = normalize_stderr(exp_result.stderr_str()); + result + .stdout_is(exp_result.stdout_str()) + .code_is(exp_result.code()); + assert_eq!(normalized_stderr, exp_normalized_stderr); + } +} + +#[test] +#[cfg(unix)] +#[ignore = "Fails because the implementation of print state is not correctly printing flags on certain platforms"] +fn test_saved_state_with_control_chars() { + let (path, _controller, _replica) = pty_path(); + let (_at, ts) = at_and_ts!(); + + // Build a valid saved state with platform-specific number of control characters + let num_cc = nix::libc::NCCS; + let cc_values: Vec = (1..=num_cc).map(|_| format!("{:x}", 0)).collect(); + let saved_state = format!("500:5:4bf:8a3b:{}", cc_values.join(":")); + + ts.ucmd().args(&["--file", &path, &saved_state]).succeeds(); + + let result = ts.ucmd().args(&["-g", "--file", &path]).run(); + + result.success().stdout_contains(":"); + + let exp_result = unwrap_or_return!(expected_result(&ts, &["-g", "--file", &path])); + result + .stdout_is(exp_result.stdout_str()) + .stderr_is(exp_result.stderr_str()) + .code_is(exp_result.code()); +} diff --git a/tests/by-util/test_test.rs b/tests/by-util/test_test.rs index 1dba782f5..4b5460cfd 100644 --- a/tests/by-util/test_test.rs +++ b/tests/by-util/test_test.rs @@ -5,10 +5,8 @@ // spell-checker:ignore (words) egid euid pseudofloat -use uutests::at_and_ucmd; -use uutests::new_ucmd; use uutests::util::TestScenario; -use uutests::util_name; +use uutests::{at_and_ucmd, new_ucmd, util_name}; #[test] fn test_empty_test_equivalent_to_false() { @@ -337,14 +335,26 @@ fn test_file_is_newer_than_and_older_than_itself() { } #[test] -fn test_non_existing_files() { - let scenario = TestScenario::new(util_name!()); +fn test_file_is_newer_than_non_existing_file() { + new_ucmd!() + .args(&["non_existing_file", "-nt", "regular_file"]) + .fails_with_code(1) + .no_output(); - let result = scenario - .ucmd() - .args(&["newer_file", "-nt", "regular_file"]) - .fails_with_code(1); - assert!(result.stderr().is_empty()); + new_ucmd!() + .args(&["regular_file", "-nt", "non_existing_file"]) + .succeeds() + .no_output(); + + new_ucmd!() + .args(&["non_existing_file", "-ot", "regular_file"]) + .succeeds() + .no_output(); + + new_ucmd!() + .args(&["regular_file", "-ot", "non_existing_file"]) + .fails_with_code(1) + .no_output(); } #[test] diff --git a/tests/by-util/test_timeout.rs b/tests/by-util/test_timeout.rs index b04b32203..9c5c6c1a4 100644 --- a/tests/by-util/test_timeout.rs +++ b/tests/by-util/test_timeout.rs @@ -15,9 +15,6 @@ fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(125); } -// FIXME: this depends on the system having true and false in PATH -// the best solution is probably to generate some test binaries that we can call for any -// utility that requires executing another program (kill, for instance) #[test] fn test_subcommand_return_code() { new_ucmd!().arg("1").arg("true").succeeds(); @@ -55,11 +52,11 @@ fn test_command_with_args() { fn test_verbose() { for verbose_flag in ["-v", "--verbose"] { new_ucmd!() - .args(&[verbose_flag, ".1", "sleep", "10"]) + .args(&[verbose_flag, ".1", "sleep", "1"]) .fails() .stderr_only("timeout: sending signal TERM to command 'sleep'\n"); new_ucmd!() - .args(&[verbose_flag, "-s0", "-k.1", ".1", "sleep", "10"]) + .args(&[verbose_flag, "-s0", "-k.1", ".1", "sleep", "1"]) .fails() .stderr_only("timeout: sending signal EXIT to command 'sleep'\ntimeout: sending signal KILL to command 'sleep'\n"); } @@ -112,7 +109,7 @@ fn test_preserve_status_even_when_send_signal() { // So, expected result is success and code 0. for cont_spelling in ["CONT", "cOnT", "SIGcont"] { new_ucmd!() - .args(&["-s", cont_spelling, "--preserve-status", ".1", "sleep", "2"]) + .args(&["-s", cont_spelling, "--preserve-status", ".1", "sleep", "1"]) .succeeds() .no_output(); } @@ -186,10 +183,10 @@ fn test_kill_subprocess() { new_ucmd!() .args(&[ // Make sure the CI can spawn the subprocess. - "10", + "1", "sh", "-c", - "trap 'echo inside_trap' TERM; sleep 30", + "trap 'echo inside_trap' TERM; sleep 5", ]) .fails_with_code(124) .stdout_contains("inside_trap"); @@ -223,3 +220,18 @@ fn test_terminate_child_on_receiving_terminate() { .code_is(143) .stdout_contains("child received TERM"); } + +#[test] +fn test_command_not_found() { + // Test exit code 127 when command doesn't exist + new_ucmd!() + .args(&["1", "/this/command/definitely/does/not/exist"]) + .fails_with_code(127); +} + +#[test] +fn test_command_cannot_invoke() { + // Test exit code 126 when command exists but cannot be invoked + // Try to execute a directory (should give permission denied or similar) + new_ucmd!().args(&["1", "/"]).fails_with_code(126); +} diff --git a/tests/fixtures/cksum/length_larger_than_512.expected b/tests/fixtures/cksum/length_larger_than_512.expected deleted file mode 100644 index 8b5d3d4c2..000000000 --- a/tests/fixtures/cksum/length_larger_than_512.expected +++ /dev/null @@ -1,2 +0,0 @@ -cksum: invalid length: '1024' -cksum: maximum digest length for 'BLAKE2b' is 512 bits diff --git a/tests/test_util_name.rs b/tests/test_util_name.rs index caf900db8..12309a6e3 100644 --- a/tests/test_util_name.rs +++ b/tests/test_util_name.rs @@ -195,9 +195,9 @@ fn util_invalid_name_invalid_command() { .unwrap(); let output = child.wait_with_output().unwrap(); assert_eq!(output.status.code(), Some(1)); - assert_eq!(output.stderr, b""); + assert_eq!(output.stdout, b""); assert_eq!( - output.stdout, + output.stderr, b"definitely_invalid: function/utility not found\n" ); } diff --git a/tests/uudoc/mod.rs b/tests/uudoc/mod.rs index fc64417e4..4be9803b8 100644 --- a/tests/uudoc/mod.rs +++ b/tests/uudoc/mod.rs @@ -34,8 +34,9 @@ fn test_manpage_generation() { ); let output_str = String::from_utf8_lossy(&output.stdout); - assert!(output_str.contains("\n.TH"), "{output_str}"); + assert!(output_str.contains("\n.TH ls"), "{output_str}"); assert!(output_str.contains('1'), "{output_str}"); + assert!(output_str.contains("\n.SH NAME\nls"), "{output_str}"); } #[test] @@ -57,8 +58,9 @@ fn test_manpage_coreutils() { ); let output_str = String::from_utf8_lossy(&output.stdout); - assert!(output_str.contains("\n.TH"), "{output_str}"); + assert!(output_str.contains("\n.TH coreutils"), "{output_str}"); assert!(output_str.contains("coreutils"), "{output_str}"); + assert!(output_str.contains("\n.SH NAME\ncoreutils"), "{output_str}"); } #[test] diff --git a/tests/uutests/src/lib/util.rs b/tests/uutests/src/lib/util.rs index ebd97ee5e..4668e7ba8 100644 --- a/tests/uutests/src/lib/util.rs +++ b/tests/uutests/src/lib/util.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. //spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd SHLVL canonicalized openpty //spell-checker: ignore (linux) winsize xpixel ypixel setrlimit FSIZE SIGBUS SIGSEGV sigbus tmpfs mksocket +//spell-checker: ignore (ToDO) ttyname #![allow(dead_code)] #![allow( @@ -2886,6 +2887,24 @@ pub fn whoami() -> String { }) } +/// Create a PTY (pseudo-terminal) for testing utilities that require a TTY. +/// +/// Returns a tuple of (path, controller_fd, replica_fd) where: +/// - path: The filesystem path to the PTY replica device +/// - controller_fd: The controller file descriptor +/// - replica_fd: The replica file descriptor +#[cfg(unix)] +pub fn pty_path() -> (String, OwnedFd, OwnedFd) { + use nix::pty::openpty; + use nix::unistd::ttyname; + let pty = openpty(None, None).expect("Failed to create PTY"); + let path = ttyname(&pty.slave) + .expect("Failed to get PTY path") + .to_string_lossy() + .to_string(); + (path, pty.master, pty.slave) +} + /// Add prefix 'g' for `util_name` if not on linux #[cfg(unix)] pub fn host_name_for(util_name: &str) -> Cow<'_, str> { diff --git a/util/GHA-delete-GNU-workflow-logs.sh b/util/GHA-delete-GNU-workflow-logs.sh index ebeb7cfe8..95f5c240e 100755 --- a/util/GHA-delete-GNU-workflow-logs.sh +++ b/util/GHA-delete-GNU-workflow-logs.sh @@ -1,6 +1,6 @@ #!/bin/sh -# spell-checker:ignore (utils) gitsome jq ; (gh) repos +# spell-checker:ignore (utils) gitsome jq jaq ; (gh) repos # ME="${0}" # ME_dir="$(dirname -- "${ME}")" @@ -14,24 +14,11 @@ ## tools available? # * `gh` available? -unset GH -if gh --version 1>/dev/null 2>&1; then - export GH="gh" -else - echo "ERR!: missing \`gh\` (see install instructions at )" 1>&2 -fi - -# * `jq` available? -unset JQ -if jq --version 1>/dev/null 2>&1; then - export JQ="jq" -else - echo "ERR!: missing \`jq\` (install with \`sudo apt install jq\`)" 1>&2 -fi - -if [ -z "${GH}" ] || [ -z "${JQ}" ]; then - exit 1 -fi +GH=$(command -v gh) +"${GH}" --version || (echo "ERR!: missing \`gh\` (see install instructions at )"; exit 1) +# * `jq` or fallback available? +: ${JQ:=$(command -v jq || command -v jaq)} +"${JQ}" --version || (echo "ERR!: missing \`jq\` (install with \`sudo apt install jq\`)"; exit 1) case "${dry_run}" in '0' | 'f' | 'false' | 'no' | 'never' | 'none') unset dry_run ;; @@ -44,6 +31,6 @@ WORK_NAME="${WORK_NAME:-GNU}" # * `--paginate` retrieves all pages # gh api --paginate "repos/${USER_NAME}/${REPO_NAME}/actions/runs" | jq -r ".workflow_runs[] | select(.name == \"${WORK_NAME}\") | (.id)" | xargs -n1 sh -c "for arg do { echo gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; if [ -z "$dry_run" ]; then gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; fi ; } ; done ;" _ -gh api "repos/${USER_NAME}/${REPO_NAME}/actions/runs" | - jq -r ".workflow_runs[] | select(.name == \"${WORK_NAME}\") | (.id)" | - xargs -n1 sh -c "for arg do { echo gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; if [ -z \"${dry_run}\" ]; then gh api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; fi ; } ; done ;" _ +"${GH}" api "repos/${USER_NAME}/${REPO_NAME}/actions/runs" | + "${JQ}" -r ".workflow_runs[] | select(.name == \"${WORK_NAME}\") | (.id)" | + xargs -n1 sh -c "for arg do { echo ${GH} api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; if [ -z \"${dry_run}\" ]; then ${GH} api repos/${USER_NAME}/${REPO_NAME}/actions/runs/\${arg} -X DELETE ; fi ; } ; done ;" _ diff --git a/util/android-commands.sh b/util/android-commands.sh index aa02e5b34..b87d7050b 100755 --- a/util/android-commands.sh +++ b/util/android-commands.sh @@ -329,7 +329,7 @@ init() { snapshot_name="${AVD_CACHE_KEY}" # shellcheck disable=SC2015 - wget -nv "https://github.com/termux/termux-app/releases/download/${termux}/termux-app_${termux}+github-debug_${arch}.apk" && + curl -sLO "https://github.com/termux/termux-app/releases/download/${termux}/termux-app_${termux}+github-debug_${arch}.apk" && snapshot "termux-app_${termux}+github-debug_${arch}.apk" && hash_rustc && exit_termux && diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 734088252..626400d6a 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -4,44 +4,29 @@ # spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW # spell-checker:ignore baddecode submodules xstrtol distros ; (vars/env) SRCDIR vdir rcexp xpart dired OSTYPE ; (utils) gnproc greadlink gsed multihardlink texinfo CARGOFLAGS -# spell-checker:ignore openat TOCTOU +# spell-checker:ignore openat TOCTOU CFLAGS +# spell-checker:ignore hfsplus casefold chattr set -e -# Use GNU version for make, nproc, readlink and sed on *BSD -case "$OSTYPE" in - *bsd*) - MAKE="gmake" - NPROC="gnproc" - READLINK="greadlink" - SED="gsed" - ;; - *) - MAKE="make" - NPROC="nproc" - READLINK="readlink" - SED="sed" - ;; -esac +# Use system's GNU version for make, nproc, readlink and sed on *BSD and macOS +MAKE=$(command -v gmake||command -v make) +NPROC=$(command -v gnproc||command -v nproc) +READLINK=$(command -v greadlink||command -v readlink) +SED=$(command -v gsed||command -v sed) + +SYSTEM_TIMEOUT=$(command -v timeout) +SYSTEM_YES=$(command -v yes) ME="${0}" ME_dir="$(dirname -- "$("${READLINK}" -fm -- "${ME}")")" REPO_main_dir="$(dirname -- "${ME_dir}")" -# Default profile is 'debug' -UU_MAKE_PROFILE='debug' + +: ${PROFILE:=debug} # default profile +export PROFILE CARGO_FEATURE_FLAGS="" -for arg in "$@" -do - if [ "$arg" == "--release-build" ]; then - UU_MAKE_PROFILE='release' - break - fi -done - -echo "UU_MAKE_PROFILE='${UU_MAKE_PROFILE}'" - ### * config (from environment with fallback defaults); note: GNU is expected to be a sibling repo directory path_UUTILS=${path_UUTILS:-${REPO_main_dir}} @@ -49,28 +34,7 @@ path_GNU="$("${READLINK}" -fm -- "${path_GNU:-${path_UUTILS}/../gnu}")" ### -# On MacOS there is no system /usr/bin/timeout -# and trying to add it to /usr/bin (with symlink of copy binary) will fail unless system integrity protection is disabled (not ideal) -# ref: https://support.apple.com/en-us/102149 -# On MacOS the Homebrew coreutils could be installed and then "sudo ln -s /opt/homebrew/bin/timeout /usr/local/bin/timeout" -# Set to /usr/local/bin/timeout instead if /usr/bin/timeout is not found -SYSTEM_TIMEOUT="timeout" -if [ -x /usr/bin/timeout ]; then - SYSTEM_TIMEOUT="/usr/bin/timeout" -elif [ -x /usr/local/bin/timeout ]; then - SYSTEM_TIMEOUT="/usr/local/bin/timeout" -fi - -SYSTEM_YES="yes" -if [ -x /usr/bin/yes ]; then - SYSTEM_YES="/usr/bin/yes" -elif [ -x /usr/local/bin/yes ]; then - SYSTEM_YES="/usr/local/bin/yes" -fi - -### - -release_tag_GNU="v9.8" +release_tag_GNU="v9.9" # check if the GNU coreutils has been cloned, if not print instructions # note: the ${path_GNU} might already exist, so we check for the .git directory @@ -97,20 +61,16 @@ echo "path_GNU='${path_GNU}'" ### if [[ ! -z "$CARGO_TARGET_DIR" ]]; then -UU_BUILD_DIR="${CARGO_TARGET_DIR}/${UU_MAKE_PROFILE}" +UU_BUILD_DIR="${CARGO_TARGET_DIR}/${PROFILE}" else -UU_BUILD_DIR="${path_UUTILS}/target/${UU_MAKE_PROFILE}" +UU_BUILD_DIR="${path_UUTILS}/target/${PROFILE}" fi echo "UU_BUILD_DIR='${UU_BUILD_DIR}'" cd "${path_UUTILS}" && echo "[ pwd:'${PWD}' ]" -# Check for SELinux support -if [ "$(uname)" == "Linux" ]; then - # Only attempt to enable SELinux features on Linux - export SELINUX_ENABLED=1 - CARGO_FEATURE_FLAGS="${CARGO_FEATURE_FLAGS} selinux" -fi +export SELINUX_ENABLED # Run this script with=1 for testing SELinux +[ "${SELINUX_ENABLED}" = 1 ] && CARGO_FEATURE_FLAGS="${CARGO_FEATURE_FLAGS} selinux" # Trim leading whitespace from feature flags CARGO_FEATURE_FLAGS="$(echo "${CARGO_FEATURE_FLAGS}" | sed -e 's/^[[:space:]]*//')" @@ -135,15 +95,13 @@ fi cd - # Pass the feature flags to make, which will pass them to cargo -"${MAKE}" PROFILE="${UU_MAKE_PROFILE}" CARGOFLAGS="${CARGO_FEATURE_FLAGS}" -touch g -echo "stat with selinux support" -./target/debug/stat -c%C g || true -rm g +"${MAKE}" PROFILE="${PROFILE}" SKIP_UTILS=more CARGOFLAGS="${CARGO_FEATURE_FLAGS}" +# min test for SELinux +[ "${SELINUX_ENABLED}" = 1 ] && touch g && "${PROFILE}"/stat -c%C g && rm g cp "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests rename this script before running, to avoid confusion with the make target # Create *sum binaries -for sum in b2sum b3sum md5sum sha1sum sha224sum sha256sum sha384sum sha512sum; do +for sum in b2sum md5sum sha1sum sha224sum sha256sum sha384sum sha512sum; do sum_path="${UU_BUILD_DIR}/${sum}" test -f "${sum_path}" || (cd ${UU_BUILD_DIR} && ln -s "hashsum" "${sum}") done @@ -162,22 +120,27 @@ for binary in $(./build-aux/gen-lists-of-programs.sh --list-progs); do } done +# Always update the PATH to test the uutils coreutils instead of the GNU coreutils +# This ensures the correct path is used even if the repository was moved or rebuilt in a different location +"${SED}" -i "s/^[[:blank:]]*PATH=.*/ PATH='${UU_BUILD_DIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" tests/local.mk + if test -f gnu-built; then echo "GNU build already found. Skip" echo "'rm -f $(pwd)/gnu-built' to force the build" echo "Note: the customization of the tests will still happen" else # Disable useless checks - sed -i 's|check-texinfo: $(syntax_checks)|check-texinfo:|' doc/local.mk - # Change the PATH to test the uutils coreutils instead of the GNU coreutils - sed -i "s/^[[:blank:]]*PATH=.*/ PATH='${UU_BUILD_DIR//\//\\/}\$(PATH_SEPARATOR)'\"\$\$PATH\" \\\/" tests/local.mk + "${SED}" -i 's|check-texinfo: $(syntax_checks)|check-texinfo:|' doc/local.mk + "${SED}" -i '/^wget.*/d' bootstrap.conf # wget is used to DL po. Remove the dep. ./bootstrap --skip-po - ./configure --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references + # Use CFLAGS for best build time since we discard GNU coreutils + CFLAGS="${CFLAGS} -pipe -O0 -s" ./configure --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references \ + --enable-single-binary=symlinks \ + "$([ "${SELINUX_ENABLED}" = 1 ] && echo --with-selinux || echo --without-selinux)" #Add timeout to to protect against hangs - sed -i 's|^"\$@|'"${SYSTEM_TIMEOUT}"' 600 "\$@|' build-aux/test-driver - sed -i 's| tr | /usr/bin/tr |' tests/init.sh + "${SED}" -i 's|^"\$@|'"${SYSTEM_TIMEOUT}"' 600 "\$@|' build-aux/test-driver # Use a better diff - sed -i 's|diff -c|diff -u|g' tests/Coreutils.pm + "${SED}" -i 's|diff -c|diff -u|g' tests/Coreutils.pm "${MAKE}" -j "$("${NPROC}")" # Handle generated factor tests @@ -192,157 +155,157 @@ else ) for i in ${seq}; do echo "strip t${i}.sh from Makefile" - sed -i -e "s/\$(tf)\/t${i}.sh//g" Makefile + "${SED}" -i -e "s/\$(tf)\/t${i}.sh//g" Makefile done # Remove tests checking for --version & --help # Not really interesting for us and logs are too big - sed -i -e '/tests\/help\/help-version.sh/ D' \ + "${SED}" -i -e '/tests\/help\/help-version.sh/ D' \ -e '/tests\/help\/help-version-getopt.sh/ D' \ Makefile touch gnu-built fi -grep -rl 'path_prepend_' tests/* | xargs -r sed -i 's| path_prepend_ ./src||' +grep -rl 'path_prepend_' tests/* | xargs -r "${SED}" -i 's| path_prepend_ ./src||' # path_prepend_ sets $abs_path_dir_: set it manually instead. -grep -rl '\$abs_path_dir_' tests/*/*.sh | xargs -r sed -i "s|\$abs_path_dir_|${UU_BUILD_DIR//\//\\/}|g" +grep -rl '\$abs_path_dir_' tests/*/*.sh | xargs -r "${SED}" -i "s|\$abs_path_dir_|${UU_BUILD_DIR//\//\\/}|g" + +# We use coreutils yes +"${SED}" -i "s|--coreutils-prog=||g" tests/misc/coreutils.sh +# Different message +"${SED}" -i "s|coreutils: unknown program 'blah'|blah: function/utility not found|" tests/misc/coreutils.sh + +# Remove hfs dependency (should be merged to upstream) +"${SED}" -i -e "s|hfsplus|ext4 -O casefold|" -e "s|cd mnt|rm -d mnt/lost+found;chattr +F mnt;cd mnt|" tests/mv/hardlink-case.sh # Use the system coreutils where the test fails due to error in a util that is not the one being tested -sed -i "s|grep '^#define HAVE_CAP 1' \$CONFIG_HEADER > /dev/null|true|" tests/ls/capability.sh -# tests/ls/abmon-align.sh - https://github.com/uutils/coreutils/issues/3505 -sed -i 's|touch |/usr/bin/touch |' tests/test/test-N.sh tests/ls/abmon-align.sh +"${SED}" -i "s|grep '^#define HAVE_CAP 1' \$CONFIG_HEADER > /dev/null|true|" tests/ls/capability.sh # our messages are better -sed -i "s|cannot stat 'symlink': Permission denied|not writing through dangling symlink 'symlink'|" tests/cp/fail-perm.sh -sed -i "s|cp: target directory 'symlink': Permission denied|cp: 'symlink' is not a directory|" tests/cp/fail-perm.sh +"${SED}" -i "s|cannot stat 'symlink': Permission denied|not writing through dangling symlink 'symlink'|" tests/cp/fail-perm.sh +"${SED}" -i "s|cp: target directory 'symlink': Permission denied|cp: 'symlink' is not a directory|" tests/cp/fail-perm.sh # Our message is a bit better -sed -i "s|cannot create regular file 'no-such/': Not a directory|'no-such/' is not a directory|" tests/mv/trailing-slash.sh +"${SED}" -i "s|cannot create regular file 'no-such/': Not a directory|'no-such/' is not a directory|" tests/mv/trailing-slash.sh # Our message is better -sed -i "s|warning: unrecognized escape|warning: incomplete hex escape|" tests/stat/stat-printf.pl +"${SED}" -i "s|warning: unrecognized escape|warning: incomplete hex escape|" tests/stat/stat-printf.pl -sed -i 's|timeout |'"${SYSTEM_TIMEOUT}"' |' tests/tail/follow-stdin.sh +"${SED}" -i 's|timeout |'"${SYSTEM_TIMEOUT}"' |' tests/tail/follow-stdin.sh # trap_sigpipe_or_skip_ fails with uutils tools because of a bug in # timeout/yes (https://github.com/uutils/coreutils/issues/7252), so we use # system's yes/timeout to make sure the tests run (instead of being skipped). -sed -i 's|\(trap .* \)timeout\( .* \)yes|'"\1${SYSTEM_TIMEOUT}\2${SYSTEM_YES}"'|' init.cfg +"${SED}" -i 's|\(trap .* \)timeout\( .* \)yes|'"\1${SYSTEM_TIMEOUT}\2${SYSTEM_YES}"'|' init.cfg # Remove dup of /usr/bin/ and /usr/local/bin/ when executed several times -grep -rlE '/usr/bin/\s?/usr/bin' init.cfg tests/* | xargs -r sed -Ei 's|/usr/bin/\s?/usr/bin/|/usr/bin/|g' -grep -rlE '/usr/local/bin/\s?/usr/local/bin' init.cfg tests/* | xargs -r sed -Ei 's|/usr/local/bin/\s?/usr/local/bin/|/usr/local/bin/|g' +grep -rlE '/usr/bin/\s?/usr/bin' init.cfg tests/* | xargs -r "${SED}" -Ei 's|/usr/bin/\s?/usr/bin/|/usr/bin/|g' +grep -rlE '/usr/local/bin/\s?/usr/local/bin' init.cfg tests/* | xargs -r "${SED}" -Ei 's|/usr/local/bin/\s?/usr/local/bin/|/usr/local/bin/|g' #### Adjust tests to make them work with Rust/coreutils # in some cases, what we are doing in rust/coreutils is good (or better) # we should not regress our project just to match what GNU is going. # So, do some changes on the fly -sed -i -e "s|removed directory 'a/'|removed directory 'a'|g" tests/rm/v-slash.sh +"${SED}" -i -e "s|removed directory 'a/'|removed directory 'a'|g" tests/rm/v-slash.sh # 'rel' doesn't exist. Our implementation is giving a better message. -sed -i -e "s|rm: cannot remove 'rel': Permission denied|rm: cannot remove 'rel': No such file or directory|g" tests/rm/inaccessible.sh +"${SED}" -i -e "s|rm: cannot remove 'rel': Permission denied|rm: cannot remove 'rel': No such file or directory|g" tests/rm/inaccessible.sh # Our implementation shows "Directory not empty" for directories that can't be accessed due to lack of execute permissions # This is actually more accurate than "Permission denied" since the real issue is that we can't empty the directory -sed -i -e "s|rm: cannot remove 'a/1': Permission denied|rm: cannot remove 'a/1/2': Permission denied|g" -e "s|rm: cannot remove 'b': Permission denied|rm: cannot remove 'a': Directory not empty\nrm: cannot remove 'b/3': Permission denied|g" tests/rm/rm2.sh +"${SED}" -i -e "s|rm: cannot remove 'a/1': Permission denied|rm: cannot remove 'a/1/2': Permission denied|g" -e "s|rm: cannot remove 'b': Permission denied|rm: cannot remove 'a': Directory not empty\nrm: cannot remove 'b/3': Permission denied|g" tests/rm/rm2.sh # overlay-headers.sh test intends to check for inotify events, # however there's a bug because `---dis` is an alias for: `---disable-inotify` sed -i -e "s|---dis ||g" tests/tail/overlay-headers.sh # Do not FAIL, just do a regular ERROR -sed -i -e "s|framework_failure_ 'no inotify_add_watch';|fail=1;|" tests/tail/inotify-rotate-resources.sh +"${SED}" -i -e "s|framework_failure_ 'no inotify_add_watch';|fail=1;|" tests/tail/inotify-rotate-resources.sh test -f "${UU_BUILD_DIR}/getlimits" || cp src/getlimits "${UU_BUILD_DIR}" # pr produces very long log and this command isn't super interesting # SKIP for now -sed -i -e "s|my \$prog = 'pr';$|my \$prog = 'pr';CuSkip::skip \"\$prog: SKIP for producing too long logs\";|" tests/pr/pr-tests.pl +"${SED}" -i -e "s|my \$prog = 'pr';$|my \$prog = 'pr';CuSkip::skip \"\$prog: SKIP for producing too long logs\";|" tests/pr/pr-tests.pl # We don't have the same error message and no need to be that specific -sed -i -e "s|invalid suffix in --pages argument|invalid --pages argument|" \ +"${SED}" -i -e "s|invalid suffix in --pages argument|invalid --pages argument|" \ -e "s|--pages argument '\$too_big' too large|invalid --pages argument '\$too_big'|" \ -e "s|invalid page range|invalid --pages argument|" tests/misc/xstrtol.pl # When decoding an invalid base32/64 string, gnu writes everything it was able to decode until # it hit the decode error, while we don't write anything if the input is invalid. -sed -i "s/\(baddecode.*OUT=>\"\).*\"/\1\"/g" tests/basenc/base64.pl -sed -i "s/\(\(b2[ml]_[69]\|b32h_[56]\|z85_8\|z85_35\).*OUT=>\)[^}]*\(.*\)/\1\"\"\3/g" tests/basenc/basenc.pl +"${SED}" -i "s/\(baddecode.*OUT=>\"\).*\"/\1\"/g" tests/basenc/base64.pl +"${SED}" -i "s/\(\(b2[ml]_[69]\|z85_8\|z85_35\).*OUT=>\)[^}]*\(.*\)/\1\"\"\3/g" tests/basenc/basenc.pl # add "error: " to the expected error message -sed -i "s/\$prog: invalid input/\$prog: error: invalid input/g" tests/basenc/basenc.pl +"${SED}" -i "s/\$prog: invalid input/\$prog: error: invalid input/g" tests/basenc/basenc.pl # basenc: swap out error message for unexpected arg -sed -i "s/ {ERR=>\"\$prog: foobar\\\\n\" \. \$try_help }/ {ERR=>\"error: unexpected argument '--foobar' found\n\n tip: to pass '--foobar' as a value, use '-- --foobar'\n\nUsage: basenc [OPTION]... [FILE]\n\nFor more information, try '--help'.\n\"}]/" tests/basenc/basenc.pl -sed -i "s/ {ERR_SUBST=>\"s\/(unrecognized|unknown) option \[-' \]\*foobar\[' \]\*\/foobar\/\"}],//" tests/basenc/basenc.pl +"${SED}" -i "s/ {ERR=>\"\$prog: foobar\\\\n\" \. \$try_help }/ {ERR=>\"error: unexpected argument '--foobar' found\n\n tip: to pass '--foobar' as a value, use '-- --foobar'\n\nUsage: basenc [OPTION]... [FILE]\n\nFor more information, try '--help'.\n\"}]/" tests/basenc/basenc.pl +"${SED}" -i "s/ {ERR_SUBST=>\"s\/(unrecognized|unknown) option \[-' \]\*foobar\[' \]\*\/foobar\/\"}],//" tests/basenc/basenc.pl # Remove the check whether a util was built. Otherwise tests against utils like "arch" are not run. -sed -i "s|require_built_ |# require_built_ |g" init.cfg +"${SED}" -i "s|require_built_ |# require_built_ |g" init.cfg # exit early for the selinux check. The first is enough for us. -sed -i "s|# Independent of whether SELinux|return 0\n #|g" init.cfg +"${SED}" -i "s|# Independent of whether SELinux|return 0\n #|g" init.cfg # Some tests are executed with the "nobody" user. # The check to verify if it works is based on the GNU coreutils version # making it too restrictive for us -sed -i "s|\$PACKAGE_VERSION|[0-9]*|g" tests/rm/fail-2eperm.sh tests/mv/sticky-to-xpart.sh init.cfg +"${SED}" -i "s|\$PACKAGE_VERSION|[0-9]*|g" tests/rm/fail-2eperm.sh tests/mv/sticky-to-xpart.sh init.cfg # usage_vs_getopt.sh is heavily modified as it runs all the binaries # with the option -/ is used, clap is returning a better error than GNU's. Adjust the GNU test -sed -i -e "s~ grep \" '\*/'\*\" err || framework_failure_~ grep \" '*-/'*\" err || framework_failure_~" tests/misc/usage_vs_getopt.sh -sed -i -e "s~ sed -n \"1s/'\\\/'/'OPT'/p\" < err >> pat || framework_failure_~ sed -n \"1s/'-\\\/'/'OPT'/p\" < err >> pat || framework_failure_~" tests/misc/usage_vs_getopt.sh +"${SED}" -i -e "s~ grep \" '\*/'\*\" err || framework_failure_~ grep \" '*-/'*\" err || framework_failure_~" tests/misc/usage_vs_getopt.sh +"${SED}" -i -e "s~ sed -n \"1s/'\\\/'/'OPT'/p\" < err >> pat || framework_failure_~ sed -n \"1s/'-\\\/'/'OPT'/p\" < err >> pat || framework_failure_~" tests/misc/usage_vs_getopt.sh # Ignore runcon, it needs some extra attention # For all other tools, we want drop-in compatibility, and that includes the exit code. -sed -i -e "s/rcexp=1$/rcexp=1\n case \"\$prg\" in runcon|stdbuf) return;; esac/" tests/misc/usage_vs_getopt.sh +"${SED}" -i -e "s/rcexp=1$/rcexp=1\n case \"\$prg\" in runcon|stdbuf) return;; esac/" tests/misc/usage_vs_getopt.sh # GNU has option=[SUFFIX], clap is -sed -i -e "s/cat opts/sed -i -e \"s| <.\*$||g\" opts/" tests/misc/usage_vs_getopt.sh +"${SED}" -i -e "s/cat opts/sed -i -e \"s| <.\*$||g\" opts/" tests/misc/usage_vs_getopt.sh # for some reasons, some stuff are duplicated, strip that -sed -i -e "s/provoked error./provoked error\ncat pat |sort -u > pat/" tests/misc/usage_vs_getopt.sh - -# Update the GNU error message to match ours -sed -i -e "s/link-to-dir: hard link not allowed for directory/failed to create hard link 'link-to-dir' =>/" -e "s|link-to-dir/: hard link not allowed for directory|failed to create hard link 'link-to-dir/' =>|" tests/ln/hard-to-sym.sh +"${SED}" -i -e "s/provoked error./provoked error\ncat pat |sort -u > pat/" tests/misc/usage_vs_getopt.sh # install verbose messages shows ginstall as command -sed -i -e "s/ginstall: creating directory/install: creating directory/g" tests/install/basic-1.sh +"${SED}" -i -e "s/ginstall: creating directory/install: creating directory/g" tests/install/basic-1.sh # GNU doesn't support padding < -LONG_MAX # disable this test case -# Use GNU sed because option -z is not available on BSD sed -"${SED}" -i -Ez "s/\n([^\n#]*pad-3\.2[^\n]*)\n([^\n]*)\n([^\n]*)/\n# uutils\/numfmt supports padding = LONG_MIN\n#\1\n#\2\n#\3/" tests/misc/numfmt.pl +"${SED}" -i -Ez "s/\n([^\n#]*pad-3\.2[^\n]*)\n([^\n]*)\n([^\n]*)/\n# uutils\/numfmt supports padding = LONG_MIN\n#\1\n#\2\n#\3/" tests/numfmt/numfmt.pl # Update the GNU error message to match the one generated by clap -sed -i -e "s/\$prog: multiple field specifications/error: the argument '--field ' cannot be used multiple times\n\nUsage: numfmt [OPTION]... [NUMBER]...\n\nFor more information, try '--help'./g" tests/misc/numfmt.pl -sed -i -e "s/Try 'mv --help' for more information/For more information, try '--help'/g" -e "s/mv: missing file operand/error: the following required arguments were not provided:\n ...\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" -e "s/mv: missing destination file operand after 'no-file'/error: The argument '...' requires at least 2 values, but only 1 was provided\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" tests/mv/diag.sh +"${SED}" -i -e "s/\$prog: multiple field specifications/error: the argument '--field ' cannot be used multiple times\n\nUsage: numfmt [OPTION]... [NUMBER]...\n\nFor more information, try '--help'./g" tests/numfmt/numfmt.pl +"${SED}" -i -e "s/Try 'mv --help' for more information/For more information, try '--help'/g" -e "s/mv: missing file operand/error: the following required arguments were not provided:\n ...\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" -e "s/mv: missing destination file operand after 'no-file'/error: The argument '...' requires at least 2 values, but only 1 was provided\n\nUsage: mv [OPTION]... [-T] SOURCE DEST\n mv [OPTION]... SOURCE... DIRECTORY\n mv [OPTION]... -t DIRECTORY SOURCE...\n/g" tests/mv/diag.sh # our error message is better -sed -i -e "s|mv: cannot overwrite 'a/t': Directory not empty|mv: cannot move 'b/t' to 'a/t': Directory not empty|" tests/mv/dir2dir.sh +"${SED}" -i -e "s|mv: cannot overwrite 'a/t': Directory not empty|mv: cannot move 'b/t' to 'a/t': Directory not empty|" tests/mv/dir2dir.sh # GNU doesn't support width > INT_MAX # disable these test cases -sed -i -E "s|^([^#]*2_31.*)$|#\1|g" tests/printf/printf-cov.pl +"${SED}" -i -E "s|^([^#]*2_31.*)$|#\1|g" tests/printf/printf-cov.pl -sed -i -e "s/du: invalid -t argument/du: invalid --threshold argument/" -e "s/du: option requires an argument/error: a value is required for '--threshold ' but none was supplied/" -e "s/Try 'du --help' for more information./\nFor more information, try '--help'./" tests/du/threshold.sh +"${SED}" -i -e "s/du: invalid -t argument/du: invalid --threshold argument/" -e "s/du: option requires an argument/error: a value is required for '--threshold ' but none was supplied/" -e "s/Try 'du --help' for more information./\nFor more information, try '--help'./" tests/du/threshold.sh # Remove the extra output check -sed -i -e "s|Try '\$prog --help' for more information.\\\n||" tests/du/files0-from.pl -sed -i -e "s|when reading file names from stdin, no file name of\"|-: No such file or directory\n\"|" -e "s| '-' allowed\\\n||" tests/du/files0-from.pl -sed -i -e "s|-: No such file or directory|cannot access '-': No such file or directory|g" tests/du/files0-from.pl +"${SED}" -i -e "s|Try '\$prog --help' for more information.\\\n||" tests/du/files0-from.pl +"${SED}" -i -e "s|-: No such file or directory|cannot access '-': No such file or directory|g" tests/du/files0-from.pl # Skip the move-dir-while-traversing test - our implementation uses safe traversal with openat() # which avoids the TOCTOU race condition that this test tries to trigger. The test uses inotify # to detect when du opens a directory path and moves it to cause an error, but our openat-based # implementation doesn't trigger inotify events on the full path, preventing the race condition. # This is actually better behavior - we're immune to this class of filesystem race attacks. -sed -i '1s/^/exit 0 # Skip test - uutils du uses safe traversal that prevents this race condition\n/' tests/du/move-dir-while-traversing.sh +"${SED}" -i '1s/^/exit 0 # Skip test - uutils du uses safe traversal that prevents this race condition\n/' tests/du/move-dir-while-traversing.sh awk 'BEGIN {count=0} /compare exp out2/ && count < 6 {sub(/compare exp out2/, "grep -q \"cannot be used with\" out2"); count++} 1' tests/df/df-output.sh > tests/df/df-output.sh.tmp && mv tests/df/df-output.sh.tmp tests/df/df-output.sh # with ls --dired, in case of error, we have a slightly different error position -sed -i -e "s|44 45|48 49|" tests/ls/stat-failed.sh +"${SED}" -i -e "s|44 45|48 49|" tests/ls/stat-failed.sh # small difference in the error message -# Use GNU sed for /c command "${SED}" -i -e "s/ls: invalid argument 'XX' for 'time style'/ls: invalid --time-style argument 'XX'/" \ -e "s/Valid arguments are:/Possible values are:/" \ -e "s/Try 'ls --help' for more information./\nFor more information try --help/" \ @@ -351,30 +314,29 @@ sed -i -e "s|44 45|48 49|" tests/ls/stat-failed.sh # disable two kind of tests: # "hostid BEFORE --help" doesn't fail for GNU. we fail. we are probably doing better # "hostid BEFORE --help AFTER " same for this -sed -i -e "s/env \$prog \$BEFORE \$opt > out2/env \$prog \$BEFORE \$opt > out2 #/" -e "s/env \$prog \$BEFORE \$opt AFTER > out3/env \$prog \$BEFORE \$opt AFTER > out3 #/" -e "s/compare exp out2/compare exp out2 #/" -e "s/compare exp out3/compare exp out3 #/" tests/help/help-version-getopt.sh +"${SED}" -i -e "s/env \$prog \$BEFORE \$opt > out2/env \$prog \$BEFORE \$opt > out2 #/" -e "s/env \$prog \$BEFORE \$opt AFTER > out3/env \$prog \$BEFORE \$opt AFTER > out3 #/" -e "s/compare exp out2/compare exp out2 #/" -e "s/compare exp out3/compare exp out3 #/" tests/help/help-version-getopt.sh # Add debug info + we have less syscall then GNU's. Adjust our check. -# Use GNU sed for /c command "${SED}" -i -e '/test \$n_stat1 = \$n_stat2 \\/c\ echo "n_stat1 = \$n_stat1"\n\ echo "n_stat2 = \$n_stat2"\n\ test \$n_stat1 -ge \$n_stat2 \\' tests/ls/stat-free-color.sh # no need to replicate this output with hashsum -sed -i -e "s|Try 'md5sum --help' for more information.\\\n||" tests/cksum/md5sum.pl +"${SED}" -i -e "s|Try 'md5sum --help' for more information.\\\n||" tests/cksum/md5sum.pl # Our ls command always outputs ANSI color codes prepended with a zero. However, # in the case of GNU, it seems inconsistent. Nevertheless, it looks like it # doesn't matter whether we prepend a zero or not. -sed -i -E 's/\^\[\[([1-9]m)/^[[0\1/g; s/\^\[\[m/^[[0m/g' tests/ls/color-norm.sh +"${SED}" -i -E 's/\^\[\[([1-9]m)/^[[0\1/g; s/\^\[\[m/^[[0m/g' tests/ls/color-norm.sh # It says in the test itself that having more than one reset is a bug, so we # don't need to replicate that behavior. -sed -i -E 's/(\^\[\[0m)+/\^\[\[0m/g' tests/ls/color-norm.sh +"${SED}" -i -E 's/(\^\[\[0m)+/\^\[\[0m/g' tests/ls/color-norm.sh # GNU's ls seems to output color codes in the order given in the environment # variable, but our ls seems to output them in a predefined order. Nevertheless, # the order doesn't matter, so it's okay. -sed -i 's/44;37/37;44/' tests/ls/multihardlink.sh +"${SED}" -i 's/44;37/37;44/' tests/ls/multihardlink.sh # Just like mentioned in the previous patch, GNU's ls output color codes in the # same way it is specified in the environment variable, but our ls emits them @@ -383,24 +345,19 @@ sed -i 's/44;37/37;44/' tests/ls/multihardlink.sh # individually, for example, ^[[31^[[42 instead of ^[[31;42, but we don't do # that anywhere in our implementation, and it looks like GNU's ls also doesn't # do that. So, it's okay to ignore the zero. -sed -i "s/color_code='0;31;42'/color_code='31;42'/" tests/ls/color-clear-to-eol.sh +"${SED}" -i "s/color_code='0;31;42'/color_code='31;42'/" tests/ls/color-clear-to-eol.sh # patching this because of the same reason as the last one. -sed -i "s/color_code='0;31;42'/color_code='31;42'/" tests/ls/quote-align.sh +"${SED}" -i "s/color_code='0;31;42'/color_code='31;42'/" tests/ls/quote-align.sh # Slightly different error message -sed -i 's/not supported/unexpected argument/' tests/mv/mv-exchange.sh -# Most tests check that `/usr/bin/tr` is working correctly before running. -# However in NixOS/Nix-based distros, the tr util is located somewhere in -# /nix/store/xxxxxxxxxxxx...xxxx/bin/tr -# We just replace the references to `/usr/bin/tr` with the result of `$(which tr)` -sed -i 's/\/usr\/bin\/tr/$(which tr)/' tests/init.sh +"${SED}" -i 's/not supported/unexpected argument/' tests/mv/mv-exchange.sh # upstream doesn't having the program name in the error message # but we do. We should keep it that way. -sed -i 's/echo "changing security context/echo "chcon: changing security context/' tests/chcon/chcon.sh +"${SED}" -i 's/echo "changing security context/echo "chcon: changing security context/' tests/chcon/chcon.sh # Disable this test, it is not relevant for us: # * the selinux crate is handling errors # * the test says "maybe we should not fail when no context available" -sed -i -e "s|returns_ 1||g" tests/cp/no-ctx.sh +"${SED}" -i -e "s|returns_ 1||g" tests/cp/no-ctx.sh diff --git a/util/build-run-test-coverage-linux.sh b/util/build-run-test-coverage-linux.sh index 3eec0dda3..5a5b5af2a 100755 --- a/util/build-run-test-coverage-linux.sh +++ b/util/build-run-test-coverage-linux.sh @@ -57,7 +57,7 @@ export CARGO_INCREMENTAL=0 export RUSTFLAGS="-Cinstrument-coverage -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" export RUSTDOCFLAGS="-Cpanic=abort" export RUSTUP_TOOLCHAIN="nightly-gnu" -export LLVM_PROFILE_FILE="${PROFRAW_DIR}/coverage-%m-%p.profraw" +export LLVM_PROFILE_FILE="${PROFRAW_DIR}/coverage-%4m.profraw" # Disable expanded command printing for the rest of the program set +x diff --git a/util/check-safe-traversal.sh b/util/check-safe-traversal.sh index ed3c5a78e..8dc9b04cf 100755 --- a/util/check-safe-traversal.sh +++ b/util/check-safe-traversal.sh @@ -173,6 +173,11 @@ fi if echo "$AVAILABLE_UTILS" | grep -q "chmod"; then cp -r test_dir test_chmod check_utility "chmod" "openat,fchmodat,newfstatat,chmod" "openat fchmodat" "-R 755 test_chmod" "recursive_chmod" + + # Additional regression guard: ensure recursion uses dirfd-relative openat, not AT_FDCWD with a multi-component path + if grep -q 'openat(AT_FDCWD, "test_chmod/' strace_chmod_recursive_chmod.log; then + fail_immediately "chmod recursed using AT_FDCWD with a multi-component path; expected dirfd-relative openat" + fi fi # Test chown - should use openat, fchownat, newfstatat diff --git a/util/compare_test_results.py b/util/compare_test_results.py index d5739deae..0f586d5f1 100644 --- a/util/compare_test_results.py +++ b/util/compare_test_results.py @@ -50,14 +50,14 @@ def identify_test_changes(current_flat, reference_flat): reference_flat (dict): Flattened dictionary of reference test results Returns: - tuple: Four lists containing regressions, fixes, newly_skipped, and newly_passing tests + tuple: Five lists containing regressions, fixes, newly_skipped, newly_passing, and newly_failing tests """ # Find regressions (tests that were passing but now failing) regressions = [] for test_path, status in current_flat.items(): if status in ("FAIL", "ERROR"): if test_path in reference_flat: - if reference_flat[test_path] in ("PASS", "SKIP"): + if reference_flat[test_path] == "PASS": regressions.append(test_path) # Find fixes (tests that were failing but now passing) @@ -88,7 +88,17 @@ def identify_test_changes(current_flat, reference_flat): ): newly_passing.append(test_path) - return regressions, fixes, newly_skipped, newly_passing + # Find newly failing tests (were skipped, now failing) + newly_failing = [] + for test_path, status in current_flat.items(): + if ( + status in ("FAIL", "ERROR") + and test_path in reference_flat + and reference_flat[test_path] == "SKIP" + ): + newly_failing.append(test_path) + + return regressions, fixes, newly_skipped, newly_passing, newly_failing def main(): @@ -135,8 +145,8 @@ def main(): reference_flat = flatten_test_results(reference_results) # Identify different categories of test changes - regressions, fixes, newly_skipped, newly_passing = identify_test_changes( - current_flat, reference_flat + regressions, fixes, newly_skipped, newly_passing, newly_failing = ( + identify_test_changes(current_flat, reference_flat) ) # Filter out intermittent issues from regressions @@ -147,6 +157,10 @@ def main(): real_fixes = [f for f in fixes if f not in ignore_list] intermittent_fixes = [f for f in fixes if f in ignore_list] + # Filter out intermittent issues from newly failing + real_newly_failing = [n for n in newly_failing if n not in ignore_list] + intermittent_newly_failing = [n for n in newly_failing if n in ignore_list] + # Print summary stats print(f"Total tests in current run: {len(current_flat)}") print(f"Total tests in reference: {len(reference_flat)}") @@ -156,6 +170,8 @@ def main(): print(f"Intermittent fixes: {len(intermittent_fixes)}") print(f"Newly skipped tests: {len(newly_skipped)}") print(f"Newly passing tests (previously skipped): {len(newly_passing)}") + print(f"Newly failing tests (previously skipped): {len(real_newly_failing)}") + print(f"Intermittent newly failing: {len(intermittent_newly_failing)}") output_lines = [] @@ -206,6 +222,21 @@ def main(): print(f"::notice ::{msg}", file=sys.stderr) output_lines.append(msg) + # Report newly failing tests (were skipped, now failing) + if real_newly_failing: + print("\nNEWLY FAILING TESTS (previously skipped):", file=sys.stderr) + for test in sorted(real_newly_failing): + msg = f"Note: The gnu test {test} was skipped on 'main' but is now failing." + print(f"::warning ::{msg}", file=sys.stderr) + output_lines.append(msg) + + if intermittent_newly_failing: + print("\nINTERMITTENT NEWLY FAILING (ignored):", file=sys.stderr) + for test in sorted(intermittent_newly_failing): + msg = f"Skip an intermittent issue {test} (was skipped on 'main', now failing)" + print(f"::notice ::{msg}", file=sys.stderr) + output_lines.append(msg) + if args.output and output_lines: with open(args.output, "w") as f: for line in output_lines: diff --git a/util/show-utils.sh b/util/show-utils.sh index 0a41698d9..3cc487940 100755 --- a/util/show-utils.sh +++ b/util/show-utils.sh @@ -5,14 +5,7 @@ # spell-checker:ignore (jq) deps startswith # Use GNU version for realpath on *BSD -case "$OSTYPE" in - *bsd*) - REALPATH="grealpath" - ;; - *) - REALPATH="realpath" - ;; -esac +REALPATH=$(command -v grealpath||command -v realpath) ME="${0}" ME_dir="$(dirname -- "${ME}")" diff --git a/util/test_compare_test_results.py b/util/test_compare_test_results.py index c3ab4d833..f10557c96 100644 --- a/util/test_compare_test_results.py +++ b/util/test_compare_test_results.py @@ -129,11 +129,11 @@ class TestIdentifyTestChanges(unittest.TestCase): } reference = { "tests/ls/test1": "PASS", - "tests/ls/test2": "SKIP", + "tests/ls/test2": "PASS", "tests/cp/test3": "PASS", "tests/cp/test4": "FAIL", } - regressions, _, _, _ = identify_test_changes(current, reference) + regressions, _, _, _, _ = identify_test_changes(current, reference) self.assertEqual(sorted(regressions), ["tests/ls/test1", "tests/ls/test2"]) def test_fixes(self): @@ -150,7 +150,7 @@ class TestIdentifyTestChanges(unittest.TestCase): "tests/cp/test3": "PASS", "tests/cp/test4": "FAIL", } - _, fixes, _, _ = identify_test_changes(current, reference) + _, fixes, _, _, _ = identify_test_changes(current, reference) self.assertEqual(sorted(fixes), ["tests/ls/test1", "tests/ls/test2"]) def test_newly_skipped(self): @@ -165,7 +165,7 @@ class TestIdentifyTestChanges(unittest.TestCase): "tests/ls/test2": "FAIL", "tests/cp/test3": "PASS", } - _, _, newly_skipped, _ = identify_test_changes(current, reference) + _, _, newly_skipped, _, _ = identify_test_changes(current, reference) self.assertEqual(newly_skipped, ["tests/ls/test1"]) def test_newly_passing(self): @@ -180,7 +180,7 @@ class TestIdentifyTestChanges(unittest.TestCase): "tests/ls/test2": "FAIL", "tests/cp/test3": "SKIP", } - _, _, _, newly_passing = identify_test_changes(current, reference) + _, _, _, newly_passing, _ = identify_test_changes(current, reference) self.assertEqual(newly_passing, ["tests/ls/test1"]) def test_all_categories(self): @@ -191,6 +191,7 @@ class TestIdentifyTestChanges(unittest.TestCase): "tests/cp/test3": "SKIP", # Newly skipped "tests/cp/test4": "PASS", # Newly passing "tests/rm/test5": "PASS", # No change + "tests/rm/test6": "FAIL", # Newly failing } reference = { "tests/ls/test1": "PASS", # Regression @@ -198,14 +199,16 @@ class TestIdentifyTestChanges(unittest.TestCase): "tests/cp/test3": "PASS", # Newly skipped "tests/cp/test4": "SKIP", # Newly passing "tests/rm/test5": "PASS", # No change + "tests/rm/test6": "SKIP", # Newly failing } - regressions, fixes, newly_skipped, newly_passing = identify_test_changes( - current, reference + regressions, fixes, newly_skipped, newly_passing, newly_failing = ( + identify_test_changes(current, reference) ) self.assertEqual(regressions, ["tests/ls/test1"]) self.assertEqual(fixes, ["tests/ls/test2"]) self.assertEqual(newly_skipped, ["tests/cp/test3"]) self.assertEqual(newly_passing, ["tests/cp/test4"]) + self.assertEqual(newly_failing, ["tests/rm/test6"]) def test_new_and_removed_tests(self): """Test handling of tests that are only in one of the datasets.""" @@ -219,13 +222,43 @@ class TestIdentifyTestChanges(unittest.TestCase): "tests/ls/test2": "PASS", "tests/rm/old_test": "FAIL", } - regressions, fixes, newly_skipped, newly_passing = identify_test_changes( - current, reference + regressions, fixes, newly_skipped, newly_passing, newly_failing = ( + identify_test_changes(current, reference) ) self.assertEqual(regressions, ["tests/ls/test2"]) self.assertEqual(fixes, []) self.assertEqual(newly_skipped, []) self.assertEqual(newly_passing, []) + self.assertEqual(newly_failing, []) + + def test_newly_failing(self): + """Test identifying newly failing tests (SKIP -> FAIL).""" + current = { + "tests/ls/test1": "FAIL", + "tests/ls/test2": "ERROR", + "tests/cp/test3": "PASS", + } + reference = { + "tests/ls/test1": "SKIP", + "tests/ls/test2": "SKIP", + "tests/cp/test3": "SKIP", + } + _, _, _, _, newly_failing = identify_test_changes(current, reference) + self.assertEqual(sorted(newly_failing), ["tests/ls/test1", "tests/ls/test2"]) + + def test_skip_to_fail_not_regression(self): + """Test that SKIP -> FAIL is not counted as a regression.""" + current = { + "tests/ls/test1": "FAIL", + "tests/ls/test2": "FAIL", + } + reference = { + "tests/ls/test1": "SKIP", + "tests/ls/test2": "PASS", + } + regressions, _, _, _, newly_failing = identify_test_changes(current, reference) + self.assertEqual(regressions, ["tests/ls/test2"]) + self.assertEqual(newly_failing, ["tests/ls/test1"]) class TestMainFunction(unittest.TestCase): @@ -285,7 +318,7 @@ class TestMainFunction(unittest.TestCase): current_flat = flatten_test_results(self.current_data) reference_flat = flatten_test_results(self.reference_data) - regressions, _, _, _ = identify_test_changes(current_flat, reference_flat) + regressions, _, _, _, _ = identify_test_changes(current_flat, reference_flat) self.assertIn("tests/ls/test2", regressions) @@ -320,7 +353,7 @@ class TestMainFunction(unittest.TestCase): current_flat = flatten_test_results(self.current_data) reference_flat = flatten_test_results(self.reference_data) - _, fixes, _, _ = identify_test_changes(current_flat, reference_flat) + _, fixes, _, _, _ = identify_test_changes(current_flat, reference_flat) # tests/cp/test1 and tests/cp/test2 should be fixed but tests/cp/test1 is in ignore list self.assertIn("tests/cp/test1", fixes) diff --git a/util/update-version.sh b/util/update-version.sh index ae28cf4f1..9b89937ab 100755 --- a/util/update-version.sh +++ b/util/update-version.sh @@ -17,8 +17,8 @@ # 10) Create the release on github https://github.com/uutils/coreutils/releases/new # 11) Make sure we have good release notes -FROM="0.3.0" -TO="0.4.0" +FROM="0.4.0" +TO="0.5.0" PROGS=$(ls -1d src/uu/*/Cargo.toml src/uu/stdbuf/src/libstdbuf/Cargo.toml src/uucore/Cargo.toml Cargo.toml fuzz/uufuzz/Cargo.toml src/uu/stdbuf/Cargo.toml) diff --git a/util/why-error.md b/util/why-error.md index 0fdff867a..73073c5e4 100644 --- a/util/why-error.md +++ b/util/why-error.md @@ -1,66 +1,40 @@ This file documents why some tests are failing: * gnu/tests/cp/preserve-gid.sh -* gnu/tests/csplit/csplit-suppress-matched.pl * gnu/tests/date/date-debug.sh -* gnu/tests/date/date-next-dow.pl -* gnu/tests/date/date-tz.sh * gnu/tests/date/date.pl -* gnu/tests/dd/direct.sh * gnu/tests/dd/no-allocate.sh * gnu/tests/dd/nocache_eof.sh * gnu/tests/dd/skip-seek-past-file.sh - https://github.com/uutils/coreutils/issues/7216 * gnu/tests/dd/stderr.sh -* gnu/tests/du/long-from-unreadable.sh - https://github.com/uutils/coreutils/issues/7217 -* gnu/tests/du/move-dir-while-traversing.sh -* gnu/tests/expr/expr-multibyte.pl -* gnu/tests/fmt/goal-option.sh * gnu/tests/fmt/non-space.sh -* gnu/tests/head/head-elide-tail.pl -* gnu/tests/head/head-pos.sh * gnu/tests/help/help-version-getopt.sh * gnu/tests/help/help-version.sh -* gnu/tests/install/install-C.sh - https://github.com/uutils/coreutils/pull/7215 * gnu/tests/ls/ls-misc.pl * gnu/tests/ls/stat-free-symlinks.sh * gnu/tests/misc/close-stdout.sh -* gnu/tests/misc/comm.pl * gnu/tests/misc/nohup.sh -* gnu/tests/misc/numfmt.pl - https://github.com/uutils/coreutils/issues/7219 / https://github.com/uutils/coreutils/issues/7221 +* gnu/tests/numfmt/numfmt.pl - https://github.com/uutils/coreutils/issues/7219 / https://github.com/uutils/coreutils/issues/7221 * gnu/tests/misc/stdbuf.sh - https://github.com/uutils/coreutils/issues/7072 -* gnu/tests/misc/tee.sh - https://github.com/uutils/coreutils/issues/7073 -* gnu/tests/misc/time-style.sh * gnu/tests/misc/tsort.pl - https://github.com/uutils/coreutils/issues/7074 * gnu/tests/misc/write-errors.sh -* gnu/tests/mv/hard-link-1.sh -* gnu/tests/mv/mv-special-1.sh - https://github.com/uutils/coreutils/issues/7076 -* gnu/tests/mv/part-fail.sh -* gnu/tests/mv/part-hardlink.sh -* gnu/tests/od/od-N.sh * gnu/tests/od/od-float.sh -* gnu/tests/printf/printf-quote.sh * gnu/tests/ptx/ptx-overrun.sh * gnu/tests/ptx/ptx.pl -* gnu/tests/rm/empty-inacc.sh - https://github.com/uutils/coreutils/issues/7033 -* gnu/tests/rm/ir-1.sh * gnu/tests/rm/one-file-system.sh - https://github.com/uutils/coreutils/issues/7011 -* gnu/tests/rm/rm1.sh -* gnu/tests/rm/rm2.sh +* gnu/tests/rm/rm1.sh - https://github.com/uutils/coreutils/issues/9479 * gnu/tests/shred/shred-passes.sh * gnu/tests/sort/sort-continue.sh * gnu/tests/sort/sort-debug-keys.sh * gnu/tests/sort/sort-debug-warn.sh -* gnu/tests/sort/sort-files0-from.pl * gnu/tests/sort/sort-float.sh * gnu/tests/sort/sort-h-thousands-sep.sh * gnu/tests/sort/sort-merge-fdlimit.sh * gnu/tests/sort/sort-month.sh * gnu/tests/sort/sort.pl -* gnu/tests/stat/stat-nanoseconds.sh * gnu/tests/tac/tac-2-nonseekable.sh * gnu/tests/tail/end-of-device.sh * gnu/tests/tail/follow-stdin.sh * gnu/tests/tail/inotify-rotate-resources.sh * gnu/tests/tail/symlink.sh -* gnu/tests/touch/obsolescent.sh * gnu/tests/tty/tty-eof.pl diff --git a/util/why-skip.md b/util/why-skip.md index 915b9460e..b0c181944 100644 --- a/util/why-skip.md +++ b/util/why-skip.md @@ -1,5 +1,4 @@ -# spell-checker:ignore epipe readdir restorecon SIGALRM capget bigtime rootfs enotsup - + = skipped test: breakpoint not hit = * tests/tail-2/inotify-race2.sh * tail-2/inotify-race.sh @@ -13,9 +12,6 @@ = LD_PRELOAD was ineffective? = * tests/cp/nfs-removal-race.sh -= failed to create hfs file system = -* tests/mv/hardlink-case.sh - = temporarily disabled = * tests/mkdir/writable-under-readonly.sh @@ -36,9 +32,6 @@ * tests/cp/no-ctx.sh * tests/cp/cp-a-selinux.sh -= failed to set xattr of file = -* tests/misc/xattr.sh - = timeout returned 142. SIGALRM not handled? = * tests/misc/timeout-group.sh @@ -51,38 +44,16 @@ = The Swedish locale with blank thousands separator is unavailable. = * tests/misc/sort-h-thousands-sep.sh -= this shell lacks ulimit support = -* tests/misc/csplit-heap.sh - -= multicall binary is disabled = -* tests/misc/coreutils.sh - = not running on GNU/Hurd = * tests/id/gnu-zero-uids.sh -= file system cannot represent big timestamps = -* tests/du/bigtime.sh - = no rootfs in mtab = * tests/df/skip-rootfs.sh = insufficient mount/ext2 support = -* tests/df/problematic-chars.sh * tests/cp/cp-mv-enotsup-xattr.sh -= 512 byte aligned O_DIRECT is not supported on this (file) system = -* tests/dd/direct.sh - -= skipped test: /usr/bin/touch -m -d '1998-01-15 23:00' didn't work = -* tests/misc/ls-time.sh - = requires controlling input terminal = * tests/misc/stty-pairs.sh * tests/misc/stty.sh * tests/misc/stty-invalid.sh - -= insufficient SEEK_DATA support = -* tests/cp/sparse-perf.sh -* tests/cp/sparse-extents.sh -* tests/cp/sparse-extents-2.sh -* tests/cp/sparse-2.sh