diff --git a/.clippy.toml b/.clippy.toml index 6339ccf21b4..0d66270ad80 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1,4 +1,3 @@ -msrv = "1.79.0" cognitive-complexity-threshold = 24 missing-docs-in-crate-items = true check-private-items = true diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index ae89a36ee8a..c10ad18a21c 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -119,6 +119,10 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.7 + - name: Install/setup prerequisites + shell: bash + run: | + sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev - name: Initialize workflow variables id: vars shell: bash @@ -192,13 +196,6 @@ jobs: ## Confirm MinSRV compatible 'Cargo.lock' # * 'Cargo.lock' is required to be in a format that `cargo` of MinSRV can interpret (eg, v1-format for MinSRV < v1.38) cargo fetch --locked --quiet || { echo "::error file=Cargo.lock::Incompatible (or out-of-date) 'Cargo.lock' file; update using \`cargo +${{ env.RUST_MIN_SRV }} update\`" ; exit 1 ; } - - name: Confirm MinSRV equivalence for '.clippy.toml' - shell: bash - run: | - ## Confirm MinSRV equivalence for '.clippy.toml' - # * ensure '.clippy.toml' MSRV configuration setting is equal to ${{ env.RUST_MIN_SRV }} - CLIPPY_MSRV=$(grep -P "(?i)^\s*msrv\s*=\s*" .clippy.toml | grep -oP "\d+([.]\d+)+") - if [ "${CLIPPY_MSRV}" != "${{ env.RUST_MIN_SRV }}" ]; then { echo "::error file=.clippy.toml::Incorrect MSRV configuration for clippy (found '${CLIPPY_MSRV}'; should be '${{ env.RUST_MIN_SRV }}'); update '.clippy.toml' with 'msrv = \"${{ env.RUST_MIN_SRV }}\"'" ; exit 1 ; } ; fi - name: Install/setup prerequisites shell: bash run: | @@ -420,14 +417,14 @@ jobs: --arg multisize "$SIZE_MULTI" \ '{($date): { sha: $sha, size: $size, multisize: $multisize, }}' > size-result.json - name: Download the previous individual size result - uses: dawidd6/action-download-artifact@v7 + uses: dawidd6/action-download-artifact@v8 with: workflow: CICD.yml name: individual-size-result repo: uutils/coreutils path: dl - name: Download the previous size result - uses: dawidd6/action-download-artifact@v7 + uses: dawidd6/action-download-artifact@v8 with: workflow: CICD.yml name: size-result @@ -492,7 +489,7 @@ jobs: job: # - { os , target , cargo-options , features , use-cross , toolchain, skip-tests } - { os: ubuntu-latest , target: arm-unknown-linux-gnueabihf , features: feat_os_unix_gnueabihf , use-cross: use-cross , skip-tests: true } - - { os: ubuntu-latest , target: aarch64-unknown-linux-gnu , features: feat_os_unix_gnueabihf , use-cross: use-cross , skip-tests: true } + - { os: ubuntu-24.04-arm , target: aarch64-unknown-linux-gnu , features: feat_os_unix_gnueabihf } - { os: ubuntu-latest , target: aarch64-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross , skip-tests: true } # - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: feat_selinux , use-cross: use-cross } - { os: ubuntu-latest , target: i686-unknown-linux-gnu , features: "feat_os_unix,test_risky_names", use-cross: use-cross } diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 0b9d8ce7fe4..e4a2558fb99 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -49,7 +49,7 @@ jobs: outputs path_GNU path_GNU_tests path_reference path_UUTILS # repo_default_branch="$DEFAULT_BRANCH" - repo_GNU_ref="v9.5" + repo_GNU_ref="v9.6" repo_reference_branch="$DEFAULT_BRANCH" outputs repo_default_branch repo_GNU_ref repo_reference_branch # @@ -91,7 +91,7 @@ jobs: working-directory: ${{ steps.vars.outputs.path_GNU }} - name: Retrieve reference artifacts - uses: dawidd6/action-download-artifact@v7 + uses: dawidd6/action-download-artifact@v8 # ref: <https://github.com/dawidd6/action-download-artifact> continue-on-error: true ## don't break the build for missing reference artifacts (may be expired or just not generated yet) with: @@ -105,7 +105,7 @@ 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 + sudo apt-get install -y autoconf autopoint bison texinfo gperf gcc g++ gdb python3-pyinotify jq valgrind libexpect-perl libacl1-dev libattr1-dev libcap-dev libselinux1-dev attr quilt - name: Add various locales shell: bash run: | diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index ef1602ad7cf..4c43b77d7f3 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -41,7 +41,7 @@ jobs: - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.7 - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.1.7 + uses: vmactions/freebsd-vm@v1.1.8 with: usesh: true sync: rsync @@ -135,7 +135,7 @@ jobs: - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.7 - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.1.7 + uses: vmactions/freebsd-vm@v1.1.8 with: usesh: true sync: rsync diff --git a/.github/workflows/ignore-intermittent.txt b/.github/workflows/ignore-intermittent.txt index eb3d8b54bf5..ed6e3b6ce80 100644 --- a/.github/workflows/ignore-intermittent.txt +++ b/.github/workflows/ignore-intermittent.txt @@ -1,3 +1,5 @@ tests/tail/inotify-dir-recreate tests/timeout/timeout tests/rm/rm1 +tests/misc/stdbuf +tests/misc/usage_vs_getopt diff --git a/Cargo.lock b/Cargo.lock index 443d56d15bb..b02f7eaa905 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,18 +8,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -31,9 +19,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -56,7 +44,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219e3ce6f2611d83b51ec2098a12702112c29e57203a6b0a0929b2cddb486608" dependencies = [ - "unicode-width 0.1.13", + "unicode-width 0.1.14", ] [[package]] @@ -82,58 +70,59 @@ checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.0" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", + "once_cell", "windows-sys 0.59.0", ] [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" dependencies = [ "derive_arbitrary", ] [[package]] name = "arrayref" -version = "0.3.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "bigdecimal" @@ -172,7 +161,7 @@ version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -183,7 +172,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.87", + "syn", ] [[package]] @@ -194,9 +183,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "bitvec" @@ -236,9 +225,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] @@ -274,9 +263,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.1.13" +version = "1.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" dependencies = [ "shlex", ] @@ -314,11 +303,32 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "chrono-tz" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6ac4f2c0bf0f44e9161aec9675e1050aa4a530663c4a9e37e108fa948bca9f" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" +dependencies = [ + "parse-zoneinfo", + "phf_codegen", +] + [[package]] name = "clang-sys" -version = "1.4.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", @@ -327,18 +337,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.23" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -349,9 +359,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.40" +version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2e663e3e3bed2d32d065a8404024dad306e699a04263ec59919529f803aee9" +checksum = "33a7e468e750fa4b6be660e8b5651ad47372e8fb114030b594c2d75d48c5ffd0" dependencies = [ "clap", ] @@ -364,9 +374,9 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clap_mangen" -version = "0.2.24" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbae9cbfdc5d4fa8711c09bd7b83f644cb48281ac35bf97af3e47b0675864bdf" +checksum = "724842fa9b144f9b89b3f3d371a89f3455eea660361d13a554f68f8ae5d6c13a" dependencies = [ "clap", "roff", @@ -374,9 +384,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "compare" @@ -386,22 +396,22 @@ checksum = "120133d4db2ec47efe2e26502ee984747630c67f51974fca0b6c1340cf2368d3" [[package]] name = "console" -version = "0.15.8" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "unicode-width 0.1.13", - "windows-sys 0.52.0", + "once_cell", + "unicode-width 0.2.0", + "windows-sys 0.59.0", ] [[package]] name = "const-random" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11df32a13d7892ec42d51d3d175faba5211ffe13ed25d4fb348ac9e9ce835593" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" dependencies = [ "const-random-macro", ] @@ -425,13 +435,13 @@ checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "coreutils" -version = "0.0.28" +version = "0.0.29" dependencies = [ "bincode", "chrono", @@ -601,7 +611,7 @@ dependencies = [ "lazy_static", "proc-macro2", "regex", - "syn 2.0.87", + "syn", "unicode-xid", ] @@ -613,7 +623,7 @@ checksum = "25fcfea2ee05889597d35e986c2ad0169694320ae5cc8f6d2640a4bb8a884560" dependencies = [ "lazy_static", "proc-macro2", - "syn 2.0.87", + "syn", ] [[package]] @@ -628,14 +638,14 @@ dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -649,43 +659,30 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a9b73a36529d9c47029b9fb3a6f0ea3cc916a261195352ba19e770fc1748b2" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.17" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" @@ -693,12 +690,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "crossterm_winapi", "filedescriptor", - "mio 1.0.2", + "mio", "parking_lot", - "rustix 0.38.40", + "rustix 0.38.43", "signal-hook", "signal-hook-mio", "winapi", @@ -741,15 +738,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" [[package]] name = "data-encoding-macro" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1559b6cba622276d6d63706db152618eeb15b89b3e4041446b05876e352e639" +checksum = "5b16d9d0d88a5273d830dac8b78ceb217ffc9b1d5404e5597a3542515329405b" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -757,12 +754,12 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332d754c0af53bc87c108fed664d121ecf59207ec4196041f04d6ab9002ad33f" +checksum = "1145d32e826a7748b69ee8fc62d3e6355ff7f1051df53141e7048162fc90481b" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn", ] [[package]] @@ -776,13 +773,13 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] [[package]] @@ -809,14 +806,14 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] [[package]] name = "dlv-list" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d529fd73d344663edfd598ccb3f344e46034db51ebd103518eae34338248ad73" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" dependencies = [ "const-random", ] @@ -841,15 +838,15 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.8.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "equivalent" @@ -859,9 +856,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys 0.52.0", @@ -873,7 +870,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22be12de19decddab85d09f251ec8363f060ccb22ec9c81bc157c0c8433946d8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "log", "scopeguard", "uuid", @@ -881,9 +878,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "file_diff" @@ -930,6 +927,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "fs_extra" version = "1.3.0" @@ -990,7 +993,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] [[package]] @@ -1027,9 +1030,9 @@ checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -1064,12 +1067,19 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ - "ahash", "allocator-api2", + "equivalent", + "foldhash", ] [[package]] @@ -1103,16 +1113,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi", + "windows-core", ] [[package]] @@ -1126,12 +1136,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -1149,11 +1159,11 @@ dependencies = [ [[package]] name = "inotify" -version = "0.9.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.8.0", "inotify-sys", "libc", ] @@ -1204,33 +1214,34 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.4" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "keccak" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ "cpufeatures", ] [[package]] name = "kqueue" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" dependencies = [ "kqueue-sys", "libc", @@ -1238,9 +1249,9 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" dependencies = [ "bitflags 1.3.2", "libc", @@ -1260,19 +1271,19 @@ checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "winapi", + "windows-targets 0.48.5", ] [[package]] name = "libm" -version = "0.2.7" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" @@ -1280,7 +1291,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", "redox_syscall", ] @@ -1293,15 +1304,15 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1315,17 +1326,17 @@ checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "lru" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -1371,32 +1382,19 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - -[[package]] -name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi", "libc", "log", "wasi", @@ -1409,7 +1407,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "cfg_aliases", "libc", @@ -1427,29 +1425,36 @@ dependencies = [ [[package]] name = "notify" -version = "6.0.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5738a2795d57ea20abec2d6d76c6081186709c0024187cd5977265eda6598b51" +checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" dependencies = [ - "bitflags 1.3.2", - "crossbeam-channel", + "bitflags 2.8.0", "filetime", "fsevent-sys", "inotify", "kqueue", "libc", - "mio 0.8.11", + "log", + "mio", + "notify-types", "walkdir", - "windows-sys 0.45.0", + "windows-sys 0.59.0", ] +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "nu-ansi-term" -version = "0.50.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2800e1520bdc966782168a627aa5d1ad92e33b984bf7c7615d31280c83ff14" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1516,9 +1521,9 @@ dependencies = [ [[package]] name = "num_threads" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] @@ -1564,7 +1569,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ "dlv-list", - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -1573,14 +1578,14 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a6229bad892b46b0dcfaaeb18ad0d2e56400f5aaea05b768bde96e73676cf75" dependencies = [ - "unicode-width 0.1.13", + "unicode-width 0.1.14", ] [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1599,11 +1604,20 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "parse_datetime" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8720474e3dd4af20cea8716703498b9f3b690f318fa9d9d9e2e38eaf44b96d0" +checksum = "ae130e79b384861c193d6016a46baa2733a6f8f17486eb36a5c098c577ce01e8" dependencies = [ "chrono", "nom", @@ -1612,18 +1626,18 @@ dependencies = [ [[package]] name = "phf" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_shared", ] [[package]] name = "phf_codegen" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ "phf_generator", "phf_shared", @@ -1631,9 +1645,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", @@ -1641,18 +1655,18 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1662,9 +1676,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "platform-info" @@ -1678,9 +1692,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.6.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "powerfmt" @@ -1690,9 +1704,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "pretty_assertions" @@ -1706,12 +1723,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.19" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" +checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" dependencies = [ "proc-macro2", - "syn 2.0.87", + "syn", ] [[package]] @@ -1725,9 +1742,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -1738,10 +1755,10 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "hex", "procfs-core", - "rustix 0.38.40", + "rustix 0.38.43", ] [[package]] @@ -1750,7 +1767,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "hex", ] @@ -1836,18 +1853,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] name = "reference-counted-singleton" -version = "0.1.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bfbf25d7eb88ddcbb1ec3d755d0634da8f7657b2cb8b74089121409ab8228f" +checksum = "5daffa8f5ca827e146485577fa9dba9bd9c6921e06e954ab8f6408c10f753086" [[package]] name = "regex" @@ -1863,9 +1880,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -1895,9 +1912,9 @@ dependencies = [ [[package]] name = "roff" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" +checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" [[package]] name = "rstest" @@ -1925,7 +1942,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.87", + "syn", "unicode-ident", ] @@ -1957,9 +1974,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.26" +version = "0.37.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84f3f8f960ed3b5a59055428714943298bf3fa2d4a1d53135084e0544829d995" +checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" dependencies = [ "bitflags 1.3.2", "errno", @@ -1971,17 +1988,23 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.40" +version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "errno", "libc", - "linux-raw-sys 0.4.14", + "linux-raw-sys 0.4.15", "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + [[package]] name = "same-file" version = "1.0.6" @@ -2009,7 +2032,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0139b2436c81305eb6bda33af151851f75bd62783817b25f44daa371119c30b5" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", "once_cell", "reference-counted-singleton", @@ -2019,9 +2042,9 @@ dependencies = [ [[package]] name = "selinux-sys" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d557667087c5b4791e180b80979cd1a92fdb9bfd92cfd4b9ab199c4d7402423" +checksum = "e5e6e2b8e07a8ff45c90f8e3611bf10c4da7a28d73a26f9ede04f927da234f52" dependencies = [ "bindgen", "cc", @@ -2031,9 +2054,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.14" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "serde" @@ -2061,7 +2084,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] [[package]] @@ -2119,15 +2142,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio 1.0.2", + "mio", "signal-hook", ] [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -2140,15 +2163,15 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "siphasher" -version = "0.3.10" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -2170,18 +2193,18 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smawk" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2192,20 +2215,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -2228,8 +2240,8 @@ dependencies = [ "fastrand", "getrandom", "once_cell", - "rustix 0.38.40", - "windows-sys 0.59.0", + "rustix 0.38.43", + "windows-sys 0.52.0", ] [[package]] @@ -2238,7 +2250,7 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" dependencies = [ - "rustix 0.37.26", + "rustix 0.37.28", "windows-sys 0.48.0", ] @@ -2248,7 +2260,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ - "rustix 0.38.40", + "rustix 0.38.43", "windows-sys 0.59.0", ] @@ -2261,7 +2273,7 @@ dependencies = [ "smawk", "terminal_size 0.2.6", "unicode-linebreak", - "unicode-width 0.1.13", + "unicode-width 0.1.14", ] [[package]] @@ -2275,11 +2287,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.9" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl 2.0.9", + "thiserror-impl 2.0.11", ] [[package]] @@ -2290,18 +2302,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] [[package]] name = "thiserror-impl" -version = "2.0.9" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] [[package]] @@ -2371,15 +2383,15 @@ checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" [[package]] name = "typenum" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-linebreak" @@ -2395,9 +2407,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" @@ -2407,9 +2419,9 @@ checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unindent" @@ -2419,9 +2431,9 @@ checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utmp-classic" @@ -2449,7 +2461,7 @@ dependencies = [ [[package]] name = "uu_arch" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "platform-info", @@ -2458,7 +2470,7 @@ dependencies = [ [[package]] name = "uu_base32" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2466,7 +2478,7 @@ dependencies = [ [[package]] name = "uu_base64" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uu_base32", @@ -2475,7 +2487,7 @@ dependencies = [ [[package]] name = "uu_basename" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2483,7 +2495,7 @@ dependencies = [ [[package]] name = "uu_basenc" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uu_base32", @@ -2492,29 +2504,29 @@ dependencies = [ [[package]] name = "uu_cat" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "nix", - "thiserror 2.0.9", + "thiserror 2.0.11", "uucore", ] [[package]] name = "uu_chcon" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "fts-sys", "libc", "selinux", - "thiserror 2.0.9", + "thiserror 2.0.11", "uucore", ] [[package]] name = "uu_chgrp" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2522,7 +2534,7 @@ dependencies = [ [[package]] name = "uu_chmod" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -2531,7 +2543,7 @@ dependencies = [ [[package]] name = "uu_chown" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2539,15 +2551,16 @@ dependencies = [ [[package]] name = "uu_chroot" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", + "thiserror 2.0.11", "uucore", ] [[package]] name = "uu_cksum" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "hex", @@ -2557,7 +2570,7 @@ dependencies = [ [[package]] name = "uu_comm" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2565,7 +2578,7 @@ dependencies = [ [[package]] name = "uu_cp" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "exacl", @@ -2581,17 +2594,17 @@ dependencies = [ [[package]] name = "uu_csplit" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "regex", - "thiserror 2.0.9", + "thiserror 2.0.11", "uucore", ] [[package]] name = "uu_cut" -version = "0.0.28" +version = "0.0.29" dependencies = [ "bstr", "clap", @@ -2601,7 +2614,7 @@ dependencies = [ [[package]] name = "uu_date" -version = "0.0.28" +version = "0.0.29" dependencies = [ "chrono", "clap", @@ -2613,7 +2626,7 @@ dependencies = [ [[package]] name = "uu_dd" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "gcd", @@ -2625,7 +2638,7 @@ dependencies = [ [[package]] name = "uu_df" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "tempfile", @@ -2635,7 +2648,7 @@ dependencies = [ [[package]] name = "uu_dir" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uu_ls", @@ -2644,7 +2657,7 @@ dependencies = [ [[package]] name = "uu_dircolors" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2652,7 +2665,7 @@ dependencies = [ [[package]] name = "uu_dirname" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2660,7 +2673,7 @@ dependencies = [ [[package]] name = "uu_du" -version = "0.0.28" +version = "0.0.29" dependencies = [ "chrono", "clap", @@ -2671,7 +2684,7 @@ dependencies = [ [[package]] name = "uu_echo" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2679,7 +2692,7 @@ dependencies = [ [[package]] name = "uu_env" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "nix", @@ -2689,7 +2702,7 @@ dependencies = [ [[package]] name = "uu_expand" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "unicode-width 0.2.0", @@ -2698,7 +2711,7 @@ dependencies = [ [[package]] name = "uu_expr" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "num-bigint", @@ -2709,7 +2722,7 @@ dependencies = [ [[package]] name = "uu_factor" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "coz", @@ -2723,7 +2736,7 @@ dependencies = [ [[package]] name = "uu_false" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2731,7 +2744,7 @@ dependencies = [ [[package]] name = "uu_fmt" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "unicode-width 0.2.0", @@ -2740,7 +2753,7 @@ dependencies = [ [[package]] name = "uu_fold" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2748,7 +2761,7 @@ dependencies = [ [[package]] name = "uu_groups" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2756,7 +2769,7 @@ dependencies = [ [[package]] name = "uu_hashsum" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "hex", @@ -2767,17 +2780,17 @@ dependencies = [ [[package]] name = "uu_head" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "memchr", - "thiserror 2.0.9", + "thiserror 2.0.11", "uucore", ] [[package]] name = "uu_hostid" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -2786,7 +2799,7 @@ dependencies = [ [[package]] name = "uu_hostname" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "dns-lookup", @@ -2797,7 +2810,7 @@ dependencies = [ [[package]] name = "uu_id" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "selinux", @@ -2806,7 +2819,7 @@ dependencies = [ [[package]] name = "uu_install" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "file_diff", @@ -2817,7 +2830,7 @@ dependencies = [ [[package]] name = "uu_join" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "memchr", @@ -2826,7 +2839,7 @@ dependencies = [ [[package]] name = "uu_kill" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "nix", @@ -2835,7 +2848,7 @@ dependencies = [ [[package]] name = "uu_link" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2843,7 +2856,7 @@ dependencies = [ [[package]] name = "uu_ln" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2851,7 +2864,7 @@ dependencies = [ [[package]] name = "uu_logname" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -2860,7 +2873,7 @@ dependencies = [ [[package]] name = "uu_ls" -version = "0.0.28" +version = "0.0.29" dependencies = [ "ansi-width", "chrono", @@ -2878,7 +2891,7 @@ dependencies = [ [[package]] name = "uu_mkdir" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2886,7 +2899,7 @@ dependencies = [ [[package]] name = "uu_mkfifo" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -2895,7 +2908,7 @@ dependencies = [ [[package]] name = "uu_mknod" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -2904,7 +2917,7 @@ dependencies = [ [[package]] name = "uu_mktemp" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "rand", @@ -2914,7 +2927,7 @@ dependencies = [ [[package]] name = "uu_more" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "crossterm", @@ -2926,17 +2939,18 @@ dependencies = [ [[package]] name = "uu_mv" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "fs_extra", "indicatif", + "thiserror 2.0.11", "uucore", ] [[package]] name = "uu_nice" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -2946,7 +2960,7 @@ dependencies = [ [[package]] name = "uu_nl" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "regex", @@ -2955,7 +2969,7 @@ dependencies = [ [[package]] name = "uu_nohup" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -2964,7 +2978,7 @@ dependencies = [ [[package]] name = "uu_nproc" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -2973,7 +2987,7 @@ dependencies = [ [[package]] name = "uu_numfmt" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2981,7 +2995,7 @@ dependencies = [ [[package]] name = "uu_od" -version = "0.0.28" +version = "0.0.29" dependencies = [ "byteorder", "clap", @@ -2991,7 +3005,7 @@ dependencies = [ [[package]] name = "uu_paste" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -2999,7 +3013,7 @@ dependencies = [ [[package]] name = "uu_pathchk" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3008,7 +3022,7 @@ dependencies = [ [[package]] name = "uu_pinky" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3016,7 +3030,7 @@ dependencies = [ [[package]] name = "uu_pr" -version = "0.0.28" +version = "0.0.29" dependencies = [ "chrono", "clap", @@ -3028,7 +3042,7 @@ dependencies = [ [[package]] name = "uu_printenv" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3036,7 +3050,7 @@ dependencies = [ [[package]] name = "uu_printf" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3044,7 +3058,7 @@ dependencies = [ [[package]] name = "uu_ptx" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "regex", @@ -3053,7 +3067,7 @@ dependencies = [ [[package]] name = "uu_pwd" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3061,7 +3075,7 @@ dependencies = [ [[package]] name = "uu_readlink" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3069,7 +3083,7 @@ dependencies = [ [[package]] name = "uu_realpath" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3077,7 +3091,7 @@ dependencies = [ [[package]] name = "uu_rm" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3088,7 +3102,7 @@ dependencies = [ [[package]] name = "uu_rmdir" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3097,29 +3111,30 @@ dependencies = [ [[package]] name = "uu_runcon" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", "selinux", - "thiserror 2.0.9", + "thiserror 2.0.11", "uucore", ] [[package]] name = "uu_seq" -version = "0.0.28" +version = "0.0.29" dependencies = [ "bigdecimal", "clap", "num-bigint", "num-traits", + "thiserror 2.0.11", "uucore", ] [[package]] name = "uu_shred" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3129,7 +3144,7 @@ dependencies = [ [[package]] name = "uu_shuf" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "memchr", @@ -3140,7 +3155,7 @@ dependencies = [ [[package]] name = "uu_sleep" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "fundu", @@ -3149,7 +3164,7 @@ dependencies = [ [[package]] name = "uu_sort" -version = "0.0.28" +version = "0.0.29" dependencies = [ "binary-heap-plus", "clap", @@ -3163,13 +3178,14 @@ dependencies = [ "rayon", "self_cell", "tempfile", + "thiserror 2.0.11", "unicode-width 0.2.0", "uucore", ] [[package]] name = "uu_split" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "memchr", @@ -3178,7 +3194,7 @@ dependencies = [ [[package]] name = "uu_stat" -version = "0.0.28" +version = "0.0.29" dependencies = [ "chrono", "clap", @@ -3187,7 +3203,7 @@ dependencies = [ [[package]] name = "uu_stdbuf" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "tempfile", @@ -3197,7 +3213,7 @@ dependencies = [ [[package]] name = "uu_stdbuf_libstdbuf" -version = "0.0.28" +version = "0.0.29" dependencies = [ "cpp", "cpp_build", @@ -3206,7 +3222,7 @@ dependencies = [ [[package]] name = "uu_stty" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "nix", @@ -3215,7 +3231,7 @@ dependencies = [ [[package]] name = "uu_sum" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3223,7 +3239,7 @@ dependencies = [ [[package]] name = "uu_sync" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3234,7 +3250,7 @@ dependencies = [ [[package]] name = "uu_tac" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "memchr", @@ -3245,7 +3261,7 @@ dependencies = [ [[package]] name = "uu_tail" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "fundu", @@ -3261,7 +3277,7 @@ dependencies = [ [[package]] name = "uu_tee" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3270,7 +3286,7 @@ dependencies = [ [[package]] name = "uu_test" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3279,7 +3295,7 @@ dependencies = [ [[package]] name = "uu_timeout" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3289,19 +3305,20 @@ dependencies = [ [[package]] name = "uu_touch" -version = "0.0.28" +version = "0.0.29" dependencies = [ "chrono", "clap", "filetime", "parse_datetime", + "thiserror 2.0.11", "uucore", "windows-sys 0.59.0", ] [[package]] name = "uu_tr" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "nom", @@ -3310,7 +3327,7 @@ dependencies = [ [[package]] name = "uu_true" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3318,7 +3335,7 @@ dependencies = [ [[package]] name = "uu_truncate" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3326,7 +3343,7 @@ dependencies = [ [[package]] name = "uu_tsort" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3334,7 +3351,7 @@ dependencies = [ [[package]] name = "uu_tty" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "nix", @@ -3343,7 +3360,7 @@ dependencies = [ [[package]] name = "uu_uname" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "platform-info", @@ -3352,7 +3369,7 @@ dependencies = [ [[package]] name = "uu_unexpand" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "unicode-width 0.2.0", @@ -3361,7 +3378,7 @@ dependencies = [ [[package]] name = "uu_uniq" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3369,7 +3386,7 @@ dependencies = [ [[package]] name = "uu_unlink" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3377,18 +3394,18 @@ dependencies = [ [[package]] name = "uu_uptime" -version = "0.0.28" +version = "0.0.29" dependencies = [ "chrono", "clap", - "thiserror 2.0.9", + "thiserror 2.0.11", "utmp-classic", "uucore", ] [[package]] name = "uu_users" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "utmp-classic", @@ -3397,7 +3414,7 @@ dependencies = [ [[package]] name = "uu_vdir" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uu_ls", @@ -3406,20 +3423,20 @@ dependencies = [ [[package]] name = "uu_wc" -version = "0.0.28" +version = "0.0.29" dependencies = [ "bytecount", "clap", "libc", "nix", - "thiserror 2.0.9", + "thiserror 2.0.11", "unicode-width 0.2.0", "uucore", ] [[package]] name = "uu_who" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -3427,7 +3444,7 @@ dependencies = [ [[package]] name = "uu_whoami" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -3437,7 +3454,7 @@ dependencies = [ [[package]] name = "uu_yes" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "itertools 0.14.0", @@ -3447,11 +3464,14 @@ dependencies = [ [[package]] name = "uucore" -version = "0.0.28" +version = "0.0.29" dependencies = [ "blake2b_simd", "blake3", + "chrono", + "chrono-tz", "clap", + "crc32fast", "data-encoding", "data-encoding-macro", "digest", @@ -3459,6 +3479,7 @@ dependencies = [ "dunce", "glob", "hex", + "iana-time-zone", "itertools 0.14.0", "lazy_static", "libc", @@ -3474,7 +3495,7 @@ dependencies = [ "sha3", "sm3", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.11", "time", "uucore_procs", "walkdir", @@ -3487,7 +3508,7 @@ dependencies = [ [[package]] name = "uucore_procs" -version = "0.0.28" +version = "0.0.29" dependencies = [ "proc-macro2", "quote", @@ -3496,13 +3517,13 @@ dependencies = [ [[package]] name = "uuhelp_parser" -version = "0.0.28" +version = "0.0.29" [[package]] name = "uuid" -version = "1.7.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" [[package]] name = "uutils_term_grid" @@ -3515,9 +3536,9 @@ dependencies = [ [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" @@ -3537,34 +3558,35 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.87", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3572,22 +3594,25 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-time" @@ -3630,7 +3655,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -3658,15 +3683,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -3694,21 +3710,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -3740,12 +3741,6 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3758,12 +3753,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3776,12 +3765,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3800,12 +3783,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3818,12 +3795,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3836,12 +3807,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3854,12 +3819,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3874,9 +3833,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.20" +version = "0.6.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" dependencies = [ "memchr", ] @@ -3897,8 +3856,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" dependencies = [ "libc", - "linux-raw-sys 0.4.14", - "rustix 0.38.40", + "linux-raw-sys 0.4.15", + "rustix 0.38.43", ] [[package]] @@ -3915,9 +3874,9 @@ checksum = "2a599daf1b507819c1121f0bf87fa37eb19daac6aff3aefefd4e6e2e0f2020fc" [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive", @@ -3925,13 +3884,13 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] [[package]] @@ -3947,7 +3906,7 @@ dependencies = [ "flate2", "indexmap", "memchr", - "thiserror 2.0.9", + "thiserror 2.0.11", "zopfli", ] diff --git a/Cargo.toml b/Cargo.toml index ea51f6f99e2..b4c369eb3cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ [package] name = "coreutils" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "coreutils ~ GNU coreutils (updated); implemented as universal (cross-platform) utils, written in Rust" @@ -278,6 +278,7 @@ chrono = { version = "0.4.38", default-features = false, features = [ "alloc", "clock", ] } +chrono-tz = "0.10.0" clap = { version = "4.5", features = ["wrap_help", "cargo"] } clap_complete = "4.4" clap_mangen = "0.2" @@ -297,6 +298,7 @@ gcd = "2.3" glob = "0.3.1" half = "2.4.1" hostname = "0.4" +iana-time-zone = "0.1.57" indicatif = "0.17.8" itertools = "0.14.0" libc = "0.2.153" @@ -307,14 +309,14 @@ memchr = "2.7.2" memmap2 = "0.9.4" nix = { version = "0.29", default-features = false } nom = "7.1.3" -notify = { version = "=6.0.1", features = ["macos_kqueue"] } +notify = { version = "=8.0.0", features = ["macos_kqueue"] } num-bigint = "0.4.4" num-prime = "0.4.4" num-traits = "0.2.19" number_prefix = "0.4" once_cell = "1.19.0" onig = { version = "~6.4", default-features = false } -parse_datetime = "0.6.0" +parse_datetime = "0.7.0" phf = "0.11.2" phf_codegen = "0.11.2" platform-info = "2.0.3" @@ -354,12 +356,13 @@ sha3 = "0.10.8" blake2b_simd = "1.0.2" blake3 = "1.5.1" sm3 = "0.4.2" +crc32fast = "1.4.2" digest = "0.10.7" -uucore = { version = "0.0.28", package = "uucore", path = "src/uucore" } -uucore_procs = { version = "0.0.28", package = "uucore_procs", path = "src/uucore_procs" } -uu_ls = { version = "0.0.28", path = "src/uu/ls" } -uu_base32 = { version = "0.0.28", path = "src/uu/base32" } +uucore = { version = "0.0.29", package = "uucore", path = "src/uucore" } +uucore_procs = { version = "0.0.29", package = "uucore_procs", path = "src/uucore_procs" } +uu_ls = { version = "0.0.29", path = "src/uu/ls" } +uu_base32 = { version = "0.0.29", path = "src/uu/base32" } [dependencies] clap = { workspace = true } @@ -375,109 +378,109 @@ zip = { workspace = true, optional = true } uuhelp_parser = { optional = true, version = ">=0.0.19", path = "src/uuhelp_parser" } # * uutils -uu_test = { optional = true, version = "0.0.28", package = "uu_test", path = "src/uu/test" } +uu_test = { optional = true, version = "0.0.29", package = "uu_test", path = "src/uu/test" } # -arch = { optional = true, version = "0.0.28", package = "uu_arch", path = "src/uu/arch" } -base32 = { optional = true, version = "0.0.28", package = "uu_base32", path = "src/uu/base32" } -base64 = { optional = true, version = "0.0.28", package = "uu_base64", path = "src/uu/base64" } -basename = { optional = true, version = "0.0.28", package = "uu_basename", path = "src/uu/basename" } -basenc = { optional = true, version = "0.0.28", package = "uu_basenc", path = "src/uu/basenc" } -cat = { optional = true, version = "0.0.28", package = "uu_cat", path = "src/uu/cat" } -chcon = { optional = true, version = "0.0.28", package = "uu_chcon", path = "src/uu/chcon" } -chgrp = { optional = true, version = "0.0.28", package = "uu_chgrp", path = "src/uu/chgrp" } -chmod = { optional = true, version = "0.0.28", package = "uu_chmod", path = "src/uu/chmod" } -chown = { optional = true, version = "0.0.28", package = "uu_chown", path = "src/uu/chown" } -chroot = { optional = true, version = "0.0.28", package = "uu_chroot", path = "src/uu/chroot" } -cksum = { optional = true, version = "0.0.28", package = "uu_cksum", path = "src/uu/cksum" } -comm = { optional = true, version = "0.0.28", package = "uu_comm", path = "src/uu/comm" } -cp = { optional = true, version = "0.0.28", package = "uu_cp", path = "src/uu/cp" } -csplit = { optional = true, version = "0.0.28", package = "uu_csplit", path = "src/uu/csplit" } -cut = { optional = true, version = "0.0.28", package = "uu_cut", path = "src/uu/cut" } -date = { optional = true, version = "0.0.28", package = "uu_date", path = "src/uu/date" } -dd = { optional = true, version = "0.0.28", package = "uu_dd", path = "src/uu/dd" } -df = { optional = true, version = "0.0.28", package = "uu_df", path = "src/uu/df" } -dir = { optional = true, version = "0.0.28", package = "uu_dir", path = "src/uu/dir" } -dircolors = { optional = true, version = "0.0.28", package = "uu_dircolors", path = "src/uu/dircolors" } -dirname = { optional = true, version = "0.0.28", package = "uu_dirname", path = "src/uu/dirname" } -du = { optional = true, version = "0.0.28", package = "uu_du", path = "src/uu/du" } -echo = { optional = true, version = "0.0.28", package = "uu_echo", path = "src/uu/echo" } -env = { optional = true, version = "0.0.28", package = "uu_env", path = "src/uu/env" } -expand = { optional = true, version = "0.0.28", package = "uu_expand", path = "src/uu/expand" } -expr = { optional = true, version = "0.0.28", package = "uu_expr", path = "src/uu/expr" } -factor = { optional = true, version = "0.0.28", package = "uu_factor", path = "src/uu/factor" } -false = { optional = true, version = "0.0.28", package = "uu_false", path = "src/uu/false" } -fmt = { optional = true, version = "0.0.28", package = "uu_fmt", path = "src/uu/fmt" } -fold = { optional = true, version = "0.0.28", package = "uu_fold", path = "src/uu/fold" } -groups = { optional = true, version = "0.0.28", package = "uu_groups", path = "src/uu/groups" } -hashsum = { optional = true, version = "0.0.28", package = "uu_hashsum", path = "src/uu/hashsum" } -head = { optional = true, version = "0.0.28", package = "uu_head", path = "src/uu/head" } -hostid = { optional = true, version = "0.0.28", package = "uu_hostid", path = "src/uu/hostid" } -hostname = { optional = true, version = "0.0.28", package = "uu_hostname", path = "src/uu/hostname" } -id = { optional = true, version = "0.0.28", package = "uu_id", path = "src/uu/id" } -install = { optional = true, version = "0.0.28", package = "uu_install", path = "src/uu/install" } -join = { optional = true, version = "0.0.28", package = "uu_join", path = "src/uu/join" } -kill = { optional = true, version = "0.0.28", package = "uu_kill", path = "src/uu/kill" } -link = { optional = true, version = "0.0.28", package = "uu_link", path = "src/uu/link" } -ln = { optional = true, version = "0.0.28", package = "uu_ln", path = "src/uu/ln" } -ls = { optional = true, version = "0.0.28", package = "uu_ls", path = "src/uu/ls" } -logname = { optional = true, version = "0.0.28", package = "uu_logname", path = "src/uu/logname" } -mkdir = { optional = true, version = "0.0.28", package = "uu_mkdir", path = "src/uu/mkdir" } -mkfifo = { optional = true, version = "0.0.28", package = "uu_mkfifo", path = "src/uu/mkfifo" } -mknod = { optional = true, version = "0.0.28", package = "uu_mknod", path = "src/uu/mknod" } -mktemp = { optional = true, version = "0.0.28", package = "uu_mktemp", path = "src/uu/mktemp" } -more = { optional = true, version = "0.0.28", package = "uu_more", path = "src/uu/more" } -mv = { optional = true, version = "0.0.28", package = "uu_mv", path = "src/uu/mv" } -nice = { optional = true, version = "0.0.28", package = "uu_nice", path = "src/uu/nice" } -nl = { optional = true, version = "0.0.28", package = "uu_nl", path = "src/uu/nl" } -nohup = { optional = true, version = "0.0.28", package = "uu_nohup", path = "src/uu/nohup" } -nproc = { optional = true, version = "0.0.28", package = "uu_nproc", path = "src/uu/nproc" } -numfmt = { optional = true, version = "0.0.28", package = "uu_numfmt", path = "src/uu/numfmt" } -od = { optional = true, version = "0.0.28", package = "uu_od", path = "src/uu/od" } -paste = { optional = true, version = "0.0.28", package = "uu_paste", path = "src/uu/paste" } -pathchk = { optional = true, version = "0.0.28", package = "uu_pathchk", path = "src/uu/pathchk" } -pinky = { optional = true, version = "0.0.28", package = "uu_pinky", path = "src/uu/pinky" } -pr = { optional = true, version = "0.0.28", package = "uu_pr", path = "src/uu/pr" } -printenv = { optional = true, version = "0.0.28", package = "uu_printenv", path = "src/uu/printenv" } -printf = { optional = true, version = "0.0.28", package = "uu_printf", path = "src/uu/printf" } -ptx = { optional = true, version = "0.0.28", package = "uu_ptx", path = "src/uu/ptx" } -pwd = { optional = true, version = "0.0.28", package = "uu_pwd", path = "src/uu/pwd" } -readlink = { optional = true, version = "0.0.28", package = "uu_readlink", path = "src/uu/readlink" } -realpath = { optional = true, version = "0.0.28", package = "uu_realpath", path = "src/uu/realpath" } -rm = { optional = true, version = "0.0.28", package = "uu_rm", path = "src/uu/rm" } -rmdir = { optional = true, version = "0.0.28", package = "uu_rmdir", path = "src/uu/rmdir" } -runcon = { optional = true, version = "0.0.28", package = "uu_runcon", path = "src/uu/runcon" } -seq = { optional = true, version = "0.0.28", package = "uu_seq", path = "src/uu/seq" } -shred = { optional = true, version = "0.0.28", package = "uu_shred", path = "src/uu/shred" } -shuf = { optional = true, version = "0.0.28", package = "uu_shuf", path = "src/uu/shuf" } -sleep = { optional = true, version = "0.0.28", package = "uu_sleep", path = "src/uu/sleep" } -sort = { optional = true, version = "0.0.28", package = "uu_sort", path = "src/uu/sort" } -split = { optional = true, version = "0.0.28", package = "uu_split", path = "src/uu/split" } -stat = { optional = true, version = "0.0.28", package = "uu_stat", path = "src/uu/stat" } -stdbuf = { optional = true, version = "0.0.28", package = "uu_stdbuf", path = "src/uu/stdbuf" } -stty = { optional = true, version = "0.0.28", package = "uu_stty", path = "src/uu/stty" } -sum = { optional = true, version = "0.0.28", package = "uu_sum", path = "src/uu/sum" } -sync = { optional = true, version = "0.0.28", package = "uu_sync", path = "src/uu/sync" } -tac = { optional = true, version = "0.0.28", package = "uu_tac", path = "src/uu/tac" } -tail = { optional = true, version = "0.0.28", package = "uu_tail", path = "src/uu/tail" } -tee = { optional = true, version = "0.0.28", package = "uu_tee", path = "src/uu/tee" } -timeout = { optional = true, version = "0.0.28", package = "uu_timeout", path = "src/uu/timeout" } -touch = { optional = true, version = "0.0.28", package = "uu_touch", path = "src/uu/touch" } -tr = { optional = true, version = "0.0.28", package = "uu_tr", path = "src/uu/tr" } -true = { optional = true, version = "0.0.28", package = "uu_true", path = "src/uu/true" } -truncate = { optional = true, version = "0.0.28", package = "uu_truncate", path = "src/uu/truncate" } -tsort = { optional = true, version = "0.0.28", package = "uu_tsort", path = "src/uu/tsort" } -tty = { optional = true, version = "0.0.28", package = "uu_tty", path = "src/uu/tty" } -uname = { optional = true, version = "0.0.28", package = "uu_uname", path = "src/uu/uname" } -unexpand = { optional = true, version = "0.0.28", package = "uu_unexpand", path = "src/uu/unexpand" } -uniq = { optional = true, version = "0.0.28", package = "uu_uniq", path = "src/uu/uniq" } -unlink = { optional = true, version = "0.0.28", package = "uu_unlink", path = "src/uu/unlink" } -uptime = { optional = true, version = "0.0.28", package = "uu_uptime", path = "src/uu/uptime" } -users = { optional = true, version = "0.0.28", package = "uu_users", path = "src/uu/users" } -vdir = { optional = true, version = "0.0.28", package = "uu_vdir", path = "src/uu/vdir" } -wc = { optional = true, version = "0.0.28", package = "uu_wc", path = "src/uu/wc" } -who = { optional = true, version = "0.0.28", package = "uu_who", path = "src/uu/who" } -whoami = { optional = true, version = "0.0.28", package = "uu_whoami", path = "src/uu/whoami" } -yes = { optional = true, version = "0.0.28", package = "uu_yes", path = "src/uu/yes" } +arch = { optional = true, version = "0.0.29", package = "uu_arch", path = "src/uu/arch" } +base32 = { optional = true, version = "0.0.29", package = "uu_base32", path = "src/uu/base32" } +base64 = { optional = true, version = "0.0.29", package = "uu_base64", path = "src/uu/base64" } +basename = { optional = true, version = "0.0.29", package = "uu_basename", path = "src/uu/basename" } +basenc = { optional = true, version = "0.0.29", package = "uu_basenc", path = "src/uu/basenc" } +cat = { optional = true, version = "0.0.29", package = "uu_cat", path = "src/uu/cat" } +chcon = { optional = true, version = "0.0.29", package = "uu_chcon", path = "src/uu/chcon" } +chgrp = { optional = true, version = "0.0.29", package = "uu_chgrp", path = "src/uu/chgrp" } +chmod = { optional = true, version = "0.0.29", package = "uu_chmod", path = "src/uu/chmod" } +chown = { optional = true, version = "0.0.29", package = "uu_chown", path = "src/uu/chown" } +chroot = { optional = true, version = "0.0.29", package = "uu_chroot", path = "src/uu/chroot" } +cksum = { optional = true, version = "0.0.29", package = "uu_cksum", path = "src/uu/cksum" } +comm = { optional = true, version = "0.0.29", package = "uu_comm", path = "src/uu/comm" } +cp = { optional = true, version = "0.0.29", package = "uu_cp", path = "src/uu/cp" } +csplit = { optional = true, version = "0.0.29", package = "uu_csplit", path = "src/uu/csplit" } +cut = { optional = true, version = "0.0.29", package = "uu_cut", path = "src/uu/cut" } +date = { optional = true, version = "0.0.29", package = "uu_date", path = "src/uu/date" } +dd = { optional = true, version = "0.0.29", package = "uu_dd", path = "src/uu/dd" } +df = { optional = true, version = "0.0.29", package = "uu_df", path = "src/uu/df" } +dir = { optional = true, version = "0.0.29", package = "uu_dir", path = "src/uu/dir" } +dircolors = { optional = true, version = "0.0.29", package = "uu_dircolors", path = "src/uu/dircolors" } +dirname = { optional = true, version = "0.0.29", package = "uu_dirname", path = "src/uu/dirname" } +du = { optional = true, version = "0.0.29", package = "uu_du", path = "src/uu/du" } +echo = { optional = true, version = "0.0.29", package = "uu_echo", path = "src/uu/echo" } +env = { optional = true, version = "0.0.29", package = "uu_env", path = "src/uu/env" } +expand = { optional = true, version = "0.0.29", package = "uu_expand", path = "src/uu/expand" } +expr = { optional = true, version = "0.0.29", package = "uu_expr", path = "src/uu/expr" } +factor = { optional = true, version = "0.0.29", package = "uu_factor", path = "src/uu/factor" } +false = { optional = true, version = "0.0.29", package = "uu_false", path = "src/uu/false" } +fmt = { optional = true, version = "0.0.29", package = "uu_fmt", path = "src/uu/fmt" } +fold = { optional = true, version = "0.0.29", package = "uu_fold", path = "src/uu/fold" } +groups = { optional = true, version = "0.0.29", package = "uu_groups", path = "src/uu/groups" } +hashsum = { optional = true, version = "0.0.29", package = "uu_hashsum", path = "src/uu/hashsum" } +head = { optional = true, version = "0.0.29", package = "uu_head", path = "src/uu/head" } +hostid = { optional = true, version = "0.0.29", package = "uu_hostid", path = "src/uu/hostid" } +hostname = { optional = true, version = "0.0.29", package = "uu_hostname", path = "src/uu/hostname" } +id = { optional = true, version = "0.0.29", package = "uu_id", path = "src/uu/id" } +install = { optional = true, version = "0.0.29", package = "uu_install", path = "src/uu/install" } +join = { optional = true, version = "0.0.29", package = "uu_join", path = "src/uu/join" } +kill = { optional = true, version = "0.0.29", package = "uu_kill", path = "src/uu/kill" } +link = { optional = true, version = "0.0.29", package = "uu_link", path = "src/uu/link" } +ln = { optional = true, version = "0.0.29", package = "uu_ln", path = "src/uu/ln" } +ls = { optional = true, version = "0.0.29", package = "uu_ls", path = "src/uu/ls" } +logname = { optional = true, version = "0.0.29", package = "uu_logname", path = "src/uu/logname" } +mkdir = { optional = true, version = "0.0.29", package = "uu_mkdir", path = "src/uu/mkdir" } +mkfifo = { optional = true, version = "0.0.29", package = "uu_mkfifo", path = "src/uu/mkfifo" } +mknod = { optional = true, version = "0.0.29", package = "uu_mknod", path = "src/uu/mknod" } +mktemp = { optional = true, version = "0.0.29", package = "uu_mktemp", path = "src/uu/mktemp" } +more = { optional = true, version = "0.0.29", package = "uu_more", path = "src/uu/more" } +mv = { optional = true, version = "0.0.29", package = "uu_mv", path = "src/uu/mv" } +nice = { optional = true, version = "0.0.29", package = "uu_nice", path = "src/uu/nice" } +nl = { optional = true, version = "0.0.29", package = "uu_nl", path = "src/uu/nl" } +nohup = { optional = true, version = "0.0.29", package = "uu_nohup", path = "src/uu/nohup" } +nproc = { optional = true, version = "0.0.29", package = "uu_nproc", path = "src/uu/nproc" } +numfmt = { optional = true, version = "0.0.29", package = "uu_numfmt", path = "src/uu/numfmt" } +od = { optional = true, version = "0.0.29", package = "uu_od", path = "src/uu/od" } +paste = { optional = true, version = "0.0.29", package = "uu_paste", path = "src/uu/paste" } +pathchk = { optional = true, version = "0.0.29", package = "uu_pathchk", path = "src/uu/pathchk" } +pinky = { optional = true, version = "0.0.29", package = "uu_pinky", path = "src/uu/pinky" } +pr = { optional = true, version = "0.0.29", package = "uu_pr", path = "src/uu/pr" } +printenv = { optional = true, version = "0.0.29", package = "uu_printenv", path = "src/uu/printenv" } +printf = { optional = true, version = "0.0.29", package = "uu_printf", path = "src/uu/printf" } +ptx = { optional = true, version = "0.0.29", package = "uu_ptx", path = "src/uu/ptx" } +pwd = { optional = true, version = "0.0.29", package = "uu_pwd", path = "src/uu/pwd" } +readlink = { optional = true, version = "0.0.29", package = "uu_readlink", path = "src/uu/readlink" } +realpath = { optional = true, version = "0.0.29", package = "uu_realpath", path = "src/uu/realpath" } +rm = { optional = true, version = "0.0.29", package = "uu_rm", path = "src/uu/rm" } +rmdir = { optional = true, version = "0.0.29", package = "uu_rmdir", path = "src/uu/rmdir" } +runcon = { optional = true, version = "0.0.29", package = "uu_runcon", path = "src/uu/runcon" } +seq = { optional = true, version = "0.0.29", package = "uu_seq", path = "src/uu/seq" } +shred = { optional = true, version = "0.0.29", package = "uu_shred", path = "src/uu/shred" } +shuf = { optional = true, version = "0.0.29", package = "uu_shuf", path = "src/uu/shuf" } +sleep = { optional = true, version = "0.0.29", package = "uu_sleep", path = "src/uu/sleep" } +sort = { optional = true, version = "0.0.29", package = "uu_sort", path = "src/uu/sort" } +split = { optional = true, version = "0.0.29", package = "uu_split", path = "src/uu/split" } +stat = { optional = true, version = "0.0.29", package = "uu_stat", path = "src/uu/stat" } +stdbuf = { optional = true, version = "0.0.29", package = "uu_stdbuf", path = "src/uu/stdbuf" } +stty = { optional = true, version = "0.0.29", package = "uu_stty", path = "src/uu/stty" } +sum = { optional = true, version = "0.0.29", package = "uu_sum", path = "src/uu/sum" } +sync = { optional = true, version = "0.0.29", package = "uu_sync", path = "src/uu/sync" } +tac = { optional = true, version = "0.0.29", package = "uu_tac", path = "src/uu/tac" } +tail = { optional = true, version = "0.0.29", package = "uu_tail", path = "src/uu/tail" } +tee = { optional = true, version = "0.0.29", package = "uu_tee", path = "src/uu/tee" } +timeout = { optional = true, version = "0.0.29", package = "uu_timeout", path = "src/uu/timeout" } +touch = { optional = true, version = "0.0.29", package = "uu_touch", path = "src/uu/touch" } +tr = { optional = true, version = "0.0.29", package = "uu_tr", path = "src/uu/tr" } +true = { optional = true, version = "0.0.29", package = "uu_true", path = "src/uu/true" } +truncate = { optional = true, version = "0.0.29", package = "uu_truncate", path = "src/uu/truncate" } +tsort = { optional = true, version = "0.0.29", package = "uu_tsort", path = "src/uu/tsort" } +tty = { optional = true, version = "0.0.29", package = "uu_tty", path = "src/uu/tty" } +uname = { optional = true, version = "0.0.29", package = "uu_uname", path = "src/uu/uname" } +unexpand = { optional = true, version = "0.0.29", package = "uu_unexpand", path = "src/uu/unexpand" } +uniq = { optional = true, version = "0.0.29", package = "uu_uniq", path = "src/uu/uniq" } +unlink = { optional = true, version = "0.0.29", package = "uu_unlink", path = "src/uu/unlink" } +uptime = { optional = true, version = "0.0.29", package = "uu_uptime", path = "src/uu/uptime" } +users = { optional = true, version = "0.0.29", package = "uu_users", path = "src/uu/users" } +vdir = { optional = true, version = "0.0.29", package = "uu_vdir", path = "src/uu/vdir" } +wc = { optional = true, version = "0.0.29", package = "uu_wc", path = "src/uu/wc" } +who = { optional = true, version = "0.0.29", package = "uu_who", path = "src/uu/who" } +whoami = { optional = true, version = "0.0.29", package = "uu_whoami", path = "src/uu/whoami" } +yes = { optional = true, version = "0.0.29", package = "uu_yes", path = "src/uu/yes" } # this breaks clippy linting with: "tests/by-util/test_factor_benches.rs: No such file or directory (os error 2)" # factor_benches = { optional = true, version = "0.0.0", package = "uu_factor_benches", path = "tests/benches/factor" } @@ -564,6 +567,8 @@ multiple_crate_versions = "allow" cargo_common_metadata = "allow" uninlined_format_args = "allow" missing_panics_doc = "allow" +# TODO remove when https://github.com/rust-lang/rust-clippy/issues/13774 is fixed +large_stack_arrays = "allow" use_self = "warn" needless_pass_by_value = "warn" @@ -571,6 +576,7 @@ semicolon_if_nothing_returned = "warn" single_char_pattern = "warn" explicit_iter_loop = "warn" if_not_else = "warn" +manual_if_else = "warn" all = { level = "deny", priority = -1 } cargo = { level = "warn", priority = -1 } diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 6f1de3b5476..0f3a3691d9d 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -241,6 +241,8 @@ DEBUG=1 bash util/run-gnu-test.sh tests/misc/sm3sum.pl Note that GNU test suite relies on individual utilities (not the multicall binary). +You also need to install [quilt](https://savannah.nongnu.org/projects/quilt), a tool used to manage a stack of patches for modifying GNU tests. + On FreeBSD, you need to install packages for GNU coreutils and sed (used in shell scripts instead of system commands): ```shell diff --git a/deny.toml b/deny.toml index 4208b9245ce..fa86931fa83 100644 --- a/deny.toml +++ b/deny.toml @@ -25,7 +25,8 @@ allow = [ "BSD-3-Clause", "BSL-1.0", "CC0-1.0", - "Unicode-DFS-2016", + "Unicode-3.0", + "Zlib", ] confidence-threshold = 0.8 @@ -57,31 +58,13 @@ skip = [ { name = "linux-raw-sys", version = "0.3.8" }, # terminal_size { name = "rustix", version = "0.37.26" }, - # notify - { name = "windows-sys", version = "0.45.0" }, # various crates { name = "windows-sys", version = "0.48.0" }, # various crates { name = "windows-sys", version = "0.52.0" }, # windows-sys - { name = "windows-targets", version = "0.42.2" }, - # windows-sys { name = "windows-targets", version = "0.48.0" }, # windows-targets - { name = "windows_aarch64_gnullvm", version = "0.42.2" }, - # windows-targets - { name = "windows_aarch64_msvc", version = "0.42.2" }, - # windows-targets - { name = "windows_i686_gnu", version = "0.42.2" }, - # windows-targets - { name = "windows_i686_msvc", version = "0.42.2" }, - # windows-targets - { name = "windows_x86_64_gnu", version = "0.42.2" }, - # windows-targets - { name = "windows_x86_64_gnullvm", version = "0.42.2" }, - # windows-targets - { name = "windows_x86_64_msvc", version = "0.42.2" }, - # windows-targets { name = "windows_aarch64_gnullvm", version = "0.48.0" }, # windows-targets { name = "windows_aarch64_msvc", version = "0.48.0" }, @@ -95,22 +78,20 @@ skip = [ { name = "windows_x86_64_gnullvm", version = "0.48.0" }, # windows-targets { name = "windows_x86_64_msvc", version = "0.48.0" }, - # data-encoding-macro-internal - { name = "syn", version = "1.0.109" }, # various crates { name = "bitflags", version = "1.3.2" }, - # clap_builder, textwrap + # textwrap { name = "terminal_size", version = "0.2.6" }, # ansi-width, console, os_display { name = "unicode-width", version = "0.1.13" }, - # notify - { name = "mio", version = "0.8.11" }, # various crates { name = "thiserror", version = "1.0.69" }, # thiserror { name = "thiserror-impl", version = "1.0.69" }, # bindgen { name = "itertools", version = "0.13.0" }, + # indexmap + { name = "hashbrown", version = "0.14.5" }, ] # spell-checker: enable diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index e07a770f40a..f4c0e93c843 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -101,9 +101,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bigdecimal" -version = "0.4.3" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9324c8014cd04590682b34f1e9448d38f0674d0f7b2dc553331016ef0e4e9ebc" +checksum = "7f31f3af01c5c65a07985c804d3366560e6fa7883d640a122819b14ec327482c" dependencies = [ "autocfg", "libm", @@ -230,6 +230,27 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "chrono-tz" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6dd8046d00723a59a2f8c5f295c515b9bb9a331ee4f8f3d4dd49e428acd3b6" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" +dependencies = [ + "parse-zoneinfo", + "phf_codegen", +] + [[package]] name = "clap" version = "4.5.4" @@ -722,6 +743,15 @@ dependencies = [ "unicode-width 0.1.12", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "parse_datetime" version = "0.6.0" @@ -733,6 +763,44 @@ dependencies = [ "regex", ] +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -932,9 +1000,15 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "similar" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "sm3" @@ -1064,7 +1138,7 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uu_cksum" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "hex", @@ -1074,7 +1148,7 @@ dependencies = [ [[package]] name = "uu_cut" -version = "0.0.28" +version = "0.0.29" dependencies = [ "bstr", "clap", @@ -1084,10 +1158,12 @@ dependencies = [ [[package]] name = "uu_date" -version = "0.0.28" +version = "0.0.29" dependencies = [ "chrono", + "chrono-tz", "clap", + "iana-time-zone", "libc", "parse_datetime", "uucore", @@ -1096,7 +1172,7 @@ dependencies = [ [[package]] name = "uu_echo" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -1104,7 +1180,7 @@ dependencies = [ [[package]] name = "uu_env" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "nix 0.29.0", @@ -1114,7 +1190,7 @@ dependencies = [ [[package]] name = "uu_expr" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "num-bigint", @@ -1125,7 +1201,7 @@ dependencies = [ [[package]] name = "uu_printf" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "uucore", @@ -1133,7 +1209,7 @@ dependencies = [ [[package]] name = "uu_seq" -version = "0.0.28" +version = "0.0.29" dependencies = [ "bigdecimal", "clap", @@ -1144,7 +1220,7 @@ dependencies = [ [[package]] name = "uu_sort" -version = "0.0.28" +version = "0.0.29" dependencies = [ "binary-heap-plus", "clap", @@ -1164,7 +1240,7 @@ dependencies = [ [[package]] name = "uu_split" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "memchr", @@ -1173,7 +1249,7 @@ dependencies = [ [[package]] name = "uu_test" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "libc", @@ -1182,7 +1258,7 @@ dependencies = [ [[package]] name = "uu_tr" -version = "0.0.28" +version = "0.0.29" dependencies = [ "clap", "nom", @@ -1191,7 +1267,7 @@ dependencies = [ [[package]] name = "uu_wc" -version = "0.0.28" +version = "0.0.29" dependencies = [ "bytecount", "clap", @@ -1204,7 +1280,7 @@ dependencies = [ [[package]] name = "uucore" -version = "0.0.28" +version = "0.0.29" dependencies = [ "blake2b_simd", "blake3", @@ -1264,7 +1340,7 @@ dependencies = [ [[package]] name = "uucore_procs" -version = "0.0.28" +version = "0.0.29" dependencies = [ "proc-macro2", "quote", @@ -1273,7 +1349,7 @@ dependencies = [ [[package]] name = "uuhelp_parser" -version = "0.0.28" +version = "0.0.29" [[package]] name = "version_check" diff --git a/src/uu/arch/Cargo.toml b/src/uu/arch/Cargo.toml index ccf2136f720..d1b8baea7d2 100644 --- a/src/uu/arch/Cargo.toml +++ b/src/uu/arch/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_arch" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "arch ~ (uutils) display machine architecture" diff --git a/src/uu/base32/Cargo.toml b/src/uu/base32/Cargo.toml index 26ab2bc6fe8..b75a4cdc05b 100644 --- a/src/uu/base32/Cargo.toml +++ b/src/uu/base32/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_base32" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "base32 ~ (uutils) decode/encode input (base32-encoding)" diff --git a/src/uu/base64/Cargo.toml b/src/uu/base64/Cargo.toml index 7110b6395b0..4ed327ddc69 100644 --- a/src/uu/base64/Cargo.toml +++ b/src/uu/base64/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_base64" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "base64 ~ (uutils) decode/encode input (base64-encoding)" diff --git a/src/uu/basename/Cargo.toml b/src/uu/basename/Cargo.toml index 18f937b091f..31c962019d6 100644 --- a/src/uu/basename/Cargo.toml +++ b/src/uu/basename/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_basename" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "basename ~ (uutils) display PATHNAME with leading directory components removed" diff --git a/src/uu/basename/src/basename.rs b/src/uu/basename/src/basename.rs index f502fb23466..3df9c98a3b3 100644 --- a/src/uu/basename/src/basename.rs +++ b/src/uu/basename/src/basename.rs @@ -125,16 +125,13 @@ fn basename(fullname: &str, suffix: &str) -> String { // Convert to path buffer and get last path component let pb = PathBuf::from(path); - match pb.components().last() { - Some(c) => { - let name = c.as_os_str().to_str().unwrap(); - if name == suffix { - name.to_string() - } else { - name.strip_suffix(suffix).unwrap_or(name).to_string() - } - } - None => String::new(), - } + pb.components().next_back().map_or_else(String::new, |c| { + let name = c.as_os_str().to_str().unwrap(); + if name == suffix { + name.to_string() + } else { + name.strip_suffix(suffix).unwrap_or(name).to_string() + } + }) } diff --git a/src/uu/basenc/Cargo.toml b/src/uu/basenc/Cargo.toml index d7a2849ca2b..a3bccb72c48 100644 --- a/src/uu/basenc/Cargo.toml +++ b/src/uu/basenc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_basenc" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "basenc ~ (uutils) decode/encode input" diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index f2df1c343ab..7a571c2cc95 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cat" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "cat ~ (uutils) concatenate and display input" diff --git a/src/uu/chcon/Cargo.toml b/src/uu/chcon/Cargo.toml index a4b914ad72a..897fafbe00f 100644 --- a/src/uu/chcon/Cargo.toml +++ b/src/uu/chcon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chcon" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "chcon ~ (uutils) change file security context" diff --git a/src/uu/chgrp/Cargo.toml b/src/uu/chgrp/Cargo.toml index 778576b887d..ec5e77ea569 100644 --- a/src/uu/chgrp/Cargo.toml +++ b/src/uu/chgrp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chgrp" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "chgrp ~ (uutils) change the group ownership of FILE" diff --git a/src/uu/chgrp/src/chgrp.rs b/src/uu/chgrp/src/chgrp.rs index fe5aee872e6..07d34071cfa 100644 --- a/src/uu/chgrp/src/chgrp.rs +++ b/src/uu/chgrp/src/chgrp.rs @@ -19,8 +19,26 @@ use std::os::unix::fs::MetadataExt; const ABOUT: &str = help_about!("chgrp.md"); const USAGE: &str = help_usage!("chgrp.md"); -fn parse_gid_and_uid(matches: &ArgMatches) -> UResult<GidUidOwnerFilter> { - let mut raw_group: String = String::new(); +fn parse_gid_from_str(group: &str) -> Result<u32, String> { + if let Some(gid_str) = group.strip_prefix(':') { + // Handle :gid format + gid_str + .parse::<u32>() + .map_err(|_| format!("invalid group id: '{}'", gid_str)) + } else { + // Try as group name first + match entries::grp2gid(group) { + Ok(g) => Ok(g), + // If group name lookup fails, try parsing as raw number + Err(_) => group + .parse::<u32>() + .map_err(|_| format!("invalid group: '{}'", group)), + } + } +} + +fn get_dest_gid(matches: &ArgMatches) -> UResult<(Option<u32>, String)> { + let mut raw_group = String::new(); let dest_gid = if let Some(file) = matches.get_one::<String>(options::REFERENCE) { fs::metadata(file) .map(|meta| { @@ -38,22 +56,38 @@ fn parse_gid_and_uid(matches: &ArgMatches) -> UResult<GidUidOwnerFilter> { if group.is_empty() { None } else { - match entries::grp2gid(group) { + match parse_gid_from_str(group) { Ok(g) => Some(g), - _ => { - return Err(USimpleError::new( - 1, - format!("invalid group: {}", group.quote()), - )) - } + Err(e) => return Err(USimpleError::new(1, e)), + } + } + }; + Ok((dest_gid, raw_group)) +} + +fn parse_gid_and_uid(matches: &ArgMatches) -> UResult<GidUidOwnerFilter> { + let (dest_gid, raw_group) = get_dest_gid(matches)?; + + // Handle --from option + let filter = if let Some(from_group) = matches.get_one::<String>(options::FROM) { + match parse_gid_from_str(from_group) { + Ok(g) => IfFrom::Group(g), + Err(_) => { + return Err(USimpleError::new( + 1, + format!("invalid user: '{}'", from_group), + )) } } + } else { + IfFrom::All }; + Ok(GidUidOwnerFilter { dest_gid, dest_uid: None, raw_owner: raw_group, - filter: IfFrom::All, + filter, }) } @@ -120,6 +154,12 @@ pub fn uu_app() -> Command { .value_hint(clap::ValueHint::FilePath) .help("use RFILE's group rather than specifying GROUP values"), ) + .arg( + Arg::new(options::FROM) + .long(options::FROM) + .value_name("GROUP") + .help("change the group only if its current group matches GROUP"), + ) .arg( Arg::new(options::RECURSIVE) .short('R') diff --git a/src/uu/chmod/Cargo.toml b/src/uu/chmod/Cargo.toml index acfeb6c0409..70e856e099e 100644 --- a/src/uu/chmod/Cargo.toml +++ b/src/uu/chmod/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chmod" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "chmod ~ (uutils) change mode of FILE" @@ -19,7 +19,7 @@ path = "src/chmod.rs" [dependencies] clap = { workspace = true } libc = { workspace = true } -uucore = { workspace = true, features = ["fs", "mode", "perms"] } +uucore = { workspace = true, features = ["entries", "fs", "mode", "perms"] } [[bin]] name = "chmod" diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index c05288e213f..d2eb22ce6a6 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -16,6 +16,7 @@ use uucore::fs::display_permissions_unix; use uucore::libc::mode_t; #[cfg(not(windows))] use uucore::mode; +use uucore::perms::{configure_symlink_and_recursion, TraverseSymlinks}; use uucore::{format_usage, help_about, help_section, help_usage, show, show_error}; const ABOUT: &str = help_about!("chmod.md"); @@ -99,7 +100,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let quiet = matches.get_flag(options::QUIET); let verbose = matches.get_flag(options::VERBOSE); let preserve_root = matches.get_flag(options::PRESERVE_ROOT); - let recursive = matches.get_flag(options::RECURSIVE); let fmode = match matches.get_one::<String>(options::REFERENCE) { Some(fref) => match fs::metadata(fref) { Ok(meta) => Some(meta.mode() & 0o7777), @@ -138,6 +138,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { return Err(UUsageError::new(1, "missing operand".to_string())); } + let (recursive, dereference, traverse_symlinks) = configure_symlink_and_recursion(&matches)?; + let chmoder = Chmoder { changes, quiet, @@ -146,6 +148,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { recursive, fmode, cmode, + traverse_symlinks, + dereference, }; chmoder.chmod(&files) @@ -237,6 +241,8 @@ struct Chmoder { recursive: bool, fmode: Option<u32>, cmode: Option<String>, + traverse_symlinks: TraverseSymlinks, + dereference: bool, } impl Chmoder { @@ -248,12 +254,19 @@ impl Chmoder { let file = Path::new(filename); if !file.exists() { if file.is_symlink() { + if !self.dereference && !self.recursive { + // The file is a symlink and we should not follow it + // Don't try to change the mode of the symlink itself + continue; + } if !self.quiet { show!(USimpleError::new( 1, format!("cannot operate on dangling symlink {}", filename.quote()), )); + set_exit_code(1); } + if self.verbose { println!( "failed to change mode of {} from 0000 (---------) to 1500 (r-x-----T)", @@ -273,6 +286,11 @@ impl Chmoder { // So we set the exit code, because it hasn't been set yet if `self.quiet` is true. set_exit_code(1); continue; + } else if !self.dereference && file.is_symlink() { + // The file is a symlink and we should not follow it + // chmod 755 --no-dereference a/link + // should not change the permissions in this case + continue; } if self.recursive && self.preserve_root && filename == "/" { return Err(USimpleError::new( @@ -294,11 +312,23 @@ impl Chmoder { fn walk_dir(&self, file_path: &Path) -> UResult<()> { let mut r = self.chmod_file(file_path); - if !file_path.is_symlink() && file_path.is_dir() { + // Determine whether to traverse symlinks based on `self.traverse_symlinks` + let should_follow_symlink = match self.traverse_symlinks { + TraverseSymlinks::All => true, + TraverseSymlinks::First => { + file_path == file_path.canonicalize().unwrap_or(file_path.to_path_buf()) + } + TraverseSymlinks::None => false, + }; + + // If the path is a directory (or we should follow symlinks), recurse into it + if (!file_path.is_symlink() || should_follow_symlink) && file_path.is_dir() { for dir_entry in file_path.read_dir()? { let path = dir_entry?.path(); if !path.is_symlink() { r = self.walk_dir(path.as_path()); + } else if should_follow_symlink { + r = self.chmod_file(path.as_path()).and(r); } } } @@ -314,19 +344,22 @@ impl Chmoder { } #[cfg(unix)] fn chmod_file(&self, file: &Path) -> UResult<()> { - use uucore::mode::get_umask; + use uucore::{mode::get_umask, perms::get_metadata}; + + let metadata = get_metadata(file, self.dereference); - let fperm = match fs::metadata(file) { + let fperm = match metadata { Ok(meta) => meta.mode() & 0o7777, Err(err) => { - if file.is_symlink() { + // Handle dangling symlinks or other errors + if file.is_symlink() && !self.dereference { if self.verbose { println!( "neither symbolic link {} nor referent has been changed", file.quote() ); } - return Ok(()); + return Ok(()); // Skip dangling symlinks } else if err.kind() == std::io::ErrorKind::PermissionDenied { // These two filenames would normally be conditionally // quoted, but GNU's tests expect them to always be quoted @@ -339,6 +372,8 @@ impl Chmoder { } } }; + + // Determine the new permissions to apply match self.fmode { Some(mode) => self.change_file(fperm, mode, file)?, None => { diff --git a/src/uu/chown/Cargo.toml b/src/uu/chown/Cargo.toml index 0312d84b2a1..be05584b38e 100644 --- a/src/uu/chown/Cargo.toml +++ b/src/uu/chown/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chown" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "chown ~ (uutils) change the ownership of FILE" diff --git a/src/uu/chroot/Cargo.toml b/src/uu/chroot/Cargo.toml index 797b6246026..65fc9c8f310 100644 --- a/src/uu/chroot/Cargo.toml +++ b/src/uu/chroot/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_chroot" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "chroot ~ (uutils) run COMMAND under a new root directory" @@ -18,6 +18,7 @@ path = "src/chroot.rs" [dependencies] clap = { workspace = true } +thiserror = { workspace = true } uucore = { workspace = true, features = ["entries", "fs"] } [[bin]] diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index f348d0c554b..4ea5db65348 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -254,6 +254,7 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::GROUPS) .long(options::GROUPS) + .overrides_with(options::GROUPS) .help("Comma-separated list of groups to switch to") .value_name("GROUP1,GROUP2..."), ) diff --git a/src/uu/chroot/src/error.rs b/src/uu/chroot/src/error.rs index b8109d41910..e88f70760c0 100644 --- a/src/uu/chroot/src/error.rs +++ b/src/uu/chroot/src/error.rs @@ -4,59 +4,75 @@ // file that was distributed with this source code. // spell-checker:ignore NEWROOT Userspec userspec //! Errors returned by chroot. -use std::fmt::Display; use std::io::Error; +use thiserror::Error; use uucore::display::Quotable; use uucore::error::UError; use uucore::libc; /// Errors that can happen while executing chroot. -#[derive(Debug)] +#[derive(Debug, Error)] pub enum ChrootError { /// Failed to enter the specified directory. - CannotEnter(String, Error), + #[error("cannot chroot to {dir}: {err}", dir = .0.quote(), err = .1)] + CannotEnter(String, #[source] Error), /// Failed to execute the specified command. - CommandFailed(String, Error), + #[error("failed to run command {cmd}: {err}", cmd = .0.to_string().quote(), err = .1)] + CommandFailed(String, #[source] Error), /// Failed to find the specified command. - CommandNotFound(String, Error), + #[error("failed to run command {cmd}: {err}", cmd = .0.to_string().quote(), err = .1)] + CommandNotFound(String, #[source] Error), + #[error("--groups parsing failed")] GroupsParsingFailed, + #[error("invalid group: {group}", group = .0.quote())] InvalidGroup(String), + #[error("invalid group list: {list}", list = .0.quote())] InvalidGroupList(String), /// The given user and group specification was invalid. + #[error("invalid userspec: {spec}", spec = .0.quote())] InvalidUserspec(String), /// The new root directory was not given. + #[error( + "Missing operand: NEWROOT\nTry '{0} --help' for more information.", + uucore::execution_phrase() + )] MissingNewRoot, + #[error("no group specified for unknown uid: {0}")] NoGroupSpecified(libc::uid_t), /// Failed to find the specified user. + #[error("invalid user")] NoSuchUser, /// Failed to find the specified group. + #[error("invalid group")] NoSuchGroup, /// The given directory does not exist. + #[error("cannot change root directory to {dir}: no such directory", dir = .0.quote())] NoSuchDirectory(String), /// The call to `setgid()` failed. - SetGidFailed(String, Error), + #[error("cannot set gid to {gid}: {err}", gid = .0, err = .1)] + SetGidFailed(String, #[source] Error), /// The call to `setgroups()` failed. + #[error("cannot set groups: {0}")] SetGroupsFailed(Error), /// The call to `setuid()` failed. - SetUserFailed(String, Error), + #[error("cannot set user to {user}: {err}", user = .0.maybe_quote(), err = .1)] + SetUserFailed(String, #[source] Error), } -impl std::error::Error for ChrootError {} - impl UError for ChrootError { // 125 if chroot itself fails // 126 if command is found but cannot be invoked @@ -69,36 +85,3 @@ impl UError for ChrootError { } } } - -impl Display for ChrootError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::CannotEnter(s, e) => write!(f, "cannot chroot to {}: {}", s.quote(), e,), - Self::CommandFailed(s, e) | Self::CommandNotFound(s, e) => { - write!(f, "failed to run command {}: {}", s.to_string().quote(), e,) - } - Self::GroupsParsingFailed => write!(f, "--groups parsing failed"), - Self::InvalidGroup(s) => write!(f, "invalid group: {}", s.quote()), - Self::InvalidGroupList(s) => write!(f, "invalid group list: {}", s.quote()), - Self::InvalidUserspec(s) => write!(f, "invalid userspec: {}", s.quote(),), - Self::MissingNewRoot => write!( - f, - "Missing operand: NEWROOT\nTry '{} --help' for more information.", - uucore::execution_phrase(), - ), - Self::NoGroupSpecified(uid) => write!(f, "no group specified for unknown uid: {}", uid), - Self::NoSuchUser => write!(f, "invalid user"), - Self::NoSuchGroup => write!(f, "invalid group"), - Self::NoSuchDirectory(s) => write!( - f, - "cannot change root directory to {}: no such directory", - s.quote(), - ), - Self::SetGidFailed(s, e) => write!(f, "cannot set gid to {s}: {e}"), - Self::SetGroupsFailed(e) => write!(f, "cannot set groups: {e}"), - Self::SetUserFailed(s, e) => { - write!(f, "cannot set user to {}: {}", s.maybe_quote(), e) - } - } - } -} diff --git a/src/uu/cksum/Cargo.toml b/src/uu/cksum/Cargo.toml index 18c1b2899f1..c8693190be7 100644 --- a/src/uu/cksum/Cargo.toml +++ b/src/uu/cksum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cksum" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "cksum ~ (uutils) display CRC and size of input" diff --git a/src/uu/cksum/cksum.md b/src/uu/cksum/cksum.md index 4b0d25f32c3..5ca83b40150 100644 --- a/src/uu/cksum/cksum.md +++ b/src/uu/cksum/cksum.md @@ -13,6 +13,7 @@ DIGEST determines the digest algorithm and default output format: - `sysv`: (equivalent to sum -s) - `bsd`: (equivalent to sum -r) - `crc`: (equivalent to cksum) +- `crc32b`: (only available through cksum) - `md5`: (equivalent to md5sum) - `sha1`: (equivalent to sha1sum) - `sha224`: (equivalent to sha224sum) diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index b9f74133814..cf95d1bd24a 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -13,8 +13,9 @@ use std::iter; use std::path::Path; use uucore::checksum::{ calculate_blake2b_length, detect_algo, digest_reader, perform_checksum_validation, - ChecksumError, ChecksumOptions, ALGORITHM_OPTIONS_BLAKE2B, ALGORITHM_OPTIONS_BSD, - ALGORITHM_OPTIONS_CRC, ALGORITHM_OPTIONS_SYSV, SUPPORTED_ALGORITHMS, + ChecksumError, ChecksumOptions, ChecksumVerbose, ALGORITHM_OPTIONS_BLAKE2B, + ALGORITHM_OPTIONS_BSD, ALGORITHM_OPTIONS_CRC, ALGORITHM_OPTIONS_CRC32B, ALGORITHM_OPTIONS_SYSV, + SUPPORTED_ALGORITHMS, }; use uucore::{ encoding, @@ -113,7 +114,10 @@ where } OutputFormat::Hexadecimal => sum_hex, OutputFormat::Base64 => match options.algo_name { - ALGORITHM_OPTIONS_CRC | ALGORITHM_OPTIONS_SYSV | ALGORITHM_OPTIONS_BSD => sum_hex, + ALGORITHM_OPTIONS_CRC + | ALGORITHM_OPTIONS_CRC32B + | ALGORITHM_OPTIONS_SYSV + | ALGORITHM_OPTIONS_BSD => sum_hex, _ => encoding::for_cksum::BASE64.encode(&hex::decode(sum_hex).unwrap()), }, }; @@ -140,7 +144,7 @@ where !not_file, String::new(), ), - ALGORITHM_OPTIONS_CRC => ( + ALGORITHM_OPTIONS_CRC | ALGORITHM_OPTIONS_CRC32B => ( format!("{sum} {sz}{}", if not_file { "" } else { " " }), !not_file, String::new(), @@ -289,7 +293,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { None => None, }; - if ["bsd", "crc", "sysv"].contains(&algo_name) && check { + if ["bsd", "crc", "sysv", "crc32b"].contains(&algo_name) && check { return Err(ChecksumError::AlgorithmNotSupportedWithCheck.into()); } @@ -319,13 +323,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { || iter::once(OsStr::new("-")).collect::<Vec<_>>(), |files| files.map(OsStr::new).collect::<Vec<_>>(), ); + + let verbose = ChecksumVerbose::new(status, quiet, warn); + let opts = ChecksumOptions { binary: binary_flag, ignore_missing, - quiet, - status, strict, - warn, + verbose, }; return perform_checksum_validation(files.iter().copied(), algo_option, length, opts); @@ -459,19 +464,22 @@ pub fn uu_app() -> Command { .short('w') .long("warn") .help("warn about improperly formatted checksum lines") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::STATUS, options::QUIET]), ) .arg( Arg::new(options::STATUS) .long("status") .help("don't output anything, status code shows success") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::WARN, options::QUIET]), ) .arg( Arg::new(options::QUIET) .long(options::QUIET) .help("don't print OK for each successfully verified file") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::WARN, options::STATUS]), ) .arg( Arg::new(options::IGNORE_MISSING) diff --git a/src/uu/comm/Cargo.toml b/src/uu/comm/Cargo.toml index 26d82dbecc6..ce250c554c3 100644 --- a/src/uu/comm/Cargo.toml +++ b/src/uu/comm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_comm" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "comm ~ (uutils) compare sorted inputs" @@ -18,7 +18,7 @@ path = "src/comm.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true } +uucore = { workspace = true, features = ["fs"] } [[bin]] name = "comm" diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index ae57b8bf8a0..e075830cb85 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -3,12 +3,13 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) delim mkdelim +// spell-checker:ignore (ToDO) delim mkdelim pairable use std::cmp::Ordering; use std::fs::{metadata, File}; -use std::io::{self, stdin, BufRead, BufReader, Stdin}; +use std::io::{self, stdin, BufRead, BufReader, Read, Stdin}; use uucore::error::{FromIo, UResult, USimpleError}; +use uucore::fs::paths_refer_to_same_file; use uucore::line_ending::LineEnding; use uucore::{format_usage, help_about, help_usage}; @@ -27,6 +28,30 @@ mod options { pub const FILE_2: &str = "FILE2"; pub const TOTAL: &str = "total"; pub const ZERO_TERMINATED: &str = "zero-terminated"; + pub const CHECK_ORDER: &str = "check-order"; + pub const NO_CHECK_ORDER: &str = "nocheck-order"; +} + +#[derive(Debug, Clone, Copy)] +enum FileNumber { + One, + Two, +} + +impl FileNumber { + fn as_str(&self) -> &'static str { + match self { + FileNumber::One => "1", + FileNumber::Two => "2", + } + } +} + +struct OrderChecker { + last_line: Vec<u8>, + file_num: FileNumber, + check_order: bool, + has_error: bool, } enum Input { @@ -60,7 +85,74 @@ impl LineReader { } } -fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) { +impl OrderChecker { + fn new(file_num: FileNumber, check_order: bool) -> Self { + Self { + last_line: Vec::new(), + file_num, + check_order, + has_error: false, + } + } + + fn verify_order(&mut self, current_line: &[u8]) -> bool { + if self.last_line.is_empty() { + self.last_line = current_line.to_vec(); + return true; + } + + let is_ordered = current_line >= &self.last_line; + if !is_ordered && !self.has_error { + eprintln!( + "comm: file {} is not in sorted order", + self.file_num.as_str() + ); + self.has_error = true; + } + + self.last_line = current_line.to_vec(); + is_ordered || !self.check_order + } +} + +// Check if two files are identical by comparing their contents +pub fn are_files_identical(path1: &str, path2: &str) -> io::Result<bool> { + // First compare file sizes + let metadata1 = std::fs::metadata(path1)?; + let metadata2 = std::fs::metadata(path2)?; + + if metadata1.len() != metadata2.len() { + return Ok(false); + } + + let file1 = File::open(path1)?; + let file2 = File::open(path2)?; + + let mut reader1 = BufReader::new(file1); + let mut reader2 = BufReader::new(file2); + + let mut buffer1 = [0; 8192]; + let mut buffer2 = [0; 8192]; + + loop { + let bytes1 = reader1.read(&mut buffer1)?; + let bytes2 = reader2.read(&mut buffer2)?; + + if bytes1 != bytes2 { + return Ok(false); + } + + if bytes1 == 0 { + return Ok(true); + } + + if buffer1[..bytes1] != buffer2[..bytes2] { + return Ok(false); + } + } +} + +fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) -> UResult<()> { let width_col_1 = usize::from(!opts.get_flag(options::COLUMN_1)); let width_col_2 = usize::from(!opts.get_flag(options::COLUMN_2)); @@ -76,6 +168,26 @@ fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) let mut total_col_2 = 0; let mut total_col_3 = 0; + let check_order = opts.get_flag(options::CHECK_ORDER); + let no_check_order = opts.get_flag(options::NO_CHECK_ORDER); + + // Determine if we should perform order checking + let should_check_order = !no_check_order + && (check_order + || if let (Some(file1), Some(file2)) = ( + opts.get_one::<String>(options::FILE_1), + opts.get_one::<String>(options::FILE_2), + ) { + !(paths_refer_to_same_file(file1, file2, true) + || are_files_identical(file1, file2).unwrap_or(false)) + } else { + true + }); + + let mut checker1 = OrderChecker::new(FileNumber::One, check_order); + let mut checker2 = OrderChecker::new(FileNumber::Two, check_order); + let mut input_error = false; + while na.is_ok() || nb.is_ok() { let ord = match (na.is_ok(), nb.is_ok()) { (false, true) => Ordering::Greater, @@ -91,6 +203,9 @@ fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) match ord { Ordering::Less => { + if should_check_order && !checker1.verify_order(ra) { + break; + } if !opts.get_flag(options::COLUMN_1) { print!("{}", String::from_utf8_lossy(ra)); } @@ -99,6 +214,9 @@ fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) total_col_1 += 1; } Ordering::Greater => { + if should_check_order && !checker2.verify_order(rb) { + break; + } if !opts.get_flag(options::COLUMN_2) { print!("{delim_col_2}{}", String::from_utf8_lossy(rb)); } @@ -107,6 +225,10 @@ fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) total_col_2 += 1; } Ordering::Equal => { + if should_check_order && (!checker1.verify_order(ra) || !checker2.verify_order(rb)) + { + break; + } if !opts.get_flag(options::COLUMN_3) { print!("{delim_col_3}{}", String::from_utf8_lossy(ra)); } @@ -117,12 +239,27 @@ fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) total_col_3 += 1; } } + + // Track if we've seen any order errors + if (checker1.has_error || checker2.has_error) && !input_error && !check_order { + input_error = true; + } } if opts.get_flag(options::TOTAL) { let line_ending = LineEnding::from_zero_flag(opts.get_flag(options::ZERO_TERMINATED)); print!("{total_col_1}{delim}{total_col_2}{delim}{total_col_3}{delim}total{line_ending}"); } + + if should_check_order && (checker1.has_error || checker2.has_error) { + // Print the input error message once at the end + if input_error { + eprintln!("comm: input is not in sorted order"); + } + Err(USimpleError::new(1, "")) + } else { + Ok(()) + } } fn open_file(name: &str, line_ending: LineEnding) -> io::Result<LineReader> { @@ -170,8 +307,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { "" => "\0", delim => delim, }; - comm(&mut f1, &mut f2, delim, &matches); - Ok(()) + + comm(&mut f1, &mut f2, delim, &matches) } pub fn uu_app() -> Command { @@ -233,4 +370,17 @@ pub fn uu_app() -> Command { .help("output a summary") .action(ArgAction::SetTrue), ) + .arg( + Arg::new(options::CHECK_ORDER) + .long(options::CHECK_ORDER) + .help("check that the input is correctly sorted, even if all input lines are pairable") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::NO_CHECK_ORDER) + .long(options::NO_CHECK_ORDER) + .help("do not check that the input is correctly sorted") + .action(ArgAction::SetTrue) + .conflicts_with(options::CHECK_ORDER), + ) } diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index 3912f3308a5..ebcd8ff877e 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cp" -version = "0.0.28" +version = "0.0.29" authors = [ "Jordy Dickinson <jordy.dickinson@gmail.com>", "Joshua S. Miller <jsmiller@uchicago.edu>", @@ -28,6 +28,7 @@ quick-error = { workspace = true } selinux = { workspace = true, optional = true } uucore = { workspace = true, features = [ "backup-control", + "buf-copy", "entries", "fs", "fsxattr", diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index c1ebc20bf80..626b65ad63e 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -10,7 +10,7 @@ use std::collections::{HashMap, HashSet}; #[cfg(not(windows))] use std::ffi::CString; use std::ffi::OsString; -use std::fs::{self, File, Metadata, OpenOptions, Permissions}; +use std::fs::{self, Metadata, OpenOptions, Permissions}; use std::io; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; @@ -932,6 +932,16 @@ impl Options { }; let update_mode = update_control::determine_update_mode(matches); + if backup_mode != BackupMode::NoBackup + && matches + .get_one::<String>(update_control::arguments::OPT_UPDATE) + .is_some_and(|v| v == "none" || v == "none-fail") + { + return Err(Error::InvalidArgument( + "--backup is mutually exclusive with -n or --update=none-fail".to_string(), + )); + } + let backup_suffix = backup_control::determine_backup_suffix(matches); let overwrite = OverwriteMode::from_matches(matches); @@ -1953,6 +1963,7 @@ fn print_paths(parents: bool, source: &Path, dest: &Path) { /// /// * `Ok(())` - The file was copied successfully. /// * `Err(CopyError)` - An error occurred while copying the file. +#[allow(clippy::too_many_arguments)] fn handle_copy_mode( source: &Path, dest: &Path, @@ -1961,15 +1972,10 @@ fn handle_copy_mode( source_metadata: &Metadata, symlinked_files: &mut HashSet<FileInformation>, source_in_command_line: bool, + source_is_fifo: bool, + #[cfg(unix)] source_is_stream: bool, ) -> CopyResult<()> { - let source_file_type = source_metadata.file_type(); - - let source_is_symlink = source_file_type.is_symlink(); - - #[cfg(unix)] - let source_is_fifo = source_file_type.is_fifo(); - #[cfg(not(unix))] - let source_is_fifo = false; + let source_is_symlink = source_metadata.is_symlink(); match options.copy_mode { CopyMode::Link => { @@ -2006,6 +2012,8 @@ fn handle_copy_mode( source_is_symlink, source_is_fifo, symlinked_files, + #[cfg(unix)] + source_is_stream, )?; } CopyMode::SymLink => { @@ -2026,6 +2034,8 @@ fn handle_copy_mode( source_is_symlink, source_is_fifo, symlinked_files, + #[cfg(unix)] + source_is_stream, )?; } update_control::UpdateMode::ReplaceNone => { @@ -2056,6 +2066,8 @@ fn handle_copy_mode( source_is_symlink, source_is_fifo, symlinked_files, + #[cfg(unix)] + source_is_stream, )?; } } @@ -2069,6 +2081,8 @@ fn handle_copy_mode( source_is_symlink, source_is_fifo, symlinked_files, + #[cfg(unix)] + source_is_stream, )?; } } @@ -2295,6 +2309,18 @@ fn copy_file( let dest_permissions = calculate_dest_permissions(dest, &source_metadata, options, context)?; + #[cfg(unix)] + let source_is_fifo = source_metadata.file_type().is_fifo(); + #[cfg(not(unix))] + let source_is_fifo = false; + + #[cfg(unix)] + let source_is_stream = source_is_fifo + || source_metadata.file_type().is_char_device() + || source_metadata.file_type().is_block_device(); + #[cfg(not(unix))] + let source_is_stream = false; + handle_copy_mode( source, dest, @@ -2303,6 +2329,9 @@ fn copy_file( &source_metadata, symlinked_files, source_in_command_line, + source_is_fifo, + #[cfg(unix)] + source_is_stream, )?; // TODO: implement something similar to gnu's lchown @@ -2318,8 +2347,16 @@ fn copy_file( if options.dereference(source_in_command_line) { if let Ok(src) = canonicalize(source, MissingHandling::Normal, ResolveMode::Physical) { - copy_attributes(&src, dest, &options.attributes)?; + if src.exists() { + copy_attributes(&src, dest, &options.attributes)?; + } } + } else if source_is_stream && source.exists() { + // Some stream files may not exist after we have copied it, + // like anonymous pipes. Thus, we can't really copy its + // attributes. However, this is already handled in the stream + // copy function (see `copy_stream` under platform/linux.rs). + copy_attributes(source, dest, &options.attributes)?; } else { copy_attributes(source, dest, &options.attributes)?; } @@ -2383,6 +2420,7 @@ fn handle_no_preserve_mode(options: &Options, org_mode: u32) -> u32 { /// Copy the file from `source` to `dest` either using the normal `fs::copy` or a /// copy-on-write scheme if --reflink is specified and the filesystem supports it. +#[allow(clippy::too_many_arguments)] fn copy_helper( source: &Path, dest: &Path, @@ -2391,6 +2429,7 @@ fn copy_helper( source_is_symlink: bool, source_is_fifo: bool, symlinked_files: &mut HashSet<FileInformation>, + #[cfg(unix)] source_is_stream: bool, ) -> CopyResult<()> { if options.parents { let parent = dest.parent().unwrap_or(dest); @@ -2401,12 +2440,7 @@ fn copy_helper( return Err(Error::NotADirectory(dest.to_path_buf())); } - if source.as_os_str() == "/dev/null" { - /* workaround a limitation of fs::copy - * https://github.com/rust-lang/rust/issues/79390 - */ - File::create(dest).context(dest.display().to_string())?; - } else if source_is_fifo && options.recursive && !options.copy_contents { + if source_is_fifo && options.recursive && !options.copy_contents { #[cfg(unix)] copy_fifo(dest, options.overwrite, options.debug)?; } else if source_is_symlink { @@ -2418,8 +2452,10 @@ fn copy_helper( options.reflink_mode, options.sparse_mode, context, - #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] + #[cfg(unix)] source_is_fifo, + #[cfg(unix)] + source_is_stream, )?; if !options.attributes_only && options.debug { diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs index 949bd5e03c7..0ca39a75ef2 100644 --- a/src/uu/cp/src/platform/linux.rs +++ b/src/uu/cp/src/platform/linux.rs @@ -12,6 +12,7 @@ use std::os::unix::fs::MetadataExt; use std::os::unix::fs::{FileTypeExt, OpenOptionsExt}; use std::os::unix::io::AsRawFd; use std::path::Path; +use uucore::buf_copy; use quick_error::ResultExt; @@ -220,8 +221,9 @@ fn check_dest_is_fifo(dest: &Path) -> bool { } } -/// Copy the contents of the given source FIFO to the given file. -fn copy_fifo_contents<P>(source: P, dest: P) -> std::io::Result<u64> +/// Copy the contents of a stream from `source` to `dest`. The `if_fifo` argument is used to +/// determine if we need to modify the file's attributes before and after copying. +fn copy_stream<P>(source: P, dest: P, is_fifo: bool) -> std::io::Result<u64> where P: AsRef<Path>, { @@ -250,8 +252,14 @@ where .write(true) .mode(mode) .open(&dest)?; - let num_bytes_copied = std::io::copy(&mut src_file, &mut dst_file)?; - dst_file.set_permissions(src_file.metadata()?.permissions())?; + + let num_bytes_copied = buf_copy::copy_stream(&mut src_file, &mut dst_file) + .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other))?; + + if is_fifo { + dst_file.set_permissions(src_file.metadata()?.permissions())?; + } + Ok(num_bytes_copied) } @@ -268,6 +276,7 @@ pub(crate) fn copy_on_write( sparse_mode: SparseMode, context: &str, source_is_fifo: bool, + source_is_stream: bool, ) -> CopyResult<CopyDebug> { let mut copy_debug = CopyDebug { offload: OffloadReflinkDebug::Unknown, @@ -279,10 +288,9 @@ pub(crate) fn copy_on_write( copy_debug.sparse_detection = SparseDebug::Zeros; // Default SparseDebug val for SparseMode::Always copy_debug.reflink = OffloadReflinkDebug::No; - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Avoided; - - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let mut copy_method = CopyMethod::Default; let result = handle_reflink_never_sparse_always(source, dest); @@ -300,10 +308,9 @@ pub(crate) fn copy_on_write( (ReflinkMode::Never, SparseMode::Never) => { copy_debug.reflink = OffloadReflinkDebug::No; - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Avoided; - - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let result = handle_reflink_never_sparse_never(source); if let Ok(debug) = result { @@ -315,9 +322,9 @@ pub(crate) fn copy_on_write( (ReflinkMode::Never, SparseMode::Auto) => { copy_debug.reflink = OffloadReflinkDebug::No; - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Avoided; - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let mut copy_method = CopyMethod::Default; let result = handle_reflink_never_sparse_auto(source, dest); @@ -335,10 +342,9 @@ pub(crate) fn copy_on_write( (ReflinkMode::Auto, SparseMode::Always) => { copy_debug.sparse_detection = SparseDebug::Zeros; // Default SparseDebug val for // SparseMode::Always - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Avoided; - - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let mut copy_method = CopyMethod::Default; let result = handle_reflink_auto_sparse_always(source, dest); @@ -356,9 +362,9 @@ pub(crate) fn copy_on_write( (ReflinkMode::Auto, SparseMode::Never) => { copy_debug.reflink = OffloadReflinkDebug::No; - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Avoided; - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let result = handle_reflink_auto_sparse_never(source); if let Ok(debug) = result { @@ -369,9 +375,9 @@ pub(crate) fn copy_on_write( } } (ReflinkMode::Auto, SparseMode::Auto) => { - if source_is_fifo { + if source_is_stream { copy_debug.offload = OffloadReflinkDebug::Unsupported; - copy_fifo_contents(source, dest).map(|_| ()) + copy_stream(source, dest, source_is_fifo).map(|_| ()) } else { let mut copy_method = CopyMethod::Default; let result = handle_reflink_auto_sparse_auto(source, dest); diff --git a/src/uu/cp/src/platform/macos.rs b/src/uu/cp/src/platform/macos.rs index 77bdbbbdb83..988dc6b2536 100644 --- a/src/uu/cp/src/platform/macos.rs +++ b/src/uu/cp/src/platform/macos.rs @@ -4,12 +4,14 @@ // file that was distributed with this source code. // spell-checker:ignore reflink use std::ffi::CString; -use std::fs::{self, File}; -use std::io; +use std::fs::{self, File, OpenOptions}; use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::OpenOptionsExt; use std::path::Path; use quick_error::ResultExt; +use uucore::buf_copy; +use uucore::mode::get_umask; use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; @@ -24,6 +26,7 @@ pub(crate) fn copy_on_write( sparse_mode: SparseMode, context: &str, source_is_fifo: bool, + source_is_stream: bool, ) -> CopyResult<CopyDebug> { if sparse_mode != SparseMode::Auto { return Err("--sparse is only supported on linux".to_string().into()); @@ -64,7 +67,7 @@ pub(crate) fn copy_on_write( // clonefile(2) fails if the destination exists. Remove it and try again. Do not // bother to check if removal worked because we're going to try to clone again. // first lets make sure the dest file is not read only - if fs::metadata(dest).map_or(false, |md| !md.permissions().readonly()) { + if fs::metadata(dest).is_ok_and(|md| !md.permissions().readonly()) { // remove and copy again // TODO: rewrite this to better match linux behavior // linux first opens the source file and destination file then uses the file @@ -85,10 +88,23 @@ pub(crate) fn copy_on_write( } _ => { copy_debug.reflink = OffloadReflinkDebug::Yes; - if source_is_fifo { + if source_is_stream { let mut src_file = File::open(source)?; - let mut dst_file = File::create(dest)?; - io::copy(&mut src_file, &mut dst_file).context(context)? + let mode = 0o622 & !get_umask(); + let mut dst_file = OpenOptions::new() + .create(true) + .write(true) + .mode(mode) + .open(dest)?; + + let context = buf_copy::copy_stream(&mut src_file, &mut dst_file) + .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other)) + .context(context)?; + + if source_is_fifo { + dst_file.set_permissions(src_file.metadata()?.permissions())?; + } + context } else { fs::copy(source, dest).context(context)? } diff --git a/src/uu/cp/src/platform/mod.rs b/src/uu/cp/src/platform/mod.rs index c7942706868..2071e928f41 100644 --- a/src/uu/cp/src/platform/mod.rs +++ b/src/uu/cp/src/platform/mod.rs @@ -2,6 +2,18 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. + +#[cfg(all( + unix, + not(any(target_os = "macos", target_os = "linux", target_os = "android")) +))] +mod other_unix; +#[cfg(all( + unix, + not(any(target_os = "macos", target_os = "linux", target_os = "android")) +))] +pub(crate) use self::other_unix::copy_on_write; + #[cfg(target_os = "macos")] mod macos; #[cfg(target_os = "macos")] @@ -12,7 +24,13 @@ mod linux; #[cfg(any(target_os = "linux", target_os = "android"))] pub(crate) use self::linux::copy_on_write; -#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] +#[cfg(not(any( + unix, + any(target_os = "macos", target_os = "linux", target_os = "android") +)))] mod other; -#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] +#[cfg(not(any( + unix, + any(target_os = "macos", target_os = "linux", target_os = "android") +)))] pub(crate) use self::other::copy_on_write; diff --git a/src/uu/cp/src/platform/other_unix.rs b/src/uu/cp/src/platform/other_unix.rs new file mode 100644 index 00000000000..aa8fed3fab1 --- /dev/null +++ b/src/uu/cp/src/platform/other_unix.rs @@ -0,0 +1,62 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore reflink +use std::fs::{self, File, OpenOptions}; +use std::os::unix::fs::OpenOptionsExt; +use std::path::Path; + +use quick_error::ResultExt; +use uucore::buf_copy; +use uucore::mode::get_umask; + +use crate::{CopyDebug, CopyResult, OffloadReflinkDebug, ReflinkMode, SparseDebug, SparseMode}; + +/// Copies `source` to `dest` for systems without copy-on-write +pub(crate) fn copy_on_write( + source: &Path, + dest: &Path, + reflink_mode: ReflinkMode, + sparse_mode: SparseMode, + context: &str, + source_is_fifo: bool, + source_is_stream: bool, +) -> CopyResult<CopyDebug> { + if reflink_mode != ReflinkMode::Never { + return Err("--reflink is only supported on linux and macOS" + .to_string() + .into()); + } + if sparse_mode != SparseMode::Auto { + return Err("--sparse is only supported on linux".to_string().into()); + } + let copy_debug = CopyDebug { + offload: OffloadReflinkDebug::Unsupported, + reflink: OffloadReflinkDebug::Unsupported, + sparse_detection: SparseDebug::Unsupported, + }; + + if source_is_stream { + let mut src_file = File::open(source)?; + let mode = 0o622 & !get_umask(); + let mut dst_file = OpenOptions::new() + .create(true) + .write(true) + .mode(mode) + .open(dest)?; + + buf_copy::copy_stream(&mut src_file, &mut dst_file) + .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other)) + .context(context)?; + + if source_is_fifo { + dst_file.set_permissions(src_file.metadata()?.permissions())?; + } + return Ok(copy_debug); + } + + fs::copy(source, dest).context(context)?; + + Ok(copy_debug) +} diff --git a/src/uu/csplit/Cargo.toml b/src/uu/csplit/Cargo.toml index b2771d8fe52..ec726e9d2b2 100644 --- a/src/uu/csplit/Cargo.toml +++ b/src/uu/csplit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_csplit" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "csplit ~ (uutils) Output pieces of FILE separated by PATTERN(s) to files 'xx00', 'xx01', ..., and output byte counts of each piece to standard output" diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index 0602f0deec7..501f97582ec 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -16,7 +16,7 @@ use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use regex::Regex; use uucore::display::Quotable; use uucore::error::{FromIo, UResult}; -use uucore::{crash_if_err, format_usage, help_about, help_section, help_usage}; +use uucore::{format_usage, help_about, help_section, help_usage}; mod csplit_error; mod patterns; @@ -51,26 +51,23 @@ pub struct CsplitOptions { } impl CsplitOptions { - fn new(matches: &ArgMatches) -> Self { + fn new(matches: &ArgMatches) -> Result<Self, CsplitError> { let keep_files = matches.get_flag(options::KEEP_FILES); let quiet = matches.get_flag(options::QUIET); let elide_empty_files = matches.get_flag(options::ELIDE_EMPTY_FILES); let suppress_matched = matches.get_flag(options::SUPPRESS_MATCHED); - Self { - split_name: crash_if_err!( - 1, - SplitName::new( - matches.get_one::<String>(options::PREFIX).cloned(), - matches.get_one::<String>(options::SUFFIX_FORMAT).cloned(), - matches.get_one::<String>(options::DIGITS).cloned() - ) - ), + Ok(Self { + split_name: SplitName::new( + matches.get_one::<String>(options::PREFIX).cloned(), + matches.get_one::<String>(options::SUFFIX_FORMAT).cloned(), + matches.get_one::<String>(options::DIGITS).cloned(), + )?, keep_files, quiet, elide_empty_files, suppress_matched, - } + }) } } @@ -90,7 +87,11 @@ pub fn csplit<T>(options: &CsplitOptions, patterns: &[String], input: T) -> Resu where T: BufRead, { - let mut input_iter = InputSplitter::new(input.lines().enumerate()); + let enumerated_input_lines = input + .lines() + .map(|line| line.map_err_context(|| "read error".to_string())) + .enumerate(); + let mut input_iter = InputSplitter::new(enumerated_input_lines); let mut split_writer = SplitWriter::new(options); let patterns: Vec<patterns::Pattern> = patterns::get_patterns(patterns)?; let ret = do_csplit(&mut split_writer, patterns, &mut input_iter); @@ -120,7 +121,7 @@ fn do_csplit<I>( input_iter: &mut InputSplitter<I>, ) -> Result<(), CsplitError> where - I: Iterator<Item = (usize, io::Result<String>)>, + I: Iterator<Item = (usize, UResult<String>)>, { // split the file based on patterns for pattern in patterns { @@ -308,7 +309,7 @@ impl SplitWriter<'_> { input_iter: &mut InputSplitter<I>, ) -> Result<(), CsplitError> where - I: Iterator<Item = (usize, io::Result<String>)>, + I: Iterator<Item = (usize, UResult<String>)>, { input_iter.rewind_buffer(); input_iter.set_size_of_buffer(1); @@ -361,7 +362,7 @@ impl SplitWriter<'_> { input_iter: &mut InputSplitter<I>, ) -> Result<(), CsplitError> where - I: Iterator<Item = (usize, io::Result<String>)>, + I: Iterator<Item = (usize, UResult<String>)>, { if offset >= 0 { // The offset is zero or positive, no need for a buffer on the lines read. @@ -375,6 +376,7 @@ impl SplitWriter<'_> { while let Some((ln, line)) = input_iter.next() { let l = line?; if regex.is_match(&l) { + let mut next_line_suppress_matched = false; match (self.options.suppress_matched, offset) { // no offset, add the line to the next split (false, 0) => { @@ -385,6 +387,11 @@ impl SplitWriter<'_> { } // a positive offset, some more lines need to be added to the current split (false, _) => self.writeln(&l)?, + // suppress matched option true, but there is a positive offset, so the line is printed + (true, 1..) => { + next_line_suppress_matched = true; + self.writeln(&l)?; + } _ => (), }; offset -= 1; @@ -405,6 +412,11 @@ impl SplitWriter<'_> { offset -= 1; } self.finish_split(); + + // if we have to suppress one line after we take the next and do nothing + if next_line_suppress_matched { + input_iter.next(); + } return Ok(()); } self.writeln(&l)?; @@ -423,7 +435,12 @@ impl SplitWriter<'_> { for line in input_iter.shrink_buffer_to_size() { self.writeln(&line)?; } - if !self.options.suppress_matched { + if self.options.suppress_matched { + // since offset_usize is for sure greater than 0 + // the first element of the buffer should be removed and this + // line inserted to be coherent with GNU implementation + input_iter.add_line_to_buffer(ln, l); + } else { // add 1 to the buffer size to make place for the matched line input_iter.set_size_of_buffer(offset_usize + 1); assert!( @@ -431,6 +448,7 @@ impl SplitWriter<'_> { "should be big enough to hold every lines" ); } + self.finish_split(); if input_iter.buffer_len() < offset_usize { return Err(CsplitError::LineOutOfRange(pattern_as_str.to_string())); @@ -456,7 +474,7 @@ impl SplitWriter<'_> { /// This is used to pass matching lines to the next split and to support patterns with a negative offset. struct InputSplitter<I> where - I: Iterator<Item = (usize, io::Result<String>)>, + I: Iterator<Item = (usize, UResult<String>)>, { iter: I, buffer: Vec<<I as Iterator>::Item>, @@ -469,7 +487,7 @@ where impl<I> InputSplitter<I> where - I: Iterator<Item = (usize, io::Result<String>)>, + I: Iterator<Item = (usize, UResult<String>)>, { fn new(iter: I) -> Self { Self { @@ -533,7 +551,7 @@ where impl<I> Iterator for InputSplitter<I> where - I: Iterator<Item = (usize, io::Result<String>)>, + I: Iterator<Item = (usize, UResult<String>)>, { type Item = <I as Iterator>::Item; @@ -561,19 +579,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .unwrap() .map(|s| s.to_string()) .collect(); - let options = CsplitOptions::new(&matches); + let options = CsplitOptions::new(&matches)?; if file_name == "-" { let stdin = io::stdin(); Ok(csplit(&options, &patterns, stdin.lock())?) } else { let file = File::open(file_name) - .map_err_context(|| format!("cannot access {}", file_name.quote()))?; - let file_metadata = file - .metadata() - .map_err_context(|| format!("cannot access {}", file_name.quote()))?; - if !file_metadata.is_file() { - return Err(CsplitError::NotRegularFile(file_name.to_string()).into()); - } + .map_err_context(|| format!("cannot open {} for reading", file_name.quote()))?; Ok(csplit(&options, &patterns, BufReader::new(file))?) } } diff --git a/src/uu/csplit/src/csplit_error.rs b/src/uu/csplit/src/csplit_error.rs index 4a83b637b07..ac1c8d01c48 100644 --- a/src/uu/csplit/src/csplit_error.rs +++ b/src/uu/csplit/src/csplit_error.rs @@ -35,6 +35,8 @@ pub enum CsplitError { SuffixFormatTooManyPercents, #[error("{} is not a regular file", ._0.quote())] NotRegularFile(String), + #[error("{}", _0)] + UError(Box<dyn UError>), } impl From<io::Error> for CsplitError { @@ -43,8 +45,17 @@ impl From<io::Error> for CsplitError { } } +impl From<Box<dyn UError>> for CsplitError { + fn from(error: Box<dyn UError>) -> Self { + Self::UError(error) + } +} + impl UError for CsplitError { fn code(&self) -> i32 { - 1 + match self { + Self::UError(e) => e.code(), + _ => 1, + } } } diff --git a/src/uu/cut/Cargo.toml b/src/uu/cut/Cargo.toml index 33046e77321..4a41b4fac4f 100644 --- a/src/uu/cut/Cargo.toml +++ b/src/uu/cut/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_cut" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "cut ~ (uutils) display byte/field columns of input lines" diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index a99f284713a..71f52250742 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore datetime [package] name = "uu_date" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "date ~ (uutils) display or set the current time" @@ -20,7 +20,7 @@ path = "src/date.rs" [dependencies] chrono = { workspace = true } clap = { workspace = true } -uucore = { workspace = true } +uucore = { workspace = true, features = ["custom-tz-fmt"] } parse_datetime = { workspace = true } [target.'cfg(unix)'.dependencies] diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 766e79bd497..a3f2ad0426c 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -15,6 +15,7 @@ use libc::{clock_settime, timespec, CLOCK_REALTIME}; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; +use uucore::custom_tz_fmt::custom_time_format; use uucore::display::Quotable; use uucore::error::FromIo; use uucore::error::{UResult, USimpleError}; @@ -272,8 +273,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { for date in dates { match date { Ok(date) => { - // GNU `date` uses `%N` for nano seconds, however crate::chrono uses `%f` - let format_string = &format_string.replace("%N", "%f"); + let format_string = custom_time_format(format_string); // Refuse to pass this string to chrono as it is crashing in this crate if format_string.contains("%#z") { return Err(USimpleError::new( @@ -283,7 +283,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } // Hack to work around panic in chrono, // TODO - remove when a fix for https://github.com/chronotope/chrono/issues/623 is released - let format_items = StrftimeItems::new(format_string); + let format_items = StrftimeItems::new(format_string.as_str()); if format_items.clone().any(|i| i == Item::Error) { return Err(USimpleError::new( 1, @@ -403,7 +403,7 @@ fn make_format_string(settings: &Settings) -> &str { Rfc3339Format::Ns => "%F %T.%f%:z", }, Format::Custom(ref fmt) => fmt, - Format::Default => "%c", + Format::Default => "%a %b %e %X %Z %Y", } } diff --git a/src/uu/dd/Cargo.toml b/src/uu/dd/Cargo.toml index 5bc117ea7cd..ceb85dcc881 100644 --- a/src/uu/dd/Cargo.toml +++ b/src/uu/dd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dd" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "dd ~ (uutils) copy and convert files" diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index ca8c2a8b570..aaa4684617a 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -54,7 +54,7 @@ use nix::{ }; use uucore::display::Quotable; #[cfg(unix)] -use uucore::error::set_exit_code; +use uucore::error::{set_exit_code, USimpleError}; use uucore::error::{FromIo, UResult}; #[cfg(target_os = "linux")] use uucore::show_if_err; @@ -338,11 +338,11 @@ impl<'a> Input<'a> { let mut src = Source::stdin_as_file(); #[cfg(unix)] if let Source::StdinFile(f) = &src { - // GNU compatibility: - // this will check whether stdin points to a folder or not - if f.metadata()?.is_file() && settings.iflags.directory { - show_error!("standard input: not a directory"); - return Err(1.into()); + if settings.iflags.directory && !f.metadata()?.is_dir() { + return Err(USimpleError::new( + 1, + "setting flags for 'standard input': Not a directory", + )); } }; if settings.skip > 0 { diff --git a/src/uu/dd/src/parseargs.rs b/src/uu/dd/src/parseargs.rs index 59836b1a1e4..e26b3495316 100644 --- a/src/uu/dd/src/parseargs.rs +++ b/src/uu/dd/src/parseargs.rs @@ -517,9 +517,7 @@ fn parse_bytes_no_x(full: &str, s: &str) -> Result<u64, ParseError> { (None, None, None) => match parser.parse_u64(s) { Ok(n) => (n, 1), Err(ParseSizeError::SizeTooBig(_)) => (u64::MAX, 1), - Err(ParseSizeError::InvalidSuffix(_) | ParseSizeError::ParseFailure(_)) => { - return Err(ParseError::InvalidNumber(full.to_string())) - } + Err(_) => return Err(ParseError::InvalidNumber(full.to_string())), }, (Some(i), None, None) => (parse_bytes_only(s, i)?, 1), (None, Some(i), None) => (parse_bytes_only(s, i)?, 2), diff --git a/src/uu/df/Cargo.toml b/src/uu/df/Cargo.toml index 3eaff87f38e..7de8028108b 100644 --- a/src/uu/df/Cargo.toml +++ b/src/uu/df/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_df" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "df ~ (uutils) display file system information" diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 8ef84a46311..8602d8af7af 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -27,6 +27,7 @@ use std::path::Path; use crate::blocks::{read_block_size, BlockSize}; use crate::columns::{Column, ColumnError}; use crate::filesystem::Filesystem; +use crate::filesystem::FsError; use crate::table::Table; const ABOUT: &str = help_about!("df.md"); @@ -188,6 +189,7 @@ impl Options { .to_string(), ), ParseSizeError::ParseFailure(s) => OptionsError::InvalidBlockSize(s), + ParseSizeError::PhysicalMem(s) => OptionsError::InvalidBlockSize(s), })?, header_mode: { if matches.get_flag(OPT_HUMAN_READABLE_BINARY) @@ -350,11 +352,25 @@ fn get_all_filesystems(opt: &Options) -> UResult<Vec<Filesystem>> { // Convert each `MountInfo` into a `Filesystem`, which contains // both the mount information and usage information. - Ok(mounts - .into_iter() - .filter_map(|m| Filesystem::new(m, None)) - .filter(|fs| opt.show_all_fs || fs.usage.blocks > 0) - .collect()) + #[cfg(not(windows))] + { + let maybe_mount = |m| Filesystem::from_mount(&mounts, &m, None).ok(); + Ok(mounts + .clone() + .into_iter() + .filter_map(maybe_mount) + .filter(|fs| opt.show_all_fs || fs.usage.blocks > 0) + .collect()) + } + #[cfg(windows)] + { + let maybe_mount = |m| Filesystem::from_mount(&m, None).ok(); + Ok(mounts + .into_iter() + .filter_map(maybe_mount) + .filter(|fs| opt.show_all_fs || fs.usage.blocks > 0) + .collect()) + } } /// For each path, get the filesystem that contains that path. @@ -385,17 +401,25 @@ where // both the mount information and usage information. for path in paths { match Filesystem::from_path(&mounts, path) { - Some(fs) => result.push(fs), - None => { - // this happens if specified file system type != file system type of the file - if path.as_ref().exists() { - show!(USimpleError::new(1, "no file systems processed")); - } else { - show!(USimpleError::new( - 1, - format!("{}: No such file or directory", path.as_ref().display()) - )); - } + Ok(fs) => result.push(fs), + Err(FsError::InvalidPath) => { + show!(USimpleError::new( + 1, + format!("{}: No such file or directory", path.as_ref().display()) + )); + } + Err(FsError::MountMissing) => { + show!(USimpleError::new(1, "no file systems processed")); + } + #[cfg(not(windows))] + Err(FsError::OverMounted) => { + show!(USimpleError::new( + 1, + format!( + "cannot access {}: over-mounted by another device", + path.as_ref().quote() + ) + )); } } } diff --git a/src/uu/df/src/filesystem.rs b/src/uu/df/src/filesystem.rs index 5e86cf31781..401a3bec7b5 100644 --- a/src/uu/df/src/filesystem.rs +++ b/src/uu/df/src/filesystem.rs @@ -37,6 +37,33 @@ pub(crate) struct Filesystem { pub usage: FsUsage, } +#[derive(Debug, PartialEq)] +pub(crate) enum FsError { + #[cfg(not(windows))] + OverMounted, + InvalidPath, + MountMissing, +} + +/// Check whether `mount` has been over-mounted. +/// +/// `mount` is considered over-mounted if it there is an element in +/// `mounts` after mount that has the same mount_dir. +#[cfg(not(windows))] +fn is_over_mounted(mounts: &[MountInfo], mount: &MountInfo) -> bool { + let last_mount_for_dir = mounts + .iter() + .filter(|m| m.mount_dir == mount.mount_dir) + .next_back(); + + if let Some(lmi) = last_mount_for_dir { + lmi.dev_name != mount.dev_name + } else { + // Should be unreachable if `mount` is in `mounts` + false + } +} + /// Find the mount info that best matches a given filesystem path. /// /// This function returns the element of `mounts` on which `path` is @@ -56,14 +83,16 @@ fn mount_info_from_path<P>( path: P, // This is really only used for testing purposes. canonicalize: bool, -) -> Option<&MountInfo> +) -> Result<&MountInfo, FsError> where P: AsRef<Path>, { // TODO Refactor this function with `Stater::find_mount_point()` // in the `stat` crate. let path = if canonicalize { - path.as_ref().canonicalize().ok()? + path.as_ref() + .canonicalize() + .map_err(|_| FsError::InvalidPath)? } else { path.as_ref().to_path_buf() }; @@ -82,12 +111,14 @@ where .find(|m| m.1.eq(&path)) .map(|m| m.0); - maybe_mount_point.or_else(|| { - mounts - .iter() - .filter(|mi| path.starts_with(&mi.mount_dir)) - .max_by_key(|mi| mi.mount_dir.len()) - }) + maybe_mount_point + .or_else(|| { + mounts + .iter() + .filter(|mi| path.starts_with(&mi.mount_dir)) + .max_by_key(|mi| mi.mount_dir.len()) + }) + .ok_or(FsError::MountMissing) } impl Filesystem { @@ -117,6 +148,27 @@ impl Filesystem { }) } + /// Find and create the filesystem from the given mount + /// after checking that the it hasn't been over-mounted + #[cfg(not(windows))] + pub(crate) fn from_mount( + mounts: &[MountInfo], + mount: &MountInfo, + file: Option<String>, + ) -> Result<Self, FsError> { + if is_over_mounted(mounts, mount) { + Err(FsError::OverMounted) + } else { + Self::new(mount.clone(), file).ok_or(FsError::MountMissing) + } + } + + /// Find and create the filesystem from the given mount. + #[cfg(windows)] + pub(crate) fn from_mount(mount: &MountInfo, file: Option<String>) -> Result<Self, FsError> { + Self::new(mount.clone(), file).ok_or(FsError::MountMissing) + } + /// Find and create the filesystem that best matches a given path. /// /// This function returns a new `Filesystem` derived from the @@ -133,16 +185,18 @@ impl Filesystem { /// * [`Path::canonicalize`] /// * [`MountInfo::mount_dir`] /// - pub(crate) fn from_path<P>(mounts: &[MountInfo], path: P) -> Option<Self> + pub(crate) fn from_path<P>(mounts: &[MountInfo], path: P) -> Result<Self, FsError> where P: AsRef<Path>, { let file = path.as_ref().display().to_string(); let canonicalize = true; - let mount_info = mount_info_from_path(mounts, path, canonicalize)?; - // TODO Make it so that we do not need to clone the `mount_info`. - let mount_info = (*mount_info).clone(); - Self::new(mount_info, Some(file)) + + let result = mount_info_from_path(mounts, path, canonicalize); + #[cfg(windows)] + return result.and_then(|mount_info| Self::from_mount(mount_info, Some(file))); + #[cfg(not(windows))] + return result.and_then(|mount_info| Self::from_mount(mounts, mount_info, Some(file))); } } @@ -153,7 +207,7 @@ mod tests { use uucore::fsext::MountInfo; - use crate::filesystem::mount_info_from_path; + use crate::filesystem::{mount_info_from_path, FsError}; // Create a fake `MountInfo` with the given directory name. fn mount_info(mount_dir: &str) -> MountInfo { @@ -183,7 +237,19 @@ mod tests { #[test] fn test_empty_mounts() { - assert!(mount_info_from_path(&[], "/", false).is_none()); + assert_eq!( + mount_info_from_path(&[], "/", false).unwrap_err(), + FsError::MountMissing + ); + } + + #[test] + fn test_bad_path() { + assert_eq!( + // This path better not exist.... + mount_info_from_path(&[], "/non-existent-path", true).unwrap_err(), + FsError::InvalidPath + ); } #[test] @@ -210,13 +276,19 @@ mod tests { #[test] fn test_no_match() { let mounts = [mount_info("/foo")]; - assert!(mount_info_from_path(&mounts, "/bar", false).is_none()); + assert_eq!( + mount_info_from_path(&mounts, "/bar", false).unwrap_err(), + FsError::MountMissing + ); } #[test] fn test_partial_match() { let mounts = [mount_info("/foo/bar")]; - assert!(mount_info_from_path(&mounts, "/foo/baz", false).is_none()); + assert_eq!( + mount_info_from_path(&mounts, "/foo/baz", false).unwrap_err(), + FsError::MountMissing + ); } #[test] @@ -237,4 +309,52 @@ mod tests { assert!(mount_info_eq(actual, &mounts[0])); } } + + #[cfg(not(windows))] + mod over_mount { + use crate::filesystem::{is_over_mounted, Filesystem, FsError}; + use uucore::fsext::MountInfo; + + fn mount_info_with_dev_name(mount_dir: &str, dev_name: Option<&str>) -> MountInfo { + MountInfo { + dev_id: Default::default(), + dev_name: dev_name.map(String::from).unwrap_or_default(), + fs_type: Default::default(), + mount_dir: String::from(mount_dir), + mount_option: Default::default(), + mount_root: Default::default(), + remote: Default::default(), + dummy: Default::default(), + } + } + + #[test] + fn test_over_mount() { + let mount_info1 = mount_info_with_dev_name("/foo", Some("dev_name_1")); + let mount_info2 = mount_info_with_dev_name("/foo", Some("dev_name_2")); + let mounts = [mount_info1, mount_info2]; + assert!(is_over_mounted(&mounts, &mounts[0])); + } + + #[test] + fn test_over_mount_not_over_mounted() { + let mount_info1 = mount_info_with_dev_name("/foo", Some("dev_name_1")); + let mount_info2 = mount_info_with_dev_name("/foo", Some("dev_name_2")); + let mounts = [mount_info1, mount_info2]; + assert!(!is_over_mounted(&mounts, &mounts[1])); + } + + #[test] + fn test_from_mount_over_mounted() { + let mount_info1 = mount_info_with_dev_name("/foo", Some("dev_name_1")); + let mount_info2 = mount_info_with_dev_name("/foo", Some("dev_name_2")); + + let mounts = [mount_info1, mount_info2]; + + assert_eq!( + Filesystem::from_mount(&mounts, &mounts[0], None).unwrap_err(), + FsError::OverMounted + ); + } + } } diff --git a/src/uu/dir/Cargo.toml b/src/uu/dir/Cargo.toml index 73dea05f15a..cfcfc8e9c87 100644 --- a/src/uu/dir/Cargo.toml +++ b/src/uu/dir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dir" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "shortcut to ls -C -b" diff --git a/src/uu/dircolors/Cargo.toml b/src/uu/dircolors/Cargo.toml index 4fd6f95371c..085b6a75f9b 100644 --- a/src/uu/dircolors/Cargo.toml +++ b/src/uu/dircolors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dircolors" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "dircolors ~ (uutils) display commands to set LS_COLORS" diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index faef0683e71..180be5e255f 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -3,12 +3,11 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) clrtoeol dircolors eightbit endcode fnmatch leftcode multihardlink rightcode setenv sgid suid colorterm +// spell-checker:ignore (ToDO) clrtoeol dircolors eightbit endcode fnmatch leftcode multihardlink rightcode setenv sgid suid colorterm disp use std::borrow::Borrow; use std::env; use std::fs::File; -//use std::io::IsTerminal; use std::io::{BufRead, BufReader}; use std::path::Path; @@ -16,7 +15,7 @@ use clap::{crate_version, Arg, ArgAction, Command}; use uucore::colors::{FILE_ATTRIBUTE_CODES, FILE_COLORS, FILE_TYPES, TERMS}; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, UUsageError}; -use uucore::{help_about, help_section, help_usage}; +use uucore::{format_usage, help_about, help_section, help_usage, parse_glob}; mod options { pub const BOURNE_SHELL: &str = "bourne-shell"; @@ -359,9 +358,6 @@ enum ParseState { Pass, } -use uucore::{format_usage, parse_glob}; - -#[allow(clippy::cognitive_complexity)] fn parse<T>(user_input: T, fmt: &OutputFmt, fp: &str) -> Result<String, String> where T: IntoIterator, @@ -372,10 +368,12 @@ where result.push_str(&prefix); + // Get environment variables once at the start let term = env::var("TERM").unwrap_or_else(|_| "none".to_owned()); - let term = term.as_str(); + let colorterm = env::var("COLORTERM").unwrap_or_default(); let mut state = ParseState::Global; + let mut saw_colorterm_match = false; for (num, line) in user_input.into_iter().enumerate() { let num = num + 1; @@ -395,52 +393,38 @@ where num )); } + let lower = key.to_lowercase(); - if lower == "term" || lower == "colorterm" { - if term.fnmatch(val) { - state = ParseState::Matched; - } else if state != ParseState::Matched { - state = ParseState::Pass; - } - } else { - if state == ParseState::Matched { - // prevent subsequent mismatched TERM from - // cancelling the input - state = ParseState::Continue; + match lower.as_str() { + "term" => { + if term.fnmatch(val) { + state = ParseState::Matched; + } else if state == ParseState::Global { + state = ParseState::Pass; + } } - if state != ParseState::Pass { - let search_key = lower.as_str(); - - if key.starts_with('.') { - if *fmt == OutputFmt::Display { - result.push_str(format!("\x1b[{val}m*{key}\t{val}\x1b[0m\n").as_str()); - } else { - result.push_str(format!("*{key}={val}:").as_str()); - } - } else if key.starts_with('*') { - if *fmt == OutputFmt::Display { - result.push_str(format!("\x1b[{val}m{key}\t{val}\x1b[0m\n").as_str()); - } else { - result.push_str(format!("{key}={val}:").as_str()); - } - } else if lower == "options" || lower == "color" || lower == "eightbit" { - // Slackware only. Ignore - } else if let Some((_, s)) = FILE_ATTRIBUTE_CODES - .iter() - .find(|&&(key, _)| key == search_key) - { - if *fmt == OutputFmt::Display { - result.push_str(format!("\x1b[{val}m{s}\t{val}\x1b[0m\n").as_str()); - } else { - result.push_str(format!("{s}={val}:").as_str()); - } + "colorterm" => { + // For COLORTERM ?*, only match if COLORTERM is non-empty + let matches = if val == "?*" { + !colorterm.is_empty() } else { - return Err(format!( - "{}:{}: unrecognized keyword {}", - fp.maybe_quote(), - num, - key - )); + colorterm.fnmatch(val) + }; + if matches { + state = ParseState::Matched; + saw_colorterm_match = true; + } else if !saw_colorterm_match && state == ParseState::Global { + state = ParseState::Pass; + } + } + _ => { + if state == ParseState::Matched { + // prevent subsequent mismatched TERM from + // cancelling the input + state = ParseState::Continue; + } + if state != ParseState::Pass { + append_entry(&mut result, fmt, key, &lower, val)?; } } } @@ -455,6 +439,46 @@ where Ok(result) } +fn append_entry( + result: &mut String, + fmt: &OutputFmt, + key: &str, + lower: &str, + val: &str, +) -> Result<(), String> { + if key.starts_with(['.', '*']) { + let entry = if key.starts_with('.') { + format!("*{key}") + } else { + key.to_string() + }; + let disp = if *fmt == OutputFmt::Display { + format!("\x1b[{val}m{entry}\t{val}\x1b[0m\n") + } else { + format!("{entry}={val}:") + }; + result.push_str(&disp); + return Ok(()); + } + + match lower { + "options" | "color" | "eightbit" => Ok(()), // Slackware only, ignore + _ => { + if let Some((_, s)) = FILE_ATTRIBUTE_CODES.iter().find(|&&(key, _)| key == lower) { + let disp = if *fmt == OutputFmt::Display { + format!("\x1b[{val}m{s}\t{val}\x1b[0m\n") + } else { + format!("{s}={val}:") + }; + result.push_str(&disp); + Ok(()) + } else { + Err(format!("unrecognized keyword {key}")) + } + } + } +} + /// Escape single quotes because they are not allowed between single quotes in shell code, and code /// enclosed by single quotes is what is returned by `parse()`. /// diff --git a/src/uu/dirname/Cargo.toml b/src/uu/dirname/Cargo.toml index ac160ffe975..a53930d7e40 100644 --- a/src/uu/dirname/Cargo.toml +++ b/src/uu/dirname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_dirname" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "dirname ~ (uutils) display parent directory of PATHNAME" diff --git a/src/uu/du/Cargo.toml b/src/uu/du/Cargo.toml index 5b9a707da50..27ec1700a5f 100644 --- a/src/uu/du/Cargo.toml +++ b/src/uu/du/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_du" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "du ~ (uutils) display disk usage" diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 2392497a935..bd017f1d515 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -1120,7 +1120,9 @@ fn format_error_message(error: &ParseSizeError, s: &str, option: &str) -> String ParseSizeError::InvalidSuffix(_) => { format!("invalid suffix in --{} argument {}", option, s.quote()) } - ParseSizeError::ParseFailure(_) => format!("invalid --{} argument {}", option, s.quote()), + ParseSizeError::ParseFailure(_) | ParseSizeError::PhysicalMem(_) => { + format!("invalid --{} argument {}", option, s.quote()) + } ParseSizeError::SizeTooBig(_) => format!("--{} argument {} too large", option, s.quote()), } } diff --git a/src/uu/echo/Cargo.toml b/src/uu/echo/Cargo.toml index faabc121df1..f3a7a6400b7 100644 --- a/src/uu/echo/Cargo.toml +++ b/src/uu/echo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_echo" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "echo ~ (uutils) display TEXT" diff --git a/src/uu/echo/src/echo.rs b/src/uu/echo/src/echo.rs index 097e4f2e980..228b5a0c123 100644 --- a/src/uu/echo/src/echo.rs +++ b/src/uu/echo/src/echo.rs @@ -4,8 +4,8 @@ // file that was distributed with this source code. use clap::builder::ValueParser; -use clap::parser::ValuesRef; -use clap::{crate_version, Arg, ArgAction, Command}; +use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; +use std::env; use std::ffi::{OsStr, OsString}; use std::io::{self, StdoutLock, Write}; use std::iter::Peekable; @@ -272,35 +272,37 @@ fn handle_double_hyphens(args: impl uucore::Args) -> impl uucore::Args { result.into_iter() } +fn collect_args(matches: &ArgMatches) -> Vec<OsString> { + matches + .get_many::<OsString>(options::STRING) + .map_or_else(Vec::new, |values| values.cloned().collect()) +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().get_matches_from(handle_double_hyphens(args)); + let is_posixly_correct = env::var("POSIXLY_CORRECT").is_ok(); - // TODO - // "If the POSIXLY_CORRECT environment variable is set, then when echo’s first argument is not -n it outputs option-like arguments instead of treating them as options." - // https://www.gnu.org/software/coreutils/manual/html_node/echo-invocation.html + let (args, trailing_newline, escaped) = if is_posixly_correct { + let mut args_iter = args.skip(1).peekable(); - let trailing_newline = !matches.get_flag(options::NO_NEWLINE); - let escaped = matches.get_flag(options::ENABLE_BACKSLASH_ESCAPE); + if args_iter.peek() == Some(&OsString::from("-n")) { + let matches = uu_app().get_matches_from(handle_double_hyphens(args_iter)); + let args = collect_args(&matches); + (args, false, true) + } else { + let args: Vec<_> = args_iter.collect(); + (args, true, true) + } + } else { + let matches = uu_app().get_matches_from(handle_double_hyphens(args.into_iter())); + let trailing_newline = !matches.get_flag(options::NO_NEWLINE); + let escaped = matches.get_flag(options::ENABLE_BACKSLASH_ESCAPE); + let args = collect_args(&matches); + (args, trailing_newline, escaped) + }; let mut stdout_lock = io::stdout().lock(); - - match matches.get_many::<OsString>(options::STRING) { - Some(arguments_after_options) => { - execute( - &mut stdout_lock, - trailing_newline, - escaped, - arguments_after_options, - )?; - } - None => { - // No strings to print, so just handle newline setting - if trailing_newline { - stdout_lock.write_all(b"\n")?; - } - } - } + execute(&mut stdout_lock, args, trailing_newline, escaped)?; Ok(()) } @@ -348,11 +350,11 @@ pub fn uu_app() -> Command { fn execute( stdout_lock: &mut StdoutLock, + arguments_after_options: Vec<OsString>, trailing_newline: bool, escaped: bool, - arguments_after_options: ValuesRef<'_, OsString>, ) -> UResult<()> { - for (i, input) in arguments_after_options.enumerate() { + for (i, input) in arguments_after_options.into_iter().enumerate() { let Some(bytes) = bytes_from_os_string(input.as_os_str()) else { return Err(USimpleError::new( 1, diff --git a/src/uu/env/Cargo.toml b/src/uu/env/Cargo.toml index 51a6e817949..9b21ed45ac9 100644 --- a/src/uu/env/Cargo.toml +++ b/src/uu/env/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_env" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "env ~ (uutils) set each NAME to VALUE in the environment and run COMMAND" diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index ea31c01079e..c4d9ef70ee6 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -130,14 +130,11 @@ fn parse_signal_opt<'a>(opts: &mut Options<'a>, opt: &'a OsStr) -> UResult<()> { } }); for sig in sig_vec { - let sig_str = match sig.to_str() { - Some(s) => s, - None => { - return Err(USimpleError::new( - 1, - format!("{}: invalid signal", sig.quote()), - )) - } + let Some(sig_str) = sig.to_str() else { + return Err(USimpleError::new( + 1, + format!("{}: invalid signal", sig.quote()), + )); }; let sig_val = parse_signal_value(sig_str)?; if !opts.ignore_signal.contains(&sig_val) { @@ -539,16 +536,19 @@ impl EnvAppData { } return Err(exit.code().unwrap().into()); } - Err(ref err) - if (err.kind() == io::ErrorKind::NotFound) - || (err.kind() == io::ErrorKind::InvalidInput) => - { - return Err(self.make_error_no_such_file_or_dir(prog.deref())); - } - Err(e) => { - uucore::show_error!("unknown error: {:?}", e); - return Err(126.into()); - } + Err(ref err) => match err.kind() { + io::ErrorKind::NotFound | io::ErrorKind::InvalidInput => { + return Err(self.make_error_no_such_file_or_dir(prog.deref())); + } + io::ErrorKind::PermissionDenied => { + uucore::show_error!("{}: Permission denied", prog.quote()); + return Err(126.into()); + } + _ => { + uucore::show_error!("unknown error: {:?}", err); + return Err(126.into()); + } + }, Ok(_) => (), } Ok(()) diff --git a/src/uu/expand/Cargo.toml b/src/uu/expand/Cargo.toml index fe7a5ba03a9..db3fad7329e 100644 --- a/src/uu/expand/Cargo.toml +++ b/src/uu/expand/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_expand" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "expand ~ (uutils) convert input tabs to spaces" diff --git a/src/uu/expr/Cargo.toml b/src/uu/expr/Cargo.toml index b8596976aec..1abf853d760 100644 --- a/src/uu/expr/Cargo.toml +++ b/src/uu/expr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_expr" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "expr ~ (uutils) display the value of EXPRESSION" diff --git a/src/uu/expr/src/expr.rs b/src/uu/expr/src/expr.rs index 4e41a6929e6..47fdb2a4ea7 100644 --- a/src/uu/expr/src/expr.rs +++ b/src/uu/expr/src/expr.rs @@ -34,6 +34,12 @@ pub enum ExprError { DivisionByZero, InvalidRegexExpression, ExpectedClosingBraceAfter(String), + ExpectedClosingBraceInsteadOf(String), + UnmatchedOpeningParenthesis, + UnmatchedClosingParenthesis, + UnmatchedOpeningBrace, + UnmatchedClosingBrace, + InvalidContent(String), } impl Display for ExprError { @@ -50,7 +56,25 @@ impl Display for ExprError { Self::DivisionByZero => write!(f, "division by zero"), Self::InvalidRegexExpression => write!(f, "Invalid regex expression"), Self::ExpectedClosingBraceAfter(s) => { - write!(f, "expected ')' after {}", s.quote()) + write!(f, "syntax error: expecting ')' after {}", s.quote()) + } + Self::ExpectedClosingBraceInsteadOf(s) => { + write!(f, "syntax error: expecting ')' instead of {}", s.quote()) + } + Self::UnmatchedOpeningParenthesis => { + write!(f, "Unmatched ( or \\(") + } + Self::UnmatchedClosingParenthesis => { + write!(f, "Unmatched ) or \\)") + } + Self::UnmatchedOpeningBrace => { + write!(f, "Unmatched \\{{") + } + Self::UnmatchedClosingBrace => { + write!(f, "Unmatched ) or \\}}") + } + Self::InvalidContent(s) => { + write!(f, "Invalid content of {}", s) } } } diff --git a/src/uu/expr/src/syntax_tree.rs b/src/uu/expr/src/syntax_tree.rs index 0a947a158a4..0288a67361b 100644 --- a/src/uu/expr/src/syntax_tree.rs +++ b/src/uu/expr/src/syntax_tree.rs @@ -6,7 +6,7 @@ // spell-checker:ignore (ToDO) ints paren prec multibytes use num_bigint::{BigInt, ParseBigIntError}; -use num_traits::ToPrimitive; +use num_traits::{ToPrimitive, Zero}; use onig::{Regex, RegexOptions, Syntax}; use crate::{ExprError, ExprResult}; @@ -139,7 +139,9 @@ impl StringOp { Self::Match => { let left = left.eval()?.eval_as_string(); let right = right.eval()?.eval_as_string(); - let re_string = format!("^{right}"); + check_posix_regex_errors(&right)?; + let prefix = if right.starts_with('*') { r"^\" } else { "^" }; + let re_string = format!("{prefix}{right}"); let re = Regex::with_options( &re_string, RegexOptions::REGEX_OPTION_NONE, @@ -148,7 +150,7 @@ impl StringOp { .map_err(|_| ExprError::InvalidRegexExpression)?; Ok(if re.captures_len() > 0 { re.captures(&left) - .map(|captures| captures.at(1).unwrap()) + .and_then(|captures| captures.at(1)) .unwrap_or("") .to_string() } else { @@ -173,6 +175,99 @@ impl StringOp { } } +/// Check for errors in a supplied regular expression +/// +/// GNU coreutils shows messages for invalid regular expressions +/// differently from the oniguruma library used by the regex crate. +/// This method attempts to do these checks manually in one pass +/// through the regular expression. +/// +/// This method is not comprehensively checking all cases in which +/// a regular expression could be invalid; any cases not caught will +/// result in a [ExprError::InvalidRegexExpression] when passing the +/// regular expression through the Oniguruma bindings. This method is +/// intended to just identify a few situations for which GNU coreutils +/// has specific error messages. +fn check_posix_regex_errors(pattern: &str) -> ExprResult<()> { + let mut escaped_parens: u64 = 0; + let mut escaped_braces: u64 = 0; + let mut escaped = false; + + let mut repeating_pattern_text = String::new(); + let mut invalid_content_error = false; + + for c in pattern.chars() { + match (escaped, c) { + (true, ')') => { + escaped_parens = escaped_parens + .checked_sub(1) + .ok_or(ExprError::UnmatchedClosingParenthesis)?; + } + (true, '(') => { + escaped_parens += 1; + } + (true, '}') => { + escaped_braces = escaped_braces + .checked_sub(1) + .ok_or(ExprError::UnmatchedClosingBrace)?; + let mut repetition = + repeating_pattern_text[..repeating_pattern_text.len() - 1].splitn(2, ','); + match ( + repetition + .next() + .expect("splitn always returns at least one string"), + repetition.next(), + ) { + ("", None) => { + // Empty repeating pattern + invalid_content_error = true; + } + (x, None) | (x, Some("")) => { + if x.parse::<i16>().is_err() { + invalid_content_error = true; + } + } + ("", Some(x)) => { + if x.parse::<i16>().is_err() { + invalid_content_error = true; + } + } + (f, Some(l)) => { + if let (Ok(f), Ok(l)) = (f.parse::<i16>(), l.parse::<i16>()) { + invalid_content_error = invalid_content_error || f > l; + } else { + invalid_content_error = true; + } + } + } + repeating_pattern_text.clear(); + } + (true, '{') => { + escaped_braces += 1; + } + _ => { + if escaped_braces > 0 && repeating_pattern_text.len() <= 13 { + repeating_pattern_text.push(c); + } + if escaped_braces > 0 && !(c.is_ascii_digit() || c == '\\' || c == ',') { + invalid_content_error = true; + } + } + } + escaped = !escaped && c == '\\'; + } + match ( + escaped_parens.is_zero(), + escaped_braces.is_zero(), + invalid_content_error, + ) { + (true, true, false) => Ok(()), + (_, false, _) => Err(ExprError::UnmatchedOpeningBrace), + (false, _, _) => Err(ExprError::UnmatchedOpeningParenthesis), + (true, true, true) => Err(ExprError::InvalidContent(r"\{\}".to_string())), + } +} + /// Precedence for infix binary operators const PRECEDENCE: &[&[(&str, BinOp)]] = &[ &[("|", BinOp::String(StringOp::Or))], @@ -442,13 +537,21 @@ impl<'a> Parser<'a> { }, "(" => { let s = self.parse_expression()?; - let close_paren = self.next()?; - if close_paren != ")" { + match self.next() { + Ok(")") => {} // Since we have parsed at least a '(', there will be a token // at `self.index - 1`. So this indexing won't panic. - return Err(ExprError::ExpectedClosingBraceAfter( - self.input[self.index - 1].into(), - )); + Ok(_) => { + return Err(ExprError::ExpectedClosingBraceInsteadOf( + self.input[self.index - 1].into(), + )); + } + Err(ExprError::MissingArgument(_)) => { + return Err(ExprError::ExpectedClosingBraceAfter( + self.input[self.index - 1].into(), + )); + } + Err(e) => return Err(e), } s } @@ -484,7 +587,10 @@ pub fn is_truthy(s: &NumOrStr) -> bool { #[cfg(test)] mod test { - use super::{AstNode, BinOp, NumericOp, RelationOp, StringOp}; + use crate::ExprError; + use crate::ExprError::InvalidContent; + + use super::{check_posix_regex_errors, AstNode, BinOp, NumericOp, RelationOp, StringOp}; impl From<&str> for AstNode { fn from(value: &str) -> Self { @@ -587,4 +693,123 @@ mod test { )), ); } + + #[test] + fn missing_closing_parenthesis() { + assert_eq!( + AstNode::parse(&["(", "42"]), + Err(ExprError::ExpectedClosingBraceAfter("42".to_string())) + ); + assert_eq!( + AstNode::parse(&["(", "42", "a"]), + Err(ExprError::ExpectedClosingBraceInsteadOf("a".to_string())) + ); + } + + #[test] + fn empty_substitution() { + // causes a panic in 0.0.25 + let result = AstNode::parse(&["a", ":", r"\(b\)*"]) + .unwrap() + .eval() + .unwrap(); + assert_eq!(result.eval_as_string(), ""); + } + + #[test] + fn starting_stars_become_escaped() { + let result = AstNode::parse(&["cats", ":", r"*cats"]) + .unwrap() + .eval() + .unwrap(); + assert_eq!(result.eval_as_string(), "0"); + + let result = AstNode::parse(&["*cats", ":", r"*cats"]) + .unwrap() + .eval() + .unwrap(); + assert_eq!(result.eval_as_string(), "5"); + } + + #[test] + fn only_match_in_beginning() { + let result = AstNode::parse(&["budget", ":", r"get"]) + .unwrap() + .eval() + .unwrap(); + assert_eq!(result.eval_as_string(), "0"); + } + + #[test] + fn check_regex_valid() { + assert!(check_posix_regex_errors(r"(a+b) \(a* b\)").is_ok()); + } + + #[test] + fn check_regex_simple_repeating_pattern() { + assert!(check_posix_regex_errors(r"\(a+b\)\{4\}").is_ok()); + } + + #[test] + fn check_regex_missing_closing() { + assert_eq!( + check_posix_regex_errors(r"\(abc"), + Err(ExprError::UnmatchedOpeningParenthesis) + ); + + assert_eq!( + check_posix_regex_errors(r"\{1,2"), + Err(ExprError::UnmatchedOpeningBrace) + ); + } + + #[test] + fn check_regex_missing_opening() { + assert_eq!( + check_posix_regex_errors(r"abc\)"), + Err(ExprError::UnmatchedClosingParenthesis) + ); + + assert_eq!( + check_posix_regex_errors(r"abc\}"), + Err(ExprError::UnmatchedClosingBrace) + ); + } + + #[test] + fn check_regex_empty_repeating_pattern() { + assert_eq!( + check_posix_regex_errors("ab\\{\\}"), + Err(InvalidContent(r"\{\}".to_string())) + ) + } + + #[test] + fn check_regex_intervals_two_numbers() { + assert_eq!( + // out of order + check_posix_regex_errors("ab\\{1,0\\}"), + Err(InvalidContent(r"\{\}".to_string())) + ); + assert_eq!( + check_posix_regex_errors("ab\\{1,a\\}"), + Err(InvalidContent(r"\{\}".to_string())) + ); + assert_eq!( + check_posix_regex_errors("ab\\{a,3\\}"), + Err(InvalidContent(r"\{\}".to_string())) + ); + assert_eq!( + check_posix_regex_errors("ab\\{a,b\\}"), + Err(InvalidContent(r"\{\}".to_string())) + ); + assert_eq!( + check_posix_regex_errors("ab\\{a,\\}"), + Err(InvalidContent(r"\{\}".to_string())) + ); + assert_eq!( + check_posix_regex_errors("ab\\{,b\\}"), + Err(InvalidContent(r"\{\}".to_string())) + ); + } } diff --git a/src/uu/factor/Cargo.toml b/src/uu/factor/Cargo.toml index 8b0d35bcb0f..08ff64f57b6 100644 --- a/src/uu/factor/Cargo.toml +++ b/src/uu/factor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_factor" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "factor ~ (uutils) display the prime factors of each NUMBER" diff --git a/src/uu/false/Cargo.toml b/src/uu/false/Cargo.toml index f8c6d84573a..5c817c754b7 100644 --- a/src/uu/false/Cargo.toml +++ b/src/uu/false/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_false" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "false ~ (uutils) do nothing and fail" diff --git a/src/uu/fmt/Cargo.toml b/src/uu/fmt/Cargo.toml index c71d9a5df32..6522c909f80 100644 --- a/src/uu/fmt/Cargo.toml +++ b/src/uu/fmt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_fmt" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "fmt ~ (uutils) reformat each paragraph of input" diff --git a/src/uu/fmt/src/parasplit.rs b/src/uu/fmt/src/parasplit.rs index 8aa18c4c987..f9da4ad58fd 100644 --- a/src/uu/fmt/src/parasplit.rs +++ b/src/uu/fmt/src/parasplit.rs @@ -255,9 +255,8 @@ impl ParagraphStream<'_> { if l_slice.starts_with("From ") { true } else { - let colon_posn = match l_slice.find(':') { - Some(n) => n, - None => return false, + let Some(colon_posn) = l_slice.find(':') else { + return false; }; // header field must be nonzero length @@ -560,12 +559,11 @@ impl<'a> Iterator for WordSplit<'a> { // find the start of the next word, and record if we find a tab character let (before_tab, after_tab, word_start) = - match self.analyze_tabs(&self.string[old_position..]) { - (b, a, Some(s)) => (b, a, s + old_position), - (_, _, None) => { - self.position = self.length; - return None; - } + if let (b, a, Some(s)) = self.analyze_tabs(&self.string[old_position..]) { + (b, a, s + old_position) + } else { + self.position = self.length; + return None; }; // find the beginning of the next whitespace diff --git a/src/uu/fold/Cargo.toml b/src/uu/fold/Cargo.toml index 5bfe63ec9c8..cf6d59ad66e 100644 --- a/src/uu/fold/Cargo.toml +++ b/src/uu/fold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_fold" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "fold ~ (uutils) wrap each line of input" diff --git a/src/uu/groups/Cargo.toml b/src/uu/groups/Cargo.toml index 14a3cb924f1..11055f529a3 100644 --- a/src/uu/groups/Cargo.toml +++ b/src/uu/groups/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_groups" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "groups ~ (uutils) display group memberships for USERNAME" diff --git a/src/uu/hashsum/Cargo.toml b/src/uu/hashsum/Cargo.toml index 42484fabf8f..9ab253bd7b0 100644 --- a/src/uu/hashsum/Cargo.toml +++ b/src/uu/hashsum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_hashsum" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "hashsum ~ (uutils) display or check input digests" diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 1d3a758f5ea..b8dc63c323d 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -24,6 +24,7 @@ use uucore::checksum::escape_filename; use uucore::checksum::perform_checksum_validation; use uucore::checksum::ChecksumError; use uucore::checksum::ChecksumOptions; +use uucore::checksum::ChecksumVerbose; use uucore::checksum::HashAlgorithm; use uucore::error::{FromIo, UResult}; use uucore::sum::{Digest, Sha3_224, Sha3_256, Sha3_384, Sha3_512, Shake128, Shake256}; @@ -240,13 +241,14 @@ pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { || iter::once(OsStr::new("-")).collect::<Vec<_>>(), |files| files.map(OsStr::new).collect::<Vec<_>>(), ); + + let verbose = ChecksumVerbose::new(status, quiet, warn); + let opts = ChecksumOptions { binary, ignore_missing, - quiet, - status, strict, - warn, + verbose, }; // Execute the checksum validation @@ -356,14 +358,16 @@ pub fn uu_app_common() -> Command { .short('q') .long(options::QUIET) .help("don't print OK for each successfully verified file") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::STATUS, options::WARN]), ) .arg( Arg::new(options::STATUS) .short('s') .long("status") .help("don't output anything, status code shows success") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::QUIET, options::WARN]), ) .arg( Arg::new(options::STRICT) @@ -382,7 +386,8 @@ pub fn uu_app_common() -> Command { .short('w') .long("warn") .help("warn about improperly formatted checksum lines") - .action(ArgAction::SetTrue), + .action(ArgAction::SetTrue) + .overrides_with_all([options::QUIET, options::STATUS]), ) .arg( Arg::new("zero") diff --git a/src/uu/head/Cargo.toml b/src/uu/head/Cargo.toml index 1ccf3b9851b..3590e11465b 100644 --- a/src/uu/head/Cargo.toml +++ b/src/uu/head/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_head" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "head ~ (uutils) display the first lines of input" diff --git a/src/uu/head/src/parse.rs b/src/uu/head/src/parse.rs index dce60bae012..619b48e05e9 100644 --- a/src/uu/head/src/parse.rs +++ b/src/uu/head/src/parse.rs @@ -91,9 +91,8 @@ fn process_num_block( } if let Some(n) = multiplier { options.push(OsString::from("-c")); - let num = match num.checked_mul(n) { - Some(n) => n, - None => return Some(Err(ParseError::Overflow)), + let Some(num) = num.checked_mul(n) else { + return Some(Err(ParseError::Overflow)); }; options.push(OsString::from(format!("{num}"))); } else { diff --git a/src/uu/hostid/Cargo.toml b/src/uu/hostid/Cargo.toml index e667e76ca5a..4e853ac8edd 100644 --- a/src/uu/hostid/Cargo.toml +++ b/src/uu/hostid/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_hostid" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "hostid ~ (uutils) display the numeric identifier of the current host" diff --git a/src/uu/hostname/Cargo.toml b/src/uu/hostname/Cargo.toml index 716d76f6902..0fa58481717 100644 --- a/src/uu/hostname/Cargo.toml +++ b/src/uu/hostname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_hostname" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "hostname ~ (uutils) display or set the host name of the current host" diff --git a/src/uu/id/Cargo.toml b/src/uu/id/Cargo.toml index 8be08ee8060..575808042c7 100644 --- a/src/uu/id/Cargo.toml +++ b/src/uu/id/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_id" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "id ~ (uutils) display user and group information for USER" diff --git a/src/uu/install/Cargo.toml b/src/uu/install/Cargo.toml index 88614cf113d..26507023806 100644 --- a/src/uu/install/Cargo.toml +++ b/src/uu/install/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_install" -version = "0.0.28" +version = "0.0.29" authors = ["Ben Eills <ben@beneills.com>", "uutils developers"] license = "MIT" description = "install ~ (uutils) copy files from SOURCE to DESTINATION (with specified attributes)" diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index cf810937794..89522f15d1f 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -652,7 +652,7 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR } let mut targetpath = target_dir.to_path_buf(); - let filename = sourcepath.components().last().unwrap(); + let filename = sourcepath.components().next_back().unwrap(); targetpath.push(filename); show_if_err!(copy(sourcepath, &targetpath, b)); diff --git a/src/uu/join/Cargo.toml b/src/uu/join/Cargo.toml index 322fcbf6bd1..a19b6818f04 100644 --- a/src/uu/join/Cargo.toml +++ b/src/uu/join/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_join" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "join ~ (uutils) merge lines from inputs with matching join fields" diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index f01f75b71d5..01e1b40fc4a 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -20,7 +20,7 @@ use std::os::unix::ffi::OsStrExt; use uucore::display::Quotable; use uucore::error::{set_exit_code, FromIo, UError, UResult, USimpleError}; use uucore::line_ending::LineEnding; -use uucore::{crash_if_err, format_usage, help_about, help_usage}; +use uucore::{format_usage, help_about, help_usage}; const ABOUT: &str = help_about!("join.md"); const USAGE: &str = help_usage!("join.md"); @@ -587,15 +587,19 @@ impl<'a> State<'a> { !self.seq.is_empty() } - fn initialize<Sep: Separator>(&mut self, read_sep: &Sep, autoformat: bool) -> usize { - if let Some(line) = crash_if_err!(1, self.read_line(read_sep)) { + fn initialize<Sep: Separator>( + &mut self, + read_sep: &Sep, + autoformat: bool, + ) -> std::io::Result<usize> { + if let Some(line) = self.read_line(read_sep)? { self.seq.push(line); if autoformat { - return self.seq[0].field_ranges.len(); + return Ok(self.seq[0].field_ranges.len()); } } - 0 + Ok(0) } fn finalize<Sep: Separator>( @@ -1008,20 +1012,21 @@ fn exec<Sep: Separator>(file1: &str, file2: &str, settings: Settings, sep: Sep) let format = if settings.autoformat { let mut format = vec![Spec::Key]; - let mut initialize = |state: &mut State| { - let max_fields = state.initialize(&sep, settings.autoformat); + let mut initialize = |state: &mut State| -> UResult<()> { + let max_fields = state.initialize(&sep, settings.autoformat)?; for i in 0..max_fields { if i != state.key { format.push(Spec::Field(state.file_num, i)); } } + Ok(()) }; - initialize(&mut state1); - initialize(&mut state2); + initialize(&mut state1)?; + initialize(&mut state2)?; format } else { - state1.initialize(&sep, settings.autoformat); - state2.initialize(&sep, settings.autoformat); + state1.initialize(&sep, settings.autoformat)?; + state2.initialize(&sep, settings.autoformat)?; settings.format }; diff --git a/src/uu/kill/Cargo.toml b/src/uu/kill/Cargo.toml index 22b05268ec1..aa7cb4749d3 100644 --- a/src/uu/kill/Cargo.toml +++ b/src/uu/kill/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_kill" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "kill ~ (uutils) send a signal to a process" diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index 1dc3526538d..7638dcc4488 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -141,6 +141,10 @@ fn handle_obsolete(args: &mut Vec<String>) -> Option<usize> { // Old signal can only be in the first argument position let slice = args[1].as_str(); if let Some(signal) = slice.strip_prefix('-') { + // With '-', a signal name must start with an uppercase char + if signal.chars().next().is_some_and(|c| c.is_lowercase()) { + return None; + } // Check if it is a valid signal let opt_signal = signal_by_name_or_value(signal); if opt_signal.is_some() { @@ -154,12 +158,7 @@ fn handle_obsolete(args: &mut Vec<String>) -> Option<usize> { } fn table() { - // GNU kill doesn't list the EXIT signal with --table, so we ignore it, too - for (idx, signal) in ALL_SIGNALS - .iter() - .enumerate() - .filter(|(_, s)| **s != "EXIT") - { + for (idx, signal) in ALL_SIGNALS.iter().enumerate() { println!("{idx: >#2} {signal}"); } } @@ -183,8 +182,7 @@ fn print_signal(signal_name_or_value: &str) -> UResult<()> { } fn print_signals() { - // GNU kill doesn't list the EXIT signal with --list, so we ignore it, too - for signal in ALL_SIGNALS.iter().filter(|x| **x != "EXIT") { + for signal in ALL_SIGNALS { println!("{signal}"); } } diff --git a/src/uu/link/Cargo.toml b/src/uu/link/Cargo.toml index 4baf6adcfcd..b8c5df3618e 100644 --- a/src/uu/link/Cargo.toml +++ b/src/uu/link/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_link" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "link ~ (uutils) create a hard (file system) link to FILE" diff --git a/src/uu/ln/Cargo.toml b/src/uu/ln/Cargo.toml index 488262a417c..5b82e211e0a 100644 --- a/src/uu/ln/Cargo.toml +++ b/src/uu/ln/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_ln" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "ln ~ (uutils) create a (file system) link to TARGET" diff --git a/src/uu/logname/Cargo.toml b/src/uu/logname/Cargo.toml index deb1d1229a1..d0bf9a8d6ab 100644 --- a/src/uu/logname/Cargo.toml +++ b/src/uu/logname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_logname" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "logname ~ (uutils) display the login name of the current user" diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index de50b4c924b..a21f178542a 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_ls" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "ls ~ (uutils) display directory contents" @@ -18,15 +18,18 @@ path = "src/ls.rs" [dependencies] ansi-width = { workspace = true } -clap = { workspace = true, features = ["env"] } chrono = { workspace = true } -number_prefix = { workspace = true } -uutils_term_grid = { workspace = true } -terminal_size = { workspace = true } +clap = { workspace = true, features = ["env"] } glob = { workspace = true } +hostname = { workspace = true } lscolors = { workspace = true } +number_prefix = { workspace = true } +once_cell = { workspace = true } +selinux = { workspace = true, optional = true } +terminal_size = { workspace = true } uucore = { workspace = true, features = [ "colors", + "custom-tz-fmt", "entries", "format", "fs", @@ -34,9 +37,7 @@ uucore = { workspace = true, features = [ "quoting-style", "version-cmp", ] } -once_cell = { workspace = true } -selinux = { workspace = true, optional = true } -hostname = { workspace = true } +uutils_term_grid = { workspace = true } [[bin]] name = "ls" diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 994eabc21b6..680fe94ab4c 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -5,19 +5,9 @@ // spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly -use clap::{ - builder::{NonEmptyStringValueParser, PossibleValue, ValueParser}, - crate_version, Arg, ArgAction, Command, -}; -use glob::{MatchOptions, Pattern}; -use lscolors::LsColors; - -use ansi_width::ansi_width; -use std::{cell::OnceCell, num::IntErrorKind}; -use std::{collections::HashSet, io::IsTerminal}; - #[cfg(windows)] use std::os::windows::fs::MetadataExt; +use std::{cell::OnceCell, num::IntErrorKind}; use std::{ cmp::Reverse, error::Error, @@ -34,7 +24,18 @@ use std::{ os::unix::fs::{FileTypeExt, MetadataExt}, time::Duration, }; +use std::{collections::HashSet, io::IsTerminal}; + +use ansi_width::ansi_width; +use chrono::{DateTime, Local, TimeDelta}; +use clap::{ + builder::{NonEmptyStringValueParser, PossibleValue, ValueParser}, + crate_version, Arg, ArgAction, Command, +}; +use glob::{MatchOptions, Pattern}; +use lscolors::LsColors; use term_grid::{Direction, Filling, Grid, GridOptions}; + use uucore::error::USimpleError; use uucore::format::human::{human_readable, SizeFormat}; #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] @@ -57,6 +58,7 @@ use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR}; use uucore::line_ending::LineEnding; use uucore::quoting_style::{self, escape_name, QuotingStyle}; use uucore::{ + custom_tz_fmt, display::Quotable, error::{set_exit_code, UError, UResult}, format_usage, @@ -67,10 +69,12 @@ use uucore::{ version_cmp::version_cmp, }; use uucore::{help_about, help_section, help_usage, parse_glob, show, show_error, show_warning}; + mod dired; use dired::{is_dired_arg_present, DiredOutput}; mod colors; use colors::{color_name, StyleManager}; + #[cfg(not(feature = "selinux"))] static CONTEXT_HELP_TEXT: &str = "print any security context of each file (not enabled)"; #[cfg(feature = "selinux")] @@ -334,6 +338,35 @@ enum TimeStyle { Format(String), } +/// Whether the given date is considered recent (i.e., in the last 6 months). +fn is_recent(time: DateTime<Local>) -> bool { + // According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. + time + TimeDelta::try_seconds(31_556_952 / 2).unwrap() > Local::now() +} + +impl TimeStyle { + /// Format the given time according to this time format style. + fn format(&self, time: DateTime<Local>) -> String { + let recent = is_recent(time); + match (self, recent) { + (Self::FullIso, _) => time.format("%Y-%m-%d %H:%M:%S.%f %z").to_string(), + (Self::LongIso, _) => time.format("%Y-%m-%d %H:%M").to_string(), + (Self::Iso, true) => time.format("%m-%d %H:%M").to_string(), + (Self::Iso, false) => time.format("%Y-%m-%d ").to_string(), + // spell-checker:ignore (word) datetime + //In this version of chrono translating can be done + //The function is chrono::datetime::DateTime::format_localized + //However it's currently still hard to get the current pure-rust-locale + //So it's not yet implemented + (Self::Locale, true) => time.format("%b %e %H:%M").to_string(), + (Self::Locale, false) => time.format("%b %e %Y").to_string(), + (Self::Format(fmt), _) => time + .format(custom_tz_fmt::custom_time_format(fmt).as_str()) + .to_string(), + } + } +} + fn parse_time_style(options: &clap::ArgMatches) -> Result<TimeStyle, LsError> { let possible_time_styles = vec![ "full-iso".to_string(), @@ -346,8 +379,8 @@ fn parse_time_style(options: &clap::ArgMatches) -> Result<TimeStyle, LsError> { //If both FULL_TIME and TIME_STYLE are present //The one added last is dominant if options.get_flag(options::FULL_TIME) - && options.indices_of(options::FULL_TIME).unwrap().last() - > options.indices_of(options::TIME_STYLE).unwrap().last() + && options.indices_of(options::FULL_TIME).unwrap().next_back() + > options.indices_of(options::TIME_STYLE).unwrap().next_back() { Ok(TimeStyle::FullIso) } else { @@ -3115,31 +3148,7 @@ fn get_time(md: &Metadata, config: &Config) -> Option<chrono::DateTime<chrono::L fn display_date(metadata: &Metadata, config: &Config) -> String { match get_time(metadata, config) { - Some(time) => { - //Date is recent if from past 6 months - //According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. - let recent = time + chrono::TimeDelta::try_seconds(31_556_952 / 2).unwrap() - > chrono::Local::now(); - - match &config.time_style { - TimeStyle::FullIso => time.format("%Y-%m-%d %H:%M:%S.%f %z"), - TimeStyle::LongIso => time.format("%Y-%m-%d %H:%M"), - TimeStyle::Iso => time.format(if recent { "%m-%d %H:%M" } else { "%Y-%m-%d " }), - TimeStyle::Locale => { - let fmt = if recent { "%b %e %H:%M" } else { "%b %e %Y" }; - - // spell-checker:ignore (word) datetime - //In this version of chrono translating can be done - //The function is chrono::datetime::DateTime::format_localized - //However it's currently still hard to get the current pure-rust-locale - //So it's not yet implemented - - time.format(fmt) - } - TimeStyle::Format(e) => time.format(e), - } - .to_string() - } + Some(time) => config.time_style.format(time), None => "???".into(), } } diff --git a/src/uu/mkdir/Cargo.toml b/src/uu/mkdir/Cargo.toml index 6fbce879a8b..80871b11544 100644 --- a/src/uu/mkdir/Cargo.toml +++ b/src/uu/mkdir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mkdir" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "mkdir ~ (uutils) create DIRECTORY" diff --git a/src/uu/mkfifo/Cargo.toml b/src/uu/mkfifo/Cargo.toml index 93ebed94ada..e2605ae1de9 100644 --- a/src/uu/mkfifo/Cargo.toml +++ b/src/uu/mkfifo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mkfifo" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "mkfifo ~ (uutils) create FIFOs (named pipes)" diff --git a/src/uu/mknod/Cargo.toml b/src/uu/mknod/Cargo.toml index 9177fe4f466..02e3f4cab8a 100644 --- a/src/uu/mknod/Cargo.toml +++ b/src/uu/mknod/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mknod" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "mknod ~ (uutils) create special file NAME of TYPE" diff --git a/src/uu/mktemp/Cargo.toml b/src/uu/mktemp/Cargo.toml index 0bcd6ac5187..60d0d28997b 100644 --- a/src/uu/mktemp/Cargo.toml +++ b/src/uu/mktemp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mktemp" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "mktemp ~ (uutils) create and display a temporary file or directory from TEMPLATE" diff --git a/src/uu/more/Cargo.toml b/src/uu/more/Cargo.toml index 5af95afe65e..ef0bb8de1e7 100644 --- a/src/uu/more/Cargo.toml +++ b/src/uu/more/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_more" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "more ~ (uutils) input perusal filter" diff --git a/src/uu/mv/Cargo.toml b/src/uu/mv/Cargo.toml index 7e8ccc16c32..13b40f7fb0c 100644 --- a/src/uu/mv/Cargo.toml +++ b/src/uu/mv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_mv" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "mv ~ (uutils) move (rename) SOURCE to DESTINATION" @@ -26,6 +26,7 @@ uucore = { workspace = true, features = [ "fsxattr", "update-control", ] } +thiserror = { workspace = true } [[bin]] name = "mv" diff --git a/src/uu/mv/src/error.rs b/src/uu/mv/src/error.rs index 6daa8188ec1..5049725a67e 100644 --- a/src/uu/mv/src/error.rs +++ b/src/uu/mv/src/error.rs @@ -2,47 +2,37 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use std::error::Error; -use std::fmt::{Display, Formatter, Result}; - +use thiserror::Error; use uucore::error::UError; -#[derive(Debug)] +#[derive(Debug, Error)] pub enum MvError { + #[error("cannot stat {0}: No such file or directory")] NoSuchFile(String), + + #[error("cannot stat {0}: Not a directory")] CannotStatNotADirectory(String), + + #[error("{0} and {1} are the same file")] SameFile(String, String), + + #[error("cannot move {0} to a subdirectory of itself, {1}")] SelfTargetSubdirectory(String, String), + + #[error("cannot overwrite directory {0} with non-directory")] DirectoryToNonDirectory(String), + + #[error("cannot overwrite non-directory {1} with directory {0}")] NonDirectoryToDirectory(String, String), + + #[error("target {0}: Not a directory")] NotADirectory(String), + + #[error("target directory {0}: Not a directory")] TargetNotADirectory(String), + + #[error("failed to access {0}: Not a directory")] FailedToAccessNotADirectory(String), } -impl Error for MvError {} impl UError for MvError {} -impl Display for MvError { - fn fmt(&self, f: &mut Formatter) -> Result { - match self { - Self::NoSuchFile(s) => write!(f, "cannot stat {s}: No such file or directory"), - Self::CannotStatNotADirectory(s) => write!(f, "cannot stat {s}: Not a directory"), - Self::SameFile(s, t) => write!(f, "{s} and {t} are the same file"), - Self::SelfTargetSubdirectory(s, t) => { - write!(f, "cannot move {s} to a subdirectory of itself, {t}") - } - Self::DirectoryToNonDirectory(t) => { - write!(f, "cannot overwrite directory {t} with non-directory") - } - Self::NonDirectoryToDirectory(s, t) => { - write!(f, "cannot overwrite non-directory {t} with directory {s}") - } - Self::NotADirectory(t) => write!(f, "target {t}: Not a directory"), - Self::TargetNotADirectory(t) => write!(f, "target directory {t}: Not a directory"), - - Self::FailedToAccessNotADirectory(t) => { - write!(f, "failed to access {t}: Not a directory") - } - } - } -} diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 675982bacba..6e533dace85 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -576,13 +576,6 @@ fn rename( let mut backup_path = None; if to.exists() { - if opts.update == UpdateMode::ReplaceIfOlder && opts.overwrite == OverwriteMode::Interactive - { - // `mv -i --update old new` when `new` exists doesn't move anything - // and exit with 0 - return Ok(()); - } - if opts.update == UpdateMode::ReplaceNone { if opts.debug { println!("skipped {}", to.quote()); diff --git a/src/uu/nice/Cargo.toml b/src/uu/nice/Cargo.toml index 3d231aff6c0..afcce849e37 100644 --- a/src/uu/nice/Cargo.toml +++ b/src/uu/nice/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nice" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "nice ~ (uutils) run PROGRAM with modified scheduling priority" diff --git a/src/uu/nl/Cargo.toml b/src/uu/nl/Cargo.toml index 7f3091d942c..d82793761ca 100644 --- a/src/uu/nl/Cargo.toml +++ b/src/uu/nl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nl" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "nl ~ (uutils) display input with added line numbers" diff --git a/src/uu/nohup/Cargo.toml b/src/uu/nohup/Cargo.toml index 55660a99240..df324856107 100644 --- a/src/uu/nohup/Cargo.toml +++ b/src/uu/nohup/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nohup" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "nohup ~ (uutils) run COMMAND, ignoring hangup signals" diff --git a/src/uu/nproc/Cargo.toml b/src/uu/nproc/Cargo.toml index ebcf8b6d2a2..5b65d445f70 100644 --- a/src/uu/nproc/Cargo.toml +++ b/src/uu/nproc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_nproc" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "nproc ~ (uutils) display the number of processing units available" diff --git a/src/uu/numfmt/Cargo.toml b/src/uu/numfmt/Cargo.toml index 1ae42b5d5cf..1313a234e32 100644 --- a/src/uu/numfmt/Cargo.toml +++ b/src/uu/numfmt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_numfmt" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "numfmt ~ (uutils) reformat NUMBER" diff --git a/src/uu/od/Cargo.toml b/src/uu/od/Cargo.toml index 2ca1678abf4..c713f121f3e 100644 --- a/src/uu/od/Cargo.toml +++ b/src/uu/od/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_od" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "od ~ (uutils) display formatted representation of input" diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 6dd75d30792..fcb72c1ae70 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -626,7 +626,9 @@ fn format_error_message(error: &ParseSizeError, s: &str, option: &str) -> String ParseSizeError::InvalidSuffix(_) => { format!("invalid suffix in --{} argument {}", option, s.quote()) } - ParseSizeError::ParseFailure(_) => format!("invalid --{} argument {}", option, s.quote()), + ParseSizeError::ParseFailure(_) | ParseSizeError::PhysicalMem(_) => { + format!("invalid --{} argument {}", option, s.quote()) + } ParseSizeError::SizeTooBig(_) => format!("--{} argument {} too large", option, s.quote()), } } diff --git a/src/uu/paste/Cargo.toml b/src/uu/paste/Cargo.toml index f8ea87965a7..dc18d3124b1 100644 --- a/src/uu/paste/Cargo.toml +++ b/src/uu/paste/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_paste" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "paste ~ (uutils) merge lines from inputs" diff --git a/src/uu/pathchk/Cargo.toml b/src/uu/pathchk/Cargo.toml index 24005d30a49..25904bfdbf4 100644 --- a/src/uu/pathchk/Cargo.toml +++ b/src/uu/pathchk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pathchk" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "pathchk ~ (uutils) diagnose invalid or non-portable PATHNAME" diff --git a/src/uu/pinky/Cargo.toml b/src/uu/pinky/Cargo.toml index cb64288c664..4af298339d4 100644 --- a/src/uu/pinky/Cargo.toml +++ b/src/uu/pinky/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pinky" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "pinky ~ (uutils) display user information" diff --git a/src/uu/pr/Cargo.toml b/src/uu/pr/Cargo.toml index 04aec10307f..2e245569e8f 100644 --- a/src/uu/pr/Cargo.toml +++ b/src/uu/pr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pr" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "pr ~ (uutils) convert text files for printing" diff --git a/src/uu/printenv/Cargo.toml b/src/uu/printenv/Cargo.toml index 65abcdd3381..e5e07ced3c2 100644 --- a/src/uu/printenv/Cargo.toml +++ b/src/uu/printenv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_printenv" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "printenv ~ (uutils) display value of environment VAR" diff --git a/src/uu/printf/Cargo.toml b/src/uu/printf/Cargo.toml index c37bcffc91e..cad30bd32b4 100644 --- a/src/uu/printf/Cargo.toml +++ b/src/uu/printf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_printf" -version = "0.0.28" +version = "0.0.29" authors = ["Nathan Ross", "uutils developers"] license = "MIT" description = "printf ~ (uutils) FORMAT and display ARGUMENTS" diff --git a/src/uu/ptx/Cargo.toml b/src/uu/ptx/Cargo.toml index de97790abcb..4d50a7cd419 100644 --- a/src/uu/ptx/Cargo.toml +++ b/src/uu/ptx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_ptx" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "ptx ~ (uutils) display a permuted index of input" diff --git a/src/uu/pwd/Cargo.toml b/src/uu/pwd/Cargo.toml index e734c181907..c9290f16b70 100644 --- a/src/uu/pwd/Cargo.toml +++ b/src/uu/pwd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_pwd" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "pwd ~ (uutils) display current working directory" diff --git a/src/uu/readlink/Cargo.toml b/src/uu/readlink/Cargo.toml index 4a226cad062..3792bb3de8d 100644 --- a/src/uu/readlink/Cargo.toml +++ b/src/uu/readlink/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_readlink" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "readlink ~ (uutils) display resolved path of PATHNAME" diff --git a/src/uu/realpath/Cargo.toml b/src/uu/realpath/Cargo.toml index dea52663e98..bd0154e2162 100644 --- a/src/uu/realpath/Cargo.toml +++ b/src/uu/realpath/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_realpath" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "realpath ~ (uutils) display resolved absolute path of PATHNAME" diff --git a/src/uu/rm/Cargo.toml b/src/uu/rm/Cargo.toml index 49e95bb06ce..c45dfe33d8a 100644 --- a/src/uu/rm/Cargo.toml +++ b/src/uu/rm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_rm" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "rm ~ (uutils) remove PATHNAME" diff --git a/src/uu/rmdir/Cargo.toml b/src/uu/rmdir/Cargo.toml index 3637b9130f7..286795e02c4 100644 --- a/src/uu/rmdir/Cargo.toml +++ b/src/uu/rmdir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_rmdir" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "rmdir ~ (uutils) remove empty DIRECTORY" diff --git a/src/uu/runcon/Cargo.toml b/src/uu/runcon/Cargo.toml index 8bb10acbbbe..cda5fc2f776 100644 --- a/src/uu/runcon/Cargo.toml +++ b/src/uu/runcon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_runcon" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "runcon ~ (uutils) run command with specified security context" diff --git a/src/uu/seq/Cargo.toml b/src/uu/seq/Cargo.toml index 99de71c1276..791290a8d06 100644 --- a/src/uu/seq/Cargo.toml +++ b/src/uu/seq/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore bigdecimal cfgs [package] name = "uu_seq" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "seq ~ (uutils) display a sequence of numbers" @@ -22,6 +22,7 @@ bigdecimal = { workspace = true } clap = { workspace = true } num-bigint = { workspace = true } num-traits = { workspace = true } +thiserror = { workspace = true } uucore = { workspace = true, features = ["format", "quoting-style"] } [[bin]] diff --git a/src/uu/seq/src/error.rs b/src/uu/seq/src/error.rs index e81c30fe673..90b1a841612 100644 --- a/src/uu/seq/src/error.rs +++ b/src/uu/seq/src/error.rs @@ -4,32 +4,40 @@ // file that was distributed with this source code. // spell-checker:ignore numberparse //! Errors returned by seq. -use std::error::Error; -use std::fmt::Display; - +use crate::numberparse::ParseNumberError; +use thiserror::Error; use uucore::display::Quotable; use uucore::error::UError; -use crate::numberparse::ParseNumberError; - -#[derive(Debug)] +#[derive(Debug, Error)] pub enum SeqError { /// An error parsing the input arguments. /// /// The parameters are the [`String`] argument as read from the /// command line and the underlying parsing error itself. + #[error("invalid {} argument: {}", parse_error_type(.1), .0.quote())] ParseError(String, ParseNumberError), /// The increment argument was zero, which is not allowed. /// /// The parameter is the increment argument as a [`String`] as read /// from the command line. + #[error("invalid Zero increment value: {}", .0.quote())] ZeroIncrement(String), /// No arguments were passed to this function, 1 or more is required + #[error("missing operand")] NoArguments, } +fn parse_error_type(e: &ParseNumberError) -> &'static str { + match e { + ParseNumberError::Float => "floating point", + ParseNumberError::Nan => "'not-a-number'", + ParseNumberError::Hex => "hexadecimal", + } +} + impl UError for SeqError { /// Always return 1. fn code(&self) -> i32 { @@ -40,22 +48,3 @@ impl UError for SeqError { true } } - -impl Error for SeqError {} - -impl Display for SeqError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::ParseError(s, e) => { - let error_type = match e { - ParseNumberError::Float => "floating point", - ParseNumberError::Nan => "'not-a-number'", - ParseNumberError::Hex => "hexadecimal", - }; - write!(f, "invalid {error_type} argument: {}", s.quote()) - } - Self::ZeroIncrement(s) => write!(f, "invalid Zero increment value: {}", s.quote()), - Self::NoArguments => write!(f, "missing operand"), - } - } -} diff --git a/src/uu/seq/src/hexadecimalfloat.rs b/src/uu/seq/src/hexadecimalfloat.rs new file mode 100644 index 00000000000..e98074dd928 --- /dev/null +++ b/src/uu/seq/src/hexadecimalfloat.rs @@ -0,0 +1,404 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore extendedbigdecimal bigdecimal hexdigit numberparse +use crate::extendedbigdecimal::ExtendedBigDecimal; +use crate::number::PreciseNumber; +use crate::numberparse::ParseNumberError; +use bigdecimal::BigDecimal; +use num_traits::FromPrimitive; + +/// The base of the hex number system +const HEX_RADIX: u32 = 16; + +/// Parse a number from a floating-point hexadecimal exponent notation. +/// +/// # Errors +/// Returns [`Err`] if: +/// - the input string is not a valid hexadecimal string +/// - the input data can't be interpreted as ['f64'] or ['BigDecimal'] +/// +/// # Examples +/// +/// ```rust,ignore +/// let input = "0x1.4p-2"; +/// let expected = 0.3125; +/// match input.parse_number::<PreciseNumber>().unwrap().number { +/// ExtendedBigDecimal::BigDecimal(bd) => assert_eq!(bd.to_f64().unwrap(),expected), +/// _ => unreachable!() +/// }; +/// ``` +pub fn parse_number(s: &str) -> Result<PreciseNumber, ParseNumberError> { + // Parse floating point parts + let (sign, remain) = parse_sign_multiplier(s.trim())?; + let remain = parse_hex_prefix(remain)?; + let (integral_part, remain) = parse_integral_part(remain)?; + let (fractional_part, remain) = parse_fractional_part(remain)?; + let (exponent_part, remain) = parse_exponent_part(remain)?; + + // Check parts. Rise error if: + // - The input string is not fully consumed + // - Only integral part is presented + // - Only exponent part is presented + // - All 3 parts are empty + match ( + integral_part, + fractional_part, + exponent_part, + remain.is_empty(), + ) { + (_, _, _, false) + | (Some(_), None, None, _) + | (None, None, Some(_), _) + | (None, None, None, _) => return Err(ParseNumberError::Float), + _ => (), + }; + + // Build a number from parts + let integral_value = integral_part.unwrap_or(0.0); + let fractional_value = fractional_part.unwrap_or(0.0); + let exponent_value = (2.0_f64).powi(exponent_part.unwrap_or(0)); + let value = sign * (integral_value + fractional_value) * exponent_value; + + // Build a PreciseNumber + let number = BigDecimal::from_f64(value).ok_or(ParseNumberError::Float)?; + let num_fractional_digits = number.fractional_digit_count().max(0) as u64; + let num_integral_digits = if value.abs() < 1.0 { + 0 + } else { + number.digits() - num_fractional_digits + }; + let num_integral_digits = num_integral_digits + if sign < 0.0 { 1 } else { 0 }; + + Ok(PreciseNumber::new( + ExtendedBigDecimal::BigDecimal(number), + num_integral_digits as usize, + num_fractional_digits as usize, + )) +} + +// Detect number precision similar to GNU coreutils. Refer to scan_arg in seq.c. There are still +// some differences from the GNU version, but this should be sufficient to test the idea. +pub fn parse_precision(s: &str) -> Option<usize> { + let hex_index = s.find(['x', 'X']); + let point_index = s.find('.'); + + if hex_index.is_some() { + // Hex value. Returns: + // - 0 for a hexadecimal integer (filled above) + // - None for a hexadecimal floating-point number (the default value of precision) + let power_index = s.find(['p', 'P']); + if point_index.is_none() && power_index.is_none() { + // No decimal point and no 'p' (power) => integer => precision = 0 + return Some(0); + } else { + return None; + } + } + + // This is a decimal floating point. The precision depends on two parameters: + // - the number of fractional digits + // - the exponent + // Let's detect the number of fractional digits + let fractional_length = if let Some(point_index) = point_index { + s[point_index + 1..] + .chars() + .take_while(|c| c.is_ascii_digit()) + .count() + } else { + 0 + }; + + let mut precision = Some(fractional_length); + + // Let's update the precision if exponent is present + if let Some(exponent_index) = s.find(['e', 'E']) { + let exponent_value: i32 = s[exponent_index + 1..].parse().unwrap_or(0); + if exponent_value < 0 { + precision = precision.map(|p| p + exponent_value.unsigned_abs() as usize); + } else { + precision = precision.map(|p| p - p.min(exponent_value as usize)); + } + } + precision +} + +/// Parse the sign multiplier. +/// +/// If a sign is present, the function reads and converts it into a multiplier. +/// If no sign is present, a multiplier of 1.0 is used. +/// +/// # Errors +/// +/// Returns [`Err`] if the input string does not start with a recognized sign or '0' symbol. +fn parse_sign_multiplier(s: &str) -> Result<(f64, &str), ParseNumberError> { + if let Some(remain) = s.strip_prefix('-') { + Ok((-1.0, remain)) + } else if let Some(remain) = s.strip_prefix('+') { + Ok((1.0, remain)) + } else if s.starts_with('0') { + Ok((1.0, s)) + } else { + Err(ParseNumberError::Float) + } +} + +/// Parses the `0x` prefix in a case-insensitive manner. +/// +/// # Errors +/// +/// Returns [`Err`] if the input string does not contain the required prefix. +fn parse_hex_prefix(s: &str) -> Result<&str, ParseNumberError> { + if !(s.starts_with("0x") || s.starts_with("0X")) { + return Err(ParseNumberError::Float); + } + Ok(&s[2..]) +} + +/// Parse the integral part in hexadecimal notation. +/// +/// The integral part is hexadecimal number located after the '0x' prefix and before '.' or 'p' +/// symbols. For example, the number 0x1.234p2 has an integral part 1. +/// +/// This part is optional. +/// +/// # Errors +/// +/// Returns [`Err`] if the integral part is present but a hexadecimal number cannot be parsed from the input string. +fn parse_integral_part(s: &str) -> Result<(Option<f64>, &str), ParseNumberError> { + // This part is optional. Skip parsing if symbol is not a hex digit. + let length = s.chars().take_while(|c| c.is_ascii_hexdigit()).count(); + if length > 0 { + let integer = + u64::from_str_radix(&s[..length], HEX_RADIX).map_err(|_| ParseNumberError::Float)?; + Ok((Some(integer as f64), &s[length..])) + } else { + Ok((None, s)) + } +} + +/// Parse the fractional part in hexadecimal notation. +/// +/// The function calculates the sum of the digits after the '.' (dot) sign. Each Nth digit is +/// interpreted as digit / 16^n, where n represents the position after the dot starting from 1. +/// +/// For example, the number 0x1.234p2 has a fractional part 234, which can be interpreted as +/// 2/16^1 + 3/16^2 + 4/16^3, where 16 is the radix of the hexadecimal number system. This equals +/// 0.125 + 0.01171875 + 0.0009765625 = 0.1376953125 in decimal. And this is exactly what the +/// function does. +/// +/// This part is optional. +/// +/// # Errors +/// +/// Returns [`Err`] if the fractional part is present but a hexadecimal number cannot be parsed from the input string. +fn parse_fractional_part(s: &str) -> Result<(Option<f64>, &str), ParseNumberError> { + // This part is optional and follows after the '.' symbol. Skip parsing if the dot is not present. + if !s.starts_with('.') { + return Ok((None, s)); + } + + let s = &s[1..]; + let mut multiplier = 1.0 / HEX_RADIX as f64; + let mut total = 0.0; + let mut length = 0; + + for c in s.chars().take_while(|c| c.is_ascii_hexdigit()) { + let digit = c + .to_digit(HEX_RADIX) + .map(|x| x as u8) + .ok_or(ParseNumberError::Float)?; + total += (digit as f64) * multiplier; + multiplier /= HEX_RADIX as f64; + length += 1; + } + + if length == 0 { + return Err(ParseNumberError::Float); + } + Ok((Some(total), &s[length..])) +} + +/// Parse the exponent part in hexadecimal notation. +/// +/// The exponent part is a decimal number located after the 'p' symbol. +/// For example, the number 0x1.234p2 has an exponent part 2. +/// +/// This part is optional. +/// +/// # Errors +/// +/// Returns [`Err`] if the exponent part is presented but a decimal number cannot be parsed from +/// the input string. +fn parse_exponent_part(s: &str) -> Result<(Option<i32>, &str), ParseNumberError> { + // This part is optional and follows after 'p' or 'P' symbols. Skip parsing if the symbols are not present + if !(s.starts_with('p') || s.starts_with('P')) { + return Ok((None, s)); + } + + let s = &s[1..]; + let length = s + .chars() + .take_while(|c| c.is_ascii_digit() || *c == '-' || *c == '+') + .count(); + + if length == 0 { + return Err(ParseNumberError::Float); + } + + let value = s[..length].parse().map_err(|_| ParseNumberError::Float)?; + Ok((Some(value), &s[length..])) +} + +#[cfg(test)] +mod tests { + + use super::{parse_number, parse_precision}; + use crate::{numberparse::ParseNumberError, ExtendedBigDecimal}; + use bigdecimal::BigDecimal; + use num_traits::ToPrimitive; + + fn parse_big_decimal(s: &str) -> Result<BigDecimal, ParseNumberError> { + match parse_number(s)?.number { + ExtendedBigDecimal::BigDecimal(bd) => Ok(bd), + _ => Err(ParseNumberError::Float), + } + } + + fn parse_f64(s: &str) -> Result<f64, ParseNumberError> { + parse_big_decimal(s)? + .to_f64() + .ok_or(ParseNumberError::Float) + } + + #[test] + fn test_parse_precise_number_case_insensitive() { + assert_eq!(parse_f64("0x1P1").unwrap(), 2.0); + assert_eq!(parse_f64("0x1p1").unwrap(), 2.0); + } + + #[test] + fn test_parse_precise_number_plus_minus_prefixes() { + assert_eq!(parse_f64("+0x1p1").unwrap(), 2.0); + assert_eq!(parse_f64("-0x1p1").unwrap(), -2.0); + } + + #[test] + fn test_parse_precise_number_power_signs() { + assert_eq!(parse_f64("0x1p1").unwrap(), 2.0); + assert_eq!(parse_f64("0x1p+1").unwrap(), 2.0); + assert_eq!(parse_f64("0x1p-1").unwrap(), 0.5); + } + + #[test] + fn test_parse_precise_number_hex() { + assert_eq!(parse_f64("0xd.dp-1").unwrap(), 6.90625); + } + + #[test] + fn test_parse_precise_number_no_power() { + assert_eq!(parse_f64("0x123.a").unwrap(), 291.625); + } + + #[test] + fn test_parse_precise_number_no_fractional() { + assert_eq!(parse_f64("0x333p-4").unwrap(), 51.1875); + } + + #[test] + fn test_parse_precise_number_no_integral() { + assert_eq!(parse_f64("0x.9").unwrap(), 0.5625); + assert_eq!(parse_f64("0x.9p2").unwrap(), 2.25); + } + + #[test] + fn test_parse_precise_number_from_valid_values() { + assert_eq!(parse_f64("0x1p1").unwrap(), 2.0); + assert_eq!(parse_f64("+0x1p1").unwrap(), 2.0); + assert_eq!(parse_f64("-0x1p1").unwrap(), -2.0); + assert_eq!(parse_f64("0x1p-1").unwrap(), 0.5); + assert_eq!(parse_f64("0x1.8").unwrap(), 1.5); + assert_eq!(parse_f64("-0x1.8").unwrap(), -1.5); + assert_eq!(parse_f64("0x1.8p2").unwrap(), 6.0); + assert_eq!(parse_f64("0x1.8p+2").unwrap(), 6.0); + assert_eq!(parse_f64("0x1.8p-2").unwrap(), 0.375); + assert_eq!(parse_f64("0x.8").unwrap(), 0.5); + assert_eq!(parse_f64("0x10p0").unwrap(), 16.0); + assert_eq!(parse_f64("0x0.0").unwrap(), 0.0); + assert_eq!(parse_f64("0x0p0").unwrap(), 0.0); + assert_eq!(parse_f64("0x0.0p0").unwrap(), 0.0); + assert_eq!(parse_f64("-0x.1p-3").unwrap(), -0.0078125); + assert_eq!(parse_f64("-0x.ep-3").unwrap(), -0.109375); + } + + #[test] + fn test_parse_float_from_invalid_values() { + let expected_error = ParseNumberError::Float; + assert_eq!(parse_f64("").unwrap_err(), expected_error); + assert_eq!(parse_f64("1").unwrap_err(), expected_error); + assert_eq!(parse_f64("1p").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x").unwrap_err(), expected_error); + assert_eq!(parse_f64("0xG").unwrap_err(), expected_error); + assert_eq!(parse_f64("0xp").unwrap_err(), expected_error); + assert_eq!(parse_f64("0xp3").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1.").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1p").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1p+").unwrap_err(), expected_error); + assert_eq!(parse_f64("-0xx1p1").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1.k").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1").unwrap_err(), expected_error); + assert_eq!(parse_f64("-0x1pa").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1.1pk").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1.8p2z").unwrap_err(), expected_error); + assert_eq!(parse_f64("0x1p3.2").unwrap_err(), expected_error); + assert_eq!(parse_f64("-0x.ep-3z").unwrap_err(), expected_error); + } + + #[test] + fn test_parse_precise_number_count_digits() { + let precise_num = parse_number("0x1.2").unwrap(); // 1.125 decimal + assert_eq!(precise_num.num_integral_digits, 1); + assert_eq!(precise_num.num_fractional_digits, 3); + + let precise_num = parse_number("-0x1.2").unwrap(); // -1.125 decimal + assert_eq!(precise_num.num_integral_digits, 2); + assert_eq!(precise_num.num_fractional_digits, 3); + + let precise_num = parse_number("0x123.8").unwrap(); // 291.5 decimal + assert_eq!(precise_num.num_integral_digits, 3); + assert_eq!(precise_num.num_fractional_digits, 1); + + let precise_num = parse_number("-0x123.8").unwrap(); // -291.5 decimal + assert_eq!(precise_num.num_integral_digits, 4); + assert_eq!(precise_num.num_fractional_digits, 1); + } + + #[test] + fn test_parse_precision_valid_values() { + assert_eq!(parse_precision("1"), Some(0)); + assert_eq!(parse_precision("0x1"), Some(0)); + assert_eq!(parse_precision("0x1.1"), None); + assert_eq!(parse_precision("0x1.1p2"), None); + assert_eq!(parse_precision("0x1.1p-2"), None); + assert_eq!(parse_precision(".1"), Some(1)); + assert_eq!(parse_precision("1.1"), Some(1)); + assert_eq!(parse_precision("1.12"), Some(2)); + assert_eq!(parse_precision("1.12345678"), Some(8)); + assert_eq!(parse_precision("1.12345678e-3"), Some(11)); + assert_eq!(parse_precision("1.1e-1"), Some(2)); + assert_eq!(parse_precision("1.1e-3"), Some(4)); + } + + #[test] + fn test_parse_precision_invalid_values() { + // Just to make sure it doesn't crash on incomplete values/bad format + // Good enough for now. + assert_eq!(parse_precision("1."), Some(0)); + assert_eq!(parse_precision("1e"), Some(0)); + assert_eq!(parse_precision("1e-"), Some(0)); + assert_eq!(parse_precision("1e+"), Some(0)); + assert_eq!(parse_precision("1em"), Some(0)); + } +} diff --git a/src/uu/seq/src/number.rs b/src/uu/seq/src/number.rs index 314c842ba15..ec6ac0f1687 100644 --- a/src/uu/seq/src/number.rs +++ b/src/uu/seq/src/number.rs @@ -19,6 +19,8 @@ use crate::extendedbigdecimal::ExtendedBigDecimal; pub struct PreciseNumber { pub number: ExtendedBigDecimal, pub num_integral_digits: usize, + + #[allow(dead_code)] pub num_fractional_digits: usize, } diff --git a/src/uu/seq/src/numberparse.rs b/src/uu/seq/src/numberparse.rs index c8dec018041..d00db16fa13 100644 --- a/src/uu/seq/src/numberparse.rs +++ b/src/uu/seq/src/numberparse.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore extendedbigdecimal bigdecimal numberparse +// spell-checker:ignore extendedbigdecimal bigdecimal numberparse hexadecimalfloat //! Parsing numbers for use in `seq`. //! //! This module provides an implementation of [`FromStr`] for the @@ -16,6 +16,7 @@ use num_traits::Num; use num_traits::Zero; use crate::extendedbigdecimal::ExtendedBigDecimal; +use crate::hexadecimalfloat; use crate::number::PreciseNumber; /// An error returned when parsing a number fails. @@ -102,15 +103,18 @@ fn parse_exponent_no_decimal(s: &str, j: usize) -> Result<PreciseNumber, ParseNu // displayed in decimal notation. For example, "1e-2" will be // displayed as "0.01", but "1e2" will be displayed as "100", // without a decimal point. - let x: BigDecimal = { - let parsed_decimal = s - .parse::<BigDecimal>() - .map_err(|_| ParseNumberError::Float)?; - if parsed_decimal == BigDecimal::zero() { - BigDecimal::zero() - } else { - parsed_decimal - } + + // In ['BigDecimal'], a positive scale represents a negative power of 10. + // This means the exponent value from the number must be inverted. However, + // since the |i64::MIN| > |i64::MAX| (i.e. |−2^63| > |2^63−1|) inverting a + // valid negative value could result in an overflow. To prevent this, we + // limit the minimal value with i64::MIN + 1. + let exponent = exponent.max(i64::MIN + 1); + let base: BigInt = s[..j].parse().map_err(|_| ParseNumberError::Float)?; + let x = if base.is_zero() { + BigDecimal::zero() + } else { + BigDecimal::from_bigint(base, -exponent) }; let num_integral_digits = if is_minus_zero_float(s, &x) { @@ -296,6 +300,14 @@ fn parse_decimal_and_exponent( /// assert_eq!(actual, expected); /// ``` fn parse_hexadecimal(s: &str) -> Result<PreciseNumber, ParseNumberError> { + if s.find(['.', 'p', 'P']).is_some() { + hexadecimalfloat::parse_number(s) + } else { + parse_hexadecimal_integer(s) + } +} + +fn parse_hexadecimal_integer(s: &str) -> Result<PreciseNumber, ParseNumberError> { let (is_neg, s) = if s.starts_with('-') { (true, &s[3..]) } else { @@ -590,4 +602,18 @@ mod tests { assert_eq!(num_fractional_digits("-0e-1"), 1); assert_eq!(num_fractional_digits("-0.0e-1"), 2); } + + #[test] + fn test_parse_min_exponents() { + // Make sure exponents <= i64::MIN do not cause errors + assert!("1e-9223372036854775807".parse::<PreciseNumber>().is_ok()); + assert!("1e-9223372036854775808".parse::<PreciseNumber>().is_ok()); + } + + #[test] + fn test_parse_max_exponents() { + // Make sure exponents >= i64::MAX cause errors + assert!("1e9223372036854775807".parse::<PreciseNumber>().is_err()); + assert!("1e9223372036854775808".parse::<PreciseNumber>().is_err()); + } } diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index e14ba35a949..323bf18300f 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) extendedbigdecimal numberparse +// spell-checker:ignore (ToDO) bigdecimal extendedbigdecimal numberparse hexadecimalfloat use std::ffi::OsString; use std::io::{stdout, ErrorKind, Write}; @@ -10,11 +10,13 @@ use clap::{crate_version, Arg, ArgAction, Command}; use num_traits::{ToPrimitive, Zero}; use uucore::error::{FromIo, UResult}; -use uucore::format::{num_format, Format}; +use uucore::format::{num_format, sprintf, Format, FormatArgument}; use uucore::{format_usage, help_about, help_usage}; mod error; mod extendedbigdecimal; +mod hexadecimalfloat; + // public to allow fuzzing #[cfg(fuzzing)] pub mod number; @@ -72,6 +74,18 @@ fn split_short_args_with_value(args: impl uucore::Args) -> impl uucore::Args { v.into_iter() } +fn select_precision( + first: Option<usize>, + increment: Option<usize>, + last: Option<usize>, +) -> Option<usize> { + match (first, increment, last) { + (Some(0), Some(0), Some(0)) => Some(0), + (Some(f), Some(i), Some(_)) => Some(f.max(i)), + _ => None, + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(split_short_args_with_value(args))?; @@ -87,8 +101,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let options = SeqOptions { separator: matches .get_one::<String>(OPT_SEPARATOR) - .map(|s| s.as_str()) - .unwrap_or("\n") + .map_or("\n", |s| s.as_str()) .to_string(), terminator: matches .get_one::<String>(OPT_TERMINATOR) @@ -99,32 +112,32 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { format: matches.get_one::<String>(OPT_FORMAT).map(|s| s.as_str()), }; - let first = if numbers.len() > 1 { + let (first, first_precision) = if numbers.len() > 1 { match numbers[0].parse() { - Ok(num) => num, + Ok(num) => (num, hexadecimalfloat::parse_precision(numbers[0])), Err(e) => return Err(SeqError::ParseError(numbers[0].to_string(), e).into()), } } else { - PreciseNumber::one() + (PreciseNumber::one(), Some(0)) }; - let increment = if numbers.len() > 2 { + let (increment, increment_precision) = if numbers.len() > 2 { match numbers[1].parse() { - Ok(num) => num, + Ok(num) => (num, hexadecimalfloat::parse_precision(numbers[1])), Err(e) => return Err(SeqError::ParseError(numbers[1].to_string(), e).into()), } } else { - PreciseNumber::one() + (PreciseNumber::one(), Some(0)) }; if increment.is_zero() { return Err(SeqError::ZeroIncrement(numbers[1].to_string()).into()); } - let last: PreciseNumber = { + let (last, last_precision): (PreciseNumber, Option<usize>) = { // We are guaranteed that `numbers.len()` is greater than zero // and at most three because of the argument specification in // `uu_app()`. let n: usize = numbers.len(); match numbers[n - 1].parse() { - Ok(num) => num, + Ok(num) => (num, hexadecimalfloat::parse_precision(numbers[n - 1])), Err(e) => return Err(SeqError::ParseError(numbers[n - 1].to_string(), e).into()), } }; @@ -133,30 +146,27 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .num_integral_digits .max(increment.num_integral_digits) .max(last.num_integral_digits); - let largest_dec = first - .num_fractional_digits - .max(increment.num_fractional_digits); - let format = match options.format { - Some(f) => { - let f = Format::<num_format::Float>::parse(f)?; - Some(f) - } - None => None, - }; + let precision = select_precision(first_precision, increment_precision, last_precision); + + let format = options + .format + .map(Format::<num_format::Float>::parse) + .transpose()?; + let result = print_seq( (first.number, increment.number, last.number), - largest_dec, + precision, &options.separator, &options.terminator, options.equal_width, padding, - &format, + format.as_ref(), ); match result { - Ok(_) => Ok(()), + Ok(()) => Ok(()), Err(err) if err.kind() == ErrorKind::BrokenPipe => Ok(()), - Err(e) => Err(e.map_err_context(|| "write error".into())), + Err(err) => Err(err.map_err_context(|| "write error".into())), } } @@ -210,38 +220,60 @@ fn done_printing<T: Zero + PartialOrd>(next: &T, increment: &T, last: &T) -> boo } } +fn format_bigdecimal(value: &bigdecimal::BigDecimal) -> Option<String> { + let format_arguments = &[FormatArgument::Float(value.to_f64()?)]; + let value_as_bytes = sprintf("%g", format_arguments).ok()?; + String::from_utf8(value_as_bytes).ok() +} + /// Write a big decimal formatted according to the given parameters. fn write_value_float( writer: &mut impl Write, value: &ExtendedBigDecimal, width: usize, - precision: usize, + precision: Option<usize>, ) -> std::io::Result<()> { - let value_as_str = - if *value == ExtendedBigDecimal::Infinity || *value == ExtendedBigDecimal::MinusInfinity { - format!("{value:>width$.precision$}") - } else { - format!("{value:>0width$.precision$}") - }; + let value_as_str = match precision { + // format with precision: decimal floats and integers + Some(precision) => match value { + ExtendedBigDecimal::Infinity | ExtendedBigDecimal::MinusInfinity => { + format!("{value:>width$.precision$}") + } + _ => format!("{value:>0width$.precision$}"), + }, + // format without precision: hexadecimal floats + None => match value { + ExtendedBigDecimal::BigDecimal(bd) => { + format_bigdecimal(bd).unwrap_or_else(|| "{value}".to_owned()) + } + _ => format!("{value:>0width$}"), + }, + }; write!(writer, "{value_as_str}") } /// Floating point based code path fn print_seq( range: RangeFloat, - largest_dec: usize, + precision: Option<usize>, separator: &str, terminator: &str, pad: bool, padding: usize, - format: &Option<Format<num_format::Float>>, + format: Option<&Format<num_format::Float>>, ) -> std::io::Result<()> { let stdout = stdout(); let mut stdout = stdout.lock(); let (first, increment, last) = range; let mut value = first; let padding = if pad { - padding + if largest_dec > 0 { largest_dec + 1 } else { 0 } + let precision_value = precision.unwrap_or(0); + padding + + if precision_value > 0 { + precision_value + 1 + } else { + 0 + } } else { 0 }; @@ -273,7 +305,7 @@ fn print_seq( }; f.fmt(&mut stdout, float)?; } - None => write_value_float(&mut stdout, &value, padding, largest_dec)?, + None => write_value_float(&mut stdout, &value, padding, precision)?, } // TODO Implement augmenting addition. value = value + increment.clone(); diff --git a/src/uu/shred/Cargo.toml b/src/uu/shred/Cargo.toml index 0f5f1d6aab8..10394565a37 100644 --- a/src/uu/shred/Cargo.toml +++ b/src/uu/shred/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_shred" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "shred ~ (uutils) hide former FILE contents with repeated overwrites" diff --git a/src/uu/shuf/Cargo.toml b/src/uu/shuf/Cargo.toml index 8b7ae28f8ea..f8b887b7e87 100644 --- a/src/uu/shuf/Cargo.toml +++ b/src/uu/shuf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_shuf" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "shuf ~ (uutils) display random permutations of input lines" diff --git a/src/uu/sleep/Cargo.toml b/src/uu/sleep/Cargo.toml index 8e72dd82a21..bb6e2e13103 100644 --- a/src/uu/sleep/Cargo.toml +++ b/src/uu/sleep/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sleep" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "sleep ~ (uutils) pause for DURATION" diff --git a/src/uu/sort/BENCHMARKING.md b/src/uu/sort/BENCHMARKING.md index 0cc344c3118..355245b077b 100644 --- a/src/uu/sort/BENCHMARKING.md +++ b/src/uu/sort/BENCHMARKING.md @@ -48,7 +48,7 @@ rand = "0.8.3" ```rust use rand::prelude::*; fn main() { - let suffixes = ['k', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + let suffixes = ['k', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q']; let mut rng = thread_rng(); for _ in 0..100000 { println!( diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index 799b4831d9d..d0e45a34459 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sort" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "sort ~ (uutils) sort input lines" @@ -28,6 +28,7 @@ rand = { workspace = true } rayon = { workspace = true } self_cell = { workspace = true } tempfile = { workspace = true } +thiserror = { workspace = true } unicode-width = { workspace = true } uucore = { workspace = true, features = ["fs", "version-cmp"] } diff --git a/src/uu/sort/src/ext_sort.rs b/src/uu/sort/src/ext_sort.rs index 57e434e99b2..f984760bd2a 100644 --- a/src/uu/sort/src/ext_sort.rs +++ b/src/uu/sort/src/ext_sort.rs @@ -224,11 +224,8 @@ fn read_write_loop<I: WriteableTmpFile>( let mut sender_option = Some(sender); let mut tmp_files = vec![]; loop { - let chunk = match receiver.recv() { - Ok(it) => it, - _ => { - return Ok(ReadResult::WroteChunksToFile { tmp_files }); - } + let Ok(chunk) = receiver.recv() else { + return Ok(ReadResult::WroteChunksToFile { tmp_files }); }; let tmp_file = write::<I>( diff --git a/src/uu/sort/src/numeric_str_cmp.rs b/src/uu/sort/src/numeric_str_cmp.rs index 54950f2dbfe..86cbddc6424 100644 --- a/src/uu/sort/src/numeric_str_cmp.rs +++ b/src/uu/sort/src/numeric_str_cmp.rs @@ -82,7 +82,10 @@ impl NumInfo { if Self::is_invalid_char(char, &mut had_decimal_pt, parse_settings) { return if let Some(start) = start { let has_si_unit = parse_settings.accept_si_units - && matches!(char, 'K' | 'k' | 'M' | 'G' | 'T' | 'P' | 'E' | 'Z' | 'Y'); + && matches!( + char, + 'K' | 'k' | 'M' | 'G' | 'T' | 'P' | 'E' | 'Z' | 'Y' | 'R' | 'Q' + ); ( Self { exponent, sign }, start..if has_si_unit { idx + 1 } else { idx }, @@ -176,6 +179,8 @@ fn get_unit(unit: Option<char>) -> u8 { 'E' => 6, 'Z' => 7, 'Y' => 8, + 'R' => 9, + 'Q' => 10, _ => 0, } } else { diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 8b6fcbb2514..edff2baae85 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -30,19 +30,20 @@ use rand::{thread_rng, Rng}; use rayon::prelude::*; use std::cmp::Ordering; use std::env; -use std::error::Error; use std::ffi::{OsStr, OsString}; -use std::fmt::Display; use std::fs::{File, OpenOptions}; use std::hash::{Hash, Hasher}; use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; +use std::num::IntErrorKind; use std::ops::Range; use std::path::Path; use std::path::PathBuf; use std::str::Utf8Error; +use thiserror::Error; use unicode_width::UnicodeWidthStr; use uucore::display::Quotable; -use uucore::error::{set_exit_code, strip_errno, UError, UResult, USimpleError, UUsageError}; +use uucore::error::strip_errno; +use uucore::error::{set_exit_code, UError, UResult, USimpleError, UUsageError}; use uucore::line_ending::LineEnding; use uucore::parse_size::{ParseSizeError, Parser}; use uucore::shortcut_value_parser::ShortcutValueParser; @@ -119,44 +120,43 @@ const POSITIVE: char = '+'; // available memory into consideration, instead of relying on this constant only. const DEFAULT_BUF_SIZE: usize = 1_000_000_000; // 1 GB -#[derive(Debug)] -enum SortError { +#[derive(Debug, Error)] +pub enum SortError { + #[error("{}", format_disorder(.file, .line_number, .line, .silent))] Disorder { file: OsString, line_number: usize, line: String, silent: bool, }, - OpenFailed { - path: String, - error: std::io::Error, - }, + + #[error("open failed: {}: {}", .path.maybe_quote(), strip_errno(.error))] + OpenFailed { path: String, error: std::io::Error }, + + #[error("failed to parse key {}: {}", .key.quote(), .msg)] + ParseKeyError { key: String, msg: String }, + + #[error("cannot read: {}: {}", .path.maybe_quote(), strip_errno(.error))] ReadFailed { path: PathBuf, error: std::io::Error, }, - ParseKeyError { - key: String, - msg: String, - }, - OpenTmpFileFailed { - error: std::io::Error, - }, - CompressProgExecutionFailed { - code: i32, - }, - CompressProgTerminatedAbnormally { - prog: String, - }, - TmpFileCreationFailed { - path: PathBuf, - }, - Uft8Error { - error: Utf8Error, - }, -} -impl Error for SortError {} + #[error("failed to open temporary file: {}", strip_errno(.error))] + OpenTmpFileFailed { error: std::io::Error }, + + #[error("couldn't execute compress program: errno {code}")] + CompressProgExecutionFailed { code: i32 }, + + #[error("{} terminated abnormally", .prog.quote())] + CompressProgTerminatedAbnormally { prog: String }, + + #[error("cannot create temporary file in '{}':", .path.display())] + TmpFileCreationFailed { path: PathBuf }, + + #[error("{error}")] + Uft8Error { error: Utf8Error }, +} impl UError for SortError { fn code(&self) -> i32 { @@ -167,60 +167,11 @@ impl UError for SortError { } } -impl Display for SortError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Disorder { - file, - line_number, - line, - silent, - } => { - if *silent { - Ok(()) - } else { - write!( - f, - "{}:{}: disorder: {}", - file.maybe_quote(), - line_number, - line - ) - } - } - Self::OpenFailed { path, error } => { - write!( - f, - "open failed: {}: {}", - path.maybe_quote(), - strip_errno(error) - ) - } - Self::ParseKeyError { key, msg } => { - write!(f, "failed to parse key {}: {}", key.quote(), msg) - } - Self::ReadFailed { path, error } => { - write!( - f, - "cannot read: {}: {}", - path.maybe_quote(), - strip_errno(error) - ) - } - Self::OpenTmpFileFailed { error } => { - write!(f, "failed to open temporary file: {}", strip_errno(error)) - } - Self::CompressProgExecutionFailed { code } => { - write!(f, "couldn't execute compress program: errno {code}") - } - Self::CompressProgTerminatedAbnormally { prog } => { - write!(f, "{} terminated abnormally", prog.quote()) - } - Self::TmpFileCreationFailed { path } => { - write!(f, "cannot create temporary file in '{}':", path.display()) - } - Self::Uft8Error { error } => write!(f, "{error}"), - } +fn format_disorder(file: &OsString, line_number: &usize, line: &String, silent: &bool) -> String { + if *silent { + String::new() + } else { + format!("{}:{}: disorder: {}", file.maybe_quote(), line_number, line) } } @@ -338,7 +289,7 @@ impl GlobalSettings { // GNU sort (8.32) invalid: b, B, 1B, p, e, z, y let size = Parser::default() .with_allow_list(&[ - "b", "k", "K", "m", "M", "g", "G", "t", "T", "P", "E", "Z", "Y", + "b", "k", "K", "m", "M", "g", "G", "t", "T", "P", "E", "Z", "Y", "R", "Q", "%", ]) .with_default_unit("K") .with_b_byte_count(true) @@ -584,8 +535,9 @@ impl<'a> Line<'a> { } else { // include a trailing si unit if selector.settings.mode == SortMode::HumanNumeric - && self.line[selection.end..initial_selection.end] - .starts_with(&['k', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'][..]) + && self.line[selection.end..initial_selection.end].starts_with( + &['k', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q'][..], + ) { selection.end += 1; } @@ -746,9 +698,17 @@ impl KeyPosition { .ok_or_else(|| format!("invalid key {}", key.quote()))?; let char = field_and_char.next(); - let field = field - .parse() - .map_err(|e| format!("failed to parse field index {}: {}", field.quote(), e))?; + let field = match field.parse::<usize>() { + Ok(f) => f, + Err(e) if *e.kind() == IntErrorKind::PosOverflow => usize::MAX, + Err(e) => { + return Err(format!( + "failed to parse field index {} {}", + field.quote(), + e + )) + } + }; if field == 0 { return Err("field index can not be 0".to_string()); } @@ -1411,14 +1371,14 @@ pub fn uu_app() -> Command { options::check::QUIET, options::check::DIAGNOSE_FIRST, ])) - .conflicts_with(options::OUTPUT) + .conflicts_with_all([options::OUTPUT, options::check::CHECK_SILENT]) .help("check for sorted input; do not sort"), ) .arg( Arg::new(options::check::CHECK_SILENT) .short('C') .long(options::check::CHECK_SILENT) - .conflicts_with(options::OUTPUT) + .conflicts_with_all([options::OUTPUT, options::check::CHECK]) .help( "exit successfully if the given file is already sorted, \ and exit with status 1 otherwise.", @@ -1895,7 +1855,9 @@ fn format_error_message(error: &ParseSizeError, s: &str, option: &str) -> String ParseSizeError::InvalidSuffix(_) => { format!("invalid suffix in --{} argument {}", option, s.quote()) } - ParseSizeError::ParseFailure(_) => format!("invalid --{} argument {}", option, s.quote()), + ParseSizeError::ParseFailure(_) | ParseSizeError::PhysicalMem(_) => { + format!("invalid --{} argument {}", option, s.quote()) + } ParseSizeError::SizeTooBig(_) => format!("--{} argument {} too large", option, s.quote()), } } diff --git a/src/uu/split/Cargo.toml b/src/uu/split/Cargo.toml index cc69a6bc4cb..8e09eb76d6b 100644 --- a/src/uu/split/Cargo.toml +++ b/src/uu/split/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_split" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "split ~ (uutils) split input into output files" diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 279e91daea1..9c5f1f4d1ee 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -919,204 +919,6 @@ impl Write for LineChunkWriter<'_> { } } -/// Write lines to each sequential output files, limited by bytes. -/// -/// This struct maintains an underlying writer representing the -/// current chunk of the output. On each call to [`write`], it writes -/// as many lines as possible to the current chunk without exceeding -/// the specified byte limit. If a single line has more bytes than the -/// limit, then fill an entire single chunk with those bytes and -/// handle the remainder of the line as if it were its own distinct -/// line. As many new underlying writers are created as needed to -/// write all the data in the input buffer. -struct LineBytesChunkWriter<'a> { - /// Parameters for creating the underlying writer for each new chunk. - settings: &'a Settings, - - /// The maximum number of bytes allowed for a single chunk of output. - chunk_size: u64, - - /// Running total of number of chunks that have been completed. - num_chunks_written: usize, - - /// Remaining capacity in number of bytes in the current chunk. - /// - /// This number starts at `chunk_size` and decreases as lines are - /// written. Once it reaches zero, a writer for a new chunk is - /// initialized and this number gets reset to `chunk_size`. - num_bytes_remaining_in_current_chunk: usize, - - /// The underlying writer for the current chunk. - /// - /// Once the number of bytes written to this writer exceeds - /// `chunk_size`, a new writer is initialized and assigned to this - /// field. - inner: BufWriter<Box<dyn Write>>, - - /// Iterator that yields filenames for each chunk. - filename_iterator: FilenameIterator<'a>, -} - -impl<'a> LineBytesChunkWriter<'a> { - fn new(chunk_size: u64, settings: &'a Settings) -> UResult<Self> { - let mut filename_iterator = FilenameIterator::new(&settings.prefix, &settings.suffix)?; - let filename = filename_iterator - .next() - .ok_or_else(|| USimpleError::new(1, "output file suffixes exhausted"))?; - if settings.verbose { - println!("creating file {}", filename.quote()); - } - let inner = settings.instantiate_current_writer(&filename, true)?; - Ok(LineBytesChunkWriter { - settings, - chunk_size, - num_bytes_remaining_in_current_chunk: usize::try_from(chunk_size).unwrap(), - num_chunks_written: 0, - inner, - filename_iterator, - }) - } -} - -impl Write for LineBytesChunkWriter<'_> { - /// Write as many lines to a chunk as possible without - /// exceeding the byte limit. If a single line has more bytes - /// than the limit, then fill an entire single chunk with those - /// bytes and handle the remainder of the line as if it were - /// its own distinct line. - /// - /// For example: if the `chunk_size` is 8 and the input is: - /// - /// ```text - /// aaaaaaaaa\nbbbb\ncccc\ndd\nee\n - /// ``` - /// - /// then the output gets broken into chunks like this: - /// - /// ```text - /// chunk 0 chunk 1 chunk 2 chunk 3 - /// - /// 0 1 2 - /// 01234567 89 01234 56789 012 345 6 - /// |------| |-------| |--------| |---| - /// aaaaaaaa a\nbbbb\n cccc\ndd\n ee\n - /// ``` - /// - /// Implements `--line-bytes=SIZE` - fn write(&mut self, mut buf: &[u8]) -> std::io::Result<usize> { - // The total number of bytes written during the loop below. - // - // It is necessary to keep this running total because we may - // be making multiple calls to `write()` on multiple different - // underlying writers and we want the final reported number of - // bytes written to reflect the total number of bytes written - // to all of the underlying writers. - let mut total_bytes_written = 0; - - // Loop until we have written all bytes in the input buffer - // (or an IO error occurs). - loop { - // If the buffer is empty, then we are done writing. - if buf.is_empty() { - return Ok(total_bytes_written); - } - - // If we have filled the current chunk with bytes, then - // start a new chunk and initialize its corresponding - // writer. - if self.num_bytes_remaining_in_current_chunk == 0 { - self.num_chunks_written += 1; - let filename = self.filename_iterator.next().ok_or_else(|| { - std::io::Error::new(ErrorKind::Other, "output file suffixes exhausted") - })?; - if self.settings.verbose { - println!("creating file {}", filename.quote()); - } - self.inner = self.settings.instantiate_current_writer(&filename, true)?; - self.num_bytes_remaining_in_current_chunk = self.chunk_size.try_into().unwrap(); - } - - // Find the first separator (default - newline character) in the buffer. - let sep = self.settings.separator; - match memchr::memchr(sep, buf) { - // If there is no separator character and the buffer is - // not empty, then write as many bytes as we can and - // then move on to the next chunk if necessary. - None => { - let end = self.num_bytes_remaining_in_current_chunk; - - // This is ugly but here to match GNU behavior. If the input - // doesn't end with a separator, pretend that it does for handling - // the second to last segment chunk. See `line-bytes.sh`. - if end == buf.len() - && self.num_bytes_remaining_in_current_chunk - < self.chunk_size.try_into().unwrap() - && buf[buf.len() - 1] != sep - { - self.num_bytes_remaining_in_current_chunk = 0; - } else { - let num_bytes_written = custom_write( - &buf[..end.min(buf.len())], - &mut self.inner, - self.settings, - )?; - self.num_bytes_remaining_in_current_chunk -= num_bytes_written; - total_bytes_written += num_bytes_written; - buf = &buf[num_bytes_written..]; - } - } - - // If there is a separator character and the line - // (including the separator character) will fit in the - // current chunk, then write the entire line and - // continue to the next iteration. (See chunk 1 in the - // example comment above.) - Some(i) if i < self.num_bytes_remaining_in_current_chunk => { - let num_bytes_written = - custom_write(&buf[..=i], &mut self.inner, self.settings)?; - self.num_bytes_remaining_in_current_chunk -= num_bytes_written; - total_bytes_written += num_bytes_written; - buf = &buf[num_bytes_written..]; - } - - // If there is a separator character, the line - // (including the separator character) will not fit in - // the current chunk, *and* no other lines have been - // written to the current chunk, then write as many - // bytes as we can and continue to the next - // iteration. (See chunk 0 in the example comment - // above.) - Some(_) - if self.num_bytes_remaining_in_current_chunk - == self.chunk_size.try_into().unwrap() => - { - let end = self.num_bytes_remaining_in_current_chunk; - let num_bytes_written = - custom_write(&buf[..end], &mut self.inner, self.settings)?; - self.num_bytes_remaining_in_current_chunk -= num_bytes_written; - total_bytes_written += num_bytes_written; - buf = &buf[num_bytes_written..]; - } - - // If there is a separator character, the line - // (including the separator character) will not fit in - // the current chunk, and at least one other line has - // been written to the current chunk, then signal to - // the next iteration that a new chunk needs to be - // created and continue to the next iteration of the - // loop to try writing the line there. - Some(_) => { - self.num_bytes_remaining_in_current_chunk = 0; - } - } - } - } - - fn flush(&mut self) -> std::io::Result<()> { - self.inner.flush() - } -} - /// Output file parameters struct OutFile { filename: String, @@ -1606,18 +1408,15 @@ where }; let bytes = line.as_slice(); - match kth_chunk { - Some(chunk_number) => { - if (i % num_chunks) == (chunk_number - 1) as usize { - stdout_writer.write_all(bytes)?; - } + if let Some(chunk_number) = kth_chunk { + if (i % num_chunks) == (chunk_number - 1) as usize { + stdout_writer.write_all(bytes)?; } - None => { - let writer = out_files.get_writer(i % num_chunks, settings)?; - let writer_stdin_open = custom_write_all(bytes, writer, settings)?; - if !writer_stdin_open { - closed_writers += 1; - } + } else { + let writer = out_files.get_writer(i % num_chunks, settings)?; + let writer_stdin_open = custom_write_all(bytes, writer, settings)?; + if !writer_stdin_open { + closed_writers += 1; } } i += 1; @@ -1629,6 +1428,114 @@ where Ok(()) } +/// Like `std::io::Lines`, but includes the line ending character. +/// +/// This struct is generally created by calling `lines_with_sep` on a +/// reader. +pub struct LinesWithSep<R> { + inner: R, + separator: u8, +} + +impl<R> Iterator for LinesWithSep<R> +where + R: BufRead, +{ + type Item = std::io::Result<Vec<u8>>; + + /// Read bytes from a buffer up to the requested number of lines. + fn next(&mut self) -> Option<Self::Item> { + let mut buf = vec![]; + match self.inner.read_until(self.separator, &mut buf) { + Ok(0) => None, + Ok(_) => Some(Ok(buf)), + Err(e) => Some(Err(e)), + } + } +} + +/// Like `std::str::lines` but includes the line ending character. +/// +/// The `separator` defines the character to interpret as the line +/// ending. For the usual notion of "line", set this to `b'\n'`. +pub fn lines_with_sep<R>(reader: R, separator: u8) -> LinesWithSep<R> +where + R: BufRead, +{ + LinesWithSep { + inner: reader, + separator, + } +} + +fn line_bytes<R>(settings: &Settings, reader: &mut R, chunk_size: usize) -> UResult<()> +where + R: BufRead, +{ + let mut filename_iterator = FilenameIterator::new(&settings.prefix, &settings.suffix)?; + + // Initialize the writer just to satisfy the compiler. It is going + // to be overwritten for sure at the beginning of the loop below + // because we start with `remaining == 0`, indicating that a new + // chunk should start. + let mut writer: BufWriter<Box<dyn Write>> = + BufWriter::new(Box::new(std::io::Cursor::new(vec![]))); + + let mut remaining = 0; + for line in lines_with_sep(reader, settings.separator) { + let line = line?; + let mut line = &line[..]; + loop { + if remaining == 0 { + let filename = filename_iterator + .next() + .ok_or_else(|| USimpleError::new(1, "output file suffixes exhausted"))?; + if settings.verbose { + println!("creating file {}", filename.quote()); + } + writer = settings.instantiate_current_writer(&filename, true)?; + remaining = chunk_size; + } + + // Special case: if this is the last line and it doesn't end + // with a newline character, then count its length as though + // it did end with a newline. If that puts it over the edge + // of this chunk, continue to the next chunk. + if line.len() == remaining + && remaining < chunk_size + && line[line.len() - 1] != settings.separator + { + remaining = 0; + continue; + } + + // If the entire line fits in this chunk, write it and + // continue to the next line. + if line.len() <= remaining { + custom_write_all(line, &mut writer, settings)?; + remaining -= line.len(); + break; + } + + // If the line is too large to fit in *any* chunk and we are + // at the start of a new chunk, write as much as we can of + // it and pass the remainder along to the next chunk. + if line.len() > chunk_size && remaining == chunk_size { + custom_write_all(&line[..chunk_size], &mut writer, settings)?; + line = &line[chunk_size..]; + remaining = 0; + continue; + } + + // If the line is too large to fit in *this* chunk, but + // might otherwise fit in the next chunk, then just continue + // to the next chunk and let it be handled there. + remaining = 0; + } + } + Ok(()) +} + #[allow(clippy::cognitive_complexity)] fn split(settings: &Settings) -> UResult<()> { let r_box = if settings.input == "-" { @@ -1701,23 +1608,6 @@ fn split(settings: &Settings) -> UResult<()> { }, } } - Strategy::LineBytes(chunk_size) => { - let mut writer = LineBytesChunkWriter::new(chunk_size, settings)?; - match std::io::copy(&mut reader, &mut writer) { - Ok(_) => Ok(()), - Err(e) => match e.kind() { - // TODO Since the writer object controls the creation of - // new files, we need to rely on the `std::io::Result` - // returned by its `write()` method to communicate any - // errors to this calling scope. If a new file cannot be - // created because we have exceeded the number of - // allowable filenames, we use `ErrorKind::Other` to - // indicate that. A special error message needs to be - // printed in that case. - ErrorKind::Other => Err(USimpleError::new(1, format!("{e}"))), - _ => Err(uio_error!(e, "input/output error")), - }, - } - } + Strategy::LineBytes(chunk_size) => line_bytes(settings, &mut reader, chunk_size as usize), } } diff --git a/src/uu/stat/Cargo.toml b/src/uu/stat/Cargo.toml index 191183cfa41..c503426d142 100644 --- a/src/uu/stat/Cargo.toml +++ b/src/uu/stat/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stat" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "stat ~ (uutils) display FILE status" diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index 5e617e7a31a..a6220267314 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -94,6 +94,7 @@ pub enum OutputType { Unsigned(u64), UnsignedHex(u64), UnsignedOct(u32), + Float(f64), Unknown, } @@ -120,6 +121,13 @@ impl std::str::FromStr for QuotingStyle { } } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum Precision { + NotSpecified, + NoNumber, + Number(usize), +} + #[derive(Debug, PartialEq, Eq)] enum Token { Char(char), @@ -127,7 +135,7 @@ enum Token { Directive { flag: Flags, width: usize, - precision: Option<usize>, + precision: Precision, format: char, }, } @@ -238,10 +246,10 @@ struct Stater { /// * `output` - A reference to the OutputType enum containing the value to be printed. /// * `flags` - A Flags struct containing formatting flags. /// * `width` - The width of the field for the printed output. -/// * `precision` - An Option containing the precision value. +/// * `precision` - How many digits of precision, if any. /// /// This function delegates the printing process to more specialized functions depending on the output type. -fn print_it(output: &OutputType, flags: Flags, width: usize, precision: Option<usize>) { +fn print_it(output: &OutputType, flags: Flags, width: usize, precision: Precision) { // If the precision is given as just '.', the precision is taken to be zero. // A negative precision is taken as if the precision were omitted. // This gives the minimum number of digits to appear for d, i, o, u, x, and X conversions, @@ -271,7 +279,7 @@ fn print_it(output: &OutputType, flags: Flags, width: usize, precision: Option<u // A sign (+ or -) should always be placed before a number produced by a signed conversion. // By default, a sign is used only for negative numbers. // A + overrides a space if both are used. - let padding_char = determine_padding_char(&flags, &precision); + let padding_char = determine_padding_char(&flags); match output { OutputType::Str(s) => print_str(s, &flags, width, precision), @@ -283,6 +291,9 @@ fn print_it(output: &OutputType, flags: Flags, width: usize, precision: Option<u OutputType::UnsignedHex(num) => { print_unsigned_hex(*num, &flags, width, precision, padding_char); } + OutputType::Float(num) => { + print_float(*num, &flags, width, precision, padding_char); + } OutputType::Unknown => print!("?"), } } @@ -292,13 +303,12 @@ fn print_it(output: &OutputType, flags: Flags, width: usize, precision: Option<u /// # Arguments /// /// * `flags` - A reference to the Flags struct containing formatting flags. -/// * `precision` - An Option containing the precision value. /// /// # Returns /// /// * Padding - An instance of the Padding enum representing the padding character. -fn determine_padding_char(flags: &Flags, precision: &Option<usize>) -> Padding { - if flags.zero && !flags.left && precision.is_none() { +fn determine_padding_char(flags: &Flags) -> Padding { + if flags.zero && !flags.left { Padding::Zero } else { Padding::Space @@ -312,10 +322,10 @@ fn determine_padding_char(flags: &Flags, precision: &Option<usize>) -> Padding { /// * `s` - The string to be printed. /// * `flags` - A reference to the Flags struct containing formatting flags. /// * `width` - The width of the field for the printed string. -/// * `precision` - An Option containing the precision value. -fn print_str(s: &str, flags: &Flags, width: usize, precision: Option<usize>) { +/// * `precision` - How many digits of precision, if any. +fn print_str(s: &str, flags: &Flags, width: usize, precision: Precision) { let s = match precision { - Some(p) if p < s.len() => &s[..p], + Precision::Number(p) if p < s.len() => &s[..p], _ => s, }; pad_and_print(s, flags.left, width, Padding::Space); @@ -415,13 +425,13 @@ fn process_token_filesystem(t: &Token, meta: StatFs, display_name: &str) { /// * `num` - The integer value to be printed. /// * `flags` - A reference to the Flags struct containing formatting flags. /// * `width` - The width of the field for the printed integer. -/// * `precision` - An Option containing the precision value. +/// * `precision` - How many digits of precision, if any. /// * `padding_char` - The padding character as determined by `determine_padding_char`. fn print_integer( num: i64, flags: &Flags, width: usize, - precision: Option<usize>, + precision: Precision, padding_char: Padding, ) { let num = num.to_string(); @@ -437,13 +447,66 @@ fn print_integer( } else { "" }; - let extended = format!( - "{prefix}{arg:0>precision$}", - precision = precision.unwrap_or(0) - ); + let extended = match precision { + Precision::NotSpecified => format!("{prefix}{arg}"), + Precision::NoNumber => format!("{prefix}{arg}"), + Precision::Number(p) => format!("{prefix}{arg:0>precision$}", precision = p), + }; pad_and_print(&extended, flags.left, width, padding_char); } +/// Truncate a float to the given number of digits after the decimal point. +fn precision_trunc(num: f64, precision: Precision) -> String { + // GNU `stat` doesn't round, it just seems to truncate to the + // given precision: + // + // $ stat -c "%.5Y" /dev/pts/ptmx + // 1736344012.76399 + // $ stat -c "%.4Y" /dev/pts/ptmx + // 1736344012.7639 + // $ stat -c "%.3Y" /dev/pts/ptmx + // 1736344012.763 + // + // Contrast this with `printf`, which seems to round the + // numbers: + // + // $ printf "%.5f\n" 1736344012.76399 + // 1736344012.76399 + // $ printf "%.4f\n" 1736344012.76399 + // 1736344012.7640 + // $ printf "%.3f\n" 1736344012.76399 + // 1736344012.764 + // + let num_str = num.to_string(); + let n = num_str.len(); + match (num_str.find('.'), precision) { + (None, Precision::NotSpecified) => num_str, + (None, Precision::NoNumber) => num_str, + (None, Precision::Number(0)) => num_str, + (None, Precision::Number(p)) => format!("{num_str}.{zeros}", zeros = "0".repeat(p)), + (Some(i), Precision::NotSpecified) => num_str[..i].to_string(), + (Some(_), Precision::NoNumber) => num_str, + (Some(i), Precision::Number(0)) => num_str[..i].to_string(), + (Some(i), Precision::Number(p)) if p < n - i => num_str[..i + 1 + p].to_string(), + (Some(i), Precision::Number(p)) => { + format!("{num_str}{zeros}", zeros = "0".repeat(p - (n - i - 1))) + } + } +} + +fn print_float(num: f64, flags: &Flags, width: usize, precision: Precision, padding_char: Padding) { + let prefix = if flags.sign { + "+" + } else if flags.space { + " " + } else { + "" + }; + let num_str = precision_trunc(num, precision); + let extended = format!("{prefix}{num_str}"); + pad_and_print(&extended, flags.left, width, padding_char) +} + /// Prints an unsigned integer value based on the provided flags, width, and precision. /// /// # Arguments @@ -451,13 +514,13 @@ fn print_integer( /// * `num` - The unsigned integer value to be printed. /// * `flags` - A reference to the Flags struct containing formatting flags. /// * `width` - The width of the field for the printed unsigned integer. -/// * `precision` - An Option containing the precision value. +/// * `precision` - How many digits of precision, if any. /// * `padding_char` - The padding character as determined by `determine_padding_char`. fn print_unsigned( num: u64, flags: &Flags, width: usize, - precision: Option<usize>, + precision: Precision, padding_char: Padding, ) { let num = num.to_string(); @@ -466,7 +529,11 @@ fn print_unsigned( } else { Cow::Borrowed(num.as_str()) }; - let s = format!("{s:0>precision$}", precision = precision.unwrap_or(0)); + let s = match precision { + Precision::NotSpecified => s, + Precision::NoNumber => s, + Precision::Number(p) => format!("{s:0>precision$}", precision = p).into(), + }; pad_and_print(&s, flags.left, width, padding_char); } @@ -477,20 +544,21 @@ fn print_unsigned( /// * `num` - The unsigned octal integer value to be printed. /// * `flags` - A reference to the Flags struct containing formatting flags. /// * `width` - The width of the field for the printed unsigned octal integer. -/// * `precision` - An Option containing the precision value. +/// * `precision` - How many digits of precision, if any. /// * `padding_char` - The padding character as determined by `determine_padding_char`. fn print_unsigned_oct( num: u32, flags: &Flags, width: usize, - precision: Option<usize>, + precision: Precision, padding_char: Padding, ) { let prefix = if flags.alter { "0" } else { "" }; - let s = format!( - "{prefix}{num:0>precision$o}", - precision = precision.unwrap_or(0) - ); + let s = match precision { + Precision::NotSpecified => format!("{prefix}{num:o}"), + Precision::NoNumber => format!("{prefix}{num:o}"), + Precision::Number(p) => format!("{prefix}{num:0>precision$o}", precision = p), + }; pad_and_print(&s, flags.left, width, padding_char); } @@ -501,20 +569,21 @@ fn print_unsigned_oct( /// * `num` - The unsigned hexadecimal integer value to be printed. /// * `flags` - A reference to the Flags struct containing formatting flags. /// * `width` - The width of the field for the printed unsigned hexadecimal integer. -/// * `precision` - An Option containing the precision value. +/// * `precision` - How many digits of precision, if any. /// * `padding_char` - The padding character as determined by `determine_padding_char`. fn print_unsigned_hex( num: u64, flags: &Flags, width: usize, - precision: Option<usize>, + precision: Precision, padding_char: Padding, ) { let prefix = if flags.alter { "0x" } else { "" }; - let s = format!( - "{prefix}{num:0>precision$x}", - precision = precision.unwrap_or(0) - ); + let s = match precision { + Precision::NotSpecified => format!("{prefix}{num:x}"), + Precision::NoNumber => format!("{prefix}{num:x}"), + Precision::Number(p) => format!("{prefix}{num:0>precision$x}", precision = p), + }; pad_and_print(&s, flags.left, width, padding_char); } @@ -530,6 +599,10 @@ impl Stater { '0' => flag.zero = true, '-' => flag.left = true, ' ' => flag.space = true, + // This is not documented but the behavior seems to be + // the same as a space. For example `stat -c "%I5s" f` + // prints " 0". + 'I' => flag.space = true, '+' => flag.sign = true, '\'' => flag.group = true, _ => break, @@ -560,7 +633,7 @@ impl Stater { Self::process_flags(chars, i, bound, &mut flag); let mut width = 0; - let mut precision = None; + let mut precision = Precision::NotSpecified; let mut j = *i; if let Some((field_width, offset)) = format_str[j..].scan_num::<usize>() { @@ -585,11 +658,11 @@ impl Stater { match format_str[j..].scan_num::<i32>() { Some((value, offset)) => { if value >= 0 { - precision = Some(value as usize); + precision = Precision::Number(value as usize); } j += offset; } - None => precision = Some(0), + None => precision = Precision::NoNumber, } check_bound(format_str, bound, old, j)?; } @@ -898,7 +971,24 @@ impl Stater { // time of last data modification, human-readable 'y' => OutputType::Str(pretty_time(meta.mtime(), meta.mtime_nsec())), // time of last data modification, seconds since Epoch - 'Y' => OutputType::Integer(meta.mtime()), + 'Y' => { + let sec = meta.mtime(); + let nsec = meta.mtime_nsec(); + let tm = + chrono::DateTime::from_timestamp(sec, nsec as u32).unwrap_or_default(); + let tm: DateTime<Local> = tm.into(); + match tm.timestamp_nanos_opt() { + None => { + let micros = tm.timestamp_micros(); + let secs = micros as f64 / 1_000_000.0; + OutputType::Float(secs) + } + Some(ns) => { + let secs = ns as f64 / 1_000_000_000.0; + OutputType::Float(secs) + } + } + } // time of last status change, human-readable 'z' => OutputType::Str(pretty_time(meta.ctime(), meta.ctime_nsec())), // time of last status change, seconds since Epoch @@ -1107,7 +1197,7 @@ fn pretty_time(sec: i64, nsec: i64) -> String { #[cfg(test)] mod tests { - use super::{group_num, Flags, ScanUtil, Stater, Token}; + use super::{group_num, precision_trunc, Flags, Precision, ScanUtil, Stater, Token}; #[test] fn test_scanners() { @@ -1155,7 +1245,7 @@ mod tests { ..Default::default() }, width: 10, - precision: Some(2), + precision: Precision::Number(2), format: 'a', }, Token::Char('c'), @@ -1166,7 +1256,7 @@ mod tests { ..Default::default() }, width: 5, - precision: Some(0), + precision: Precision::NoNumber, format: 'w', }, Token::Char('\n'), @@ -1186,7 +1276,7 @@ mod tests { ..Default::default() }, width: 15, - precision: None, + precision: Precision::NotSpecified, format: 'a', }, Token::Byte(b'\t'), @@ -1205,7 +1295,7 @@ mod tests { ..Default::default() }, width: 20, - precision: None, + precision: Precision::NotSpecified, format: 'w', }, Token::Byte(b'\x12'), @@ -1216,4 +1306,16 @@ mod tests { ]; assert_eq!(&expected, &Stater::generate_tokens(s, true).unwrap()); } + + #[test] + fn test_precision_trunc() { + assert_eq!(precision_trunc(123.456, Precision::NotSpecified), "123"); + assert_eq!(precision_trunc(123.456, Precision::NoNumber), "123.456"); + assert_eq!(precision_trunc(123.456, Precision::Number(0)), "123"); + assert_eq!(precision_trunc(123.456, Precision::Number(1)), "123.4"); + assert_eq!(precision_trunc(123.456, Precision::Number(2)), "123.45"); + assert_eq!(precision_trunc(123.456, Precision::Number(3)), "123.456"); + assert_eq!(precision_trunc(123.456, Precision::Number(4)), "123.4560"); + assert_eq!(precision_trunc(123.456, Precision::Number(5)), "123.45600"); + } } diff --git a/src/uu/stdbuf/Cargo.toml b/src/uu/stdbuf/Cargo.toml index 0e17c3441e3..75af9db3960 100644 --- a/src/uu/stdbuf/Cargo.toml +++ b/src/uu/stdbuf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stdbuf" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "stdbuf ~ (uutils) run COMMAND with modified standard stream buffering" @@ -22,7 +22,7 @@ tempfile = { workspace = true } uucore = { workspace = true } [build-dependencies] -libstdbuf = { version = "0.0.28", package = "uu_stdbuf_libstdbuf", path = "src/libstdbuf" } +libstdbuf = { version = "0.0.29", package = "uu_stdbuf_libstdbuf", path = "src/libstdbuf" } [[bin]] name = "stdbuf" diff --git a/src/uu/stdbuf/src/libstdbuf/Cargo.toml b/src/uu/stdbuf/src/libstdbuf/Cargo.toml index 67a7e903e04..a49832b3434 100644 --- a/src/uu/stdbuf/src/libstdbuf/Cargo.toml +++ b/src/uu/stdbuf/src/libstdbuf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stdbuf_libstdbuf" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "stdbuf/libstdbuf ~ (uutils); dynamic library required for stdbuf" diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index bc7b2394911..4540c60d89f 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -157,9 +157,24 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { set_command_env(&mut command, "_STDBUF_E", &options.stderr); command.args(command_params); - let mut process = command - .spawn() - .map_err_context(|| "failed to execute process".to_string())?; + const EXEC_ERROR: &str = "failed to execute process:"; + let mut process = match command.spawn() { + Ok(p) => p, + Err(e) => { + return match e.kind() { + std::io::ErrorKind::PermissionDenied => Err(USimpleError::new( + 126, + format!("{EXEC_ERROR} Permission denied"), + )), + std::io::ErrorKind::NotFound => Err(USimpleError::new( + 127, + format!("{EXEC_ERROR} No such file or directory"), + )), + _ => Err(USimpleError::new(1, format!("{EXEC_ERROR} {}", e))), + } + } + }; + let status = process.wait().map_err_context(String::new)?; match status.code() { Some(i) => { diff --git a/src/uu/stty/Cargo.toml b/src/uu/stty/Cargo.toml index c38df5819d9..7d34d13f1d4 100644 --- a/src/uu/stty/Cargo.toml +++ b/src/uu/stty/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_stty" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "stty ~ (uutils) print or change terminal characteristics" diff --git a/src/uu/sum/Cargo.toml b/src/uu/sum/Cargo.toml index 548bc44e072..1995f11df85 100644 --- a/src/uu/sum/Cargo.toml +++ b/src/uu/sum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sum" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "sum ~ (uutils) display checksum and block counts for input" diff --git a/src/uu/sync/Cargo.toml b/src/uu/sync/Cargo.toml index 5c02cada17e..8ce6cb73adb 100644 --- a/src/uu/sync/Cargo.toml +++ b/src/uu/sync/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_sync" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "sync ~ (uutils) synchronize cache writes to storage" diff --git a/src/uu/tac/Cargo.toml b/src/uu/tac/Cargo.toml index 60d4f3b3e7e..2d9aedb8e40 100644 --- a/src/uu/tac/Cargo.toml +++ b/src/uu/tac/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "uu_tac" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "tac ~ (uutils) concatenate and display input lines in reverse order" diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index d687a6f1d31..011ee31ceb4 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore (libs) kqueue fundu [package] name = "uu_tail" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "tail ~ (uutils) display the last lines of input" diff --git a/src/uu/tail/src/parse.rs b/src/uu/tail/src/parse.rs index 6d6826077f8..2e768d1c913 100644 --- a/src/uu/tail/src/parse.rs +++ b/src/uu/tail/src/parse.rs @@ -34,9 +34,8 @@ pub enum ParseError { /// Parses obsolete syntax /// tail -\[NUM\]\[bcl\]\[f\] and tail +\[NUM\]\[bcl\]\[f\] pub fn parse_obsolete(src: &OsString) -> Option<Result<ObsoleteArgs, ParseError>> { - let mut rest = match src.to_str() { - Some(src) => src, - None => return Some(Err(ParseError::InvalidEncoding)), + let Some(mut rest) = src.to_str() else { + return Some(Err(ParseError::InvalidEncoding)); }; let sign = if let Some(r) = rest.strip_prefix('-') { rest = r; @@ -86,9 +85,8 @@ pub fn parse_obsolete(src: &OsString) -> Option<Result<ObsoleteArgs, ParseError> } let multiplier = if mode == 'b' { 512 } else { 1 }; - let num = match num.checked_mul(multiplier) { - Some(n) => n, - None => return Some(Err(ParseError::Overflow)), + let Some(num) = num.checked_mul(multiplier) else { + return Some(Err(ParseError::Overflow)); }; Some(Ok(ObsoleteArgs { diff --git a/src/uu/tee/Cargo.toml b/src/uu/tee/Cargo.toml index bee28ffb159..282ae46731e 100644 --- a/src/uu/tee/Cargo.toml +++ b/src/uu/tee/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tee" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "tee ~ (uutils) display input and copy to FILE" diff --git a/src/uu/test/Cargo.toml b/src/uu/test/Cargo.toml index 6994efeaa5a..16e6376ed69 100644 --- a/src/uu/test/Cargo.toml +++ b/src/uu/test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_test" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "test ~ (uutils) evaluate comparison and file type expressions" diff --git a/src/uu/test/src/test.rs b/src/uu/test/src/test.rs index ec8bc91d911..0ef70b1c3f4 100644 --- a/src/uu/test/src/test.rs +++ b/src/uu/test/src/test.rs @@ -210,13 +210,8 @@ fn integers(a: &OsStr, b: &OsStr, op: &OsStr) -> ParseResult<bool> { fn files(a: &OsStr, b: &OsStr, op: &OsStr) -> ParseResult<bool> { // Don't manage the error. GNU doesn't show error when doing // test foo -nt bar - let f_a = match fs::metadata(a) { - Ok(f) => f, - Err(_) => return Ok(false), - }; - let f_b = match fs::metadata(b) { - Ok(f) => f, - Err(_) => return Ok(false), + let (Ok(f_a), Ok(f_b)) = (fs::metadata(a), fs::metadata(b)) else { + return Ok(false); }; Ok(match op.to_str() { @@ -290,11 +285,8 @@ fn path(path: &OsStr, condition: &PathCondition) -> bool { fs::metadata(path) }; - let metadata = match metadata { - Ok(metadata) => metadata, - Err(_) => { - return false; - } + let Ok(metadata) = metadata else { + return false; }; let file_type = metadata.file_type(); diff --git a/src/uu/timeout/Cargo.toml b/src/uu/timeout/Cargo.toml index 8c5f5dcbe8e..eddcf8222f6 100644 --- a/src/uu/timeout/Cargo.toml +++ b/src/uu/timeout/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_timeout" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "timeout ~ (uutils) run COMMAND with a DURATION time limit" diff --git a/src/uu/timeout/src/timeout.rs b/src/uu/timeout/src/timeout.rs index 2ba93769aa1..3194d273714 100644 --- a/src/uu/timeout/src/timeout.rs +++ b/src/uu/timeout/src/timeout.rs @@ -129,6 +129,7 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::FOREGROUND) .long(options::FOREGROUND) + .short('f') .help( "when not running timeout directly from a shell prompt, allow \ COMMAND to read from the TTY and get TTY signals; in this mode, \ @@ -148,6 +149,7 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::PRESERVE_STATUS) .long(options::PRESERVE_STATUS) + .short('p') .help("exit with the same status as COMMAND, even when the command times out") .action(ArgAction::SetTrue), ) diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index 1aa788c8d9f..e1e9ecb9556 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore datetime [package] name = "uu_touch" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "touch ~ (uutils) change FILE timestamps" @@ -22,6 +22,7 @@ filetime = { workspace = true } clap = { workspace = true } chrono = { workspace = true } parse_datetime = { workspace = true } +thiserror = { workspace = true } uucore = { workspace = true, features = ["libc"] } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/src/uu/touch/src/error.rs b/src/uu/touch/src/error.rs index b39f3faf8d1..78cc8f33050 100644 --- a/src/uu/touch/src/error.rs +++ b/src/uu/touch/src/error.rs @@ -4,29 +4,31 @@ // file that was distributed with this source code. // spell-checker:ignore (misc) uioerror - -use std::error::Error; -use std::fmt::{Display, Formatter, Result}; -use std::path::PathBuf; - use filetime::FileTime; +use std::path::PathBuf; +use thiserror::Error; use uucore::display::Quotable; use uucore::error::{UError, UIoError}; -#[derive(Debug)] +#[derive(Debug, Error)] pub enum TouchError { + #[error("Unable to parse date: {0}")] InvalidDateFormat(String), /// The source time couldn't be converted to a [chrono::DateTime] + #[error("Source has invalid access or modification time: {0}")] InvalidFiletime(FileTime), /// The reference file's attributes could not be found or read + #[error("failed to get attributes of {}: {}", .0.quote(), to_uioerror(.1))] ReferenceFileInaccessible(PathBuf, std::io::Error), /// An error getting a path to stdout on Windows + #[error("GetFinalPathNameByHandleW failed with code {0}")] WindowsStdoutPathError(String), /// An error encountered on a specific file + #[error("{error}")] TouchFileError { path: PathBuf, index: usize, @@ -34,31 +36,6 @@ pub enum TouchError { }, } -impl Error for TouchError {} -impl UError for TouchError {} -impl Display for TouchError { - fn fmt(&self, f: &mut Formatter) -> Result { - match self { - Self::InvalidDateFormat(s) => write!(f, "Unable to parse date: {s}"), - Self::InvalidFiletime(time) => { - write!(f, "Source has invalid access or modification time: {time}",) - } - Self::ReferenceFileInaccessible(path, err) => { - write!( - f, - "failed to get attributes of {}: {}", - path.quote(), - to_uioerror(err) - ) - } - Self::WindowsStdoutPathError(code) => { - write!(f, "GetFinalPathNameByHandleW failed with code {code}") - } - Self::TouchFileError { error, .. } => write!(f, "{error}"), - } - } -} - fn to_uioerror(err: &std::io::Error) -> UIoError { let copy = if let Some(code) = err.raw_os_error() { std::io::Error::from_raw_os_error(code) @@ -67,3 +44,5 @@ fn to_uioerror(err: &std::io::Error) -> UIoError { }; UIoError::from(copy) } + +impl UError for TouchError {} diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index f2c78ea61c3..de66e52ee28 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -599,14 +599,11 @@ fn parse_timestamp(s: &str) -> UResult<FileTime> { let local = NaiveDateTime::parse_from_str(&ts, format) .map_err(|_| USimpleError::new(1, format!("invalid date ts format {}", ts.quote())))?; - let mut local = match chrono::Local.from_local_datetime(&local) { - LocalResult::Single(dt) => dt, - _ => { - return Err(USimpleError::new( - 1, - format!("invalid date ts format {}", ts.quote()), - )) - } + let LocalResult::Single(mut local) = chrono::Local.from_local_datetime(&local) else { + return Err(USimpleError::new( + 1, + format!("invalid date ts format {}", ts.quote()), + )); }; // Chrono caps seconds at 59, but 60 is valid. It might be a leap second diff --git a/src/uu/tr/Cargo.toml b/src/uu/tr/Cargo.toml index 6378b7766ea..a9a0e2089b6 100644 --- a/src/uu/tr/Cargo.toml +++ b/src/uu/tr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tr" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "tr ~ (uutils) translate characters within input and display" diff --git a/src/uu/true/Cargo.toml b/src/uu/true/Cargo.toml index 6b1657276d7..e9d85a6c941 100644 --- a/src/uu/true/Cargo.toml +++ b/src/uu/true/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_true" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "true ~ (uutils) do nothing and succeed" diff --git a/src/uu/truncate/Cargo.toml b/src/uu/truncate/Cargo.toml index 9c19a8d8ad4..6845ce27d32 100644 --- a/src/uu/truncate/Cargo.toml +++ b/src/uu/truncate/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_truncate" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "truncate ~ (uutils) truncate (or extend) FILE to SIZE" diff --git a/src/uu/tsort/Cargo.toml b/src/uu/tsort/Cargo.toml index 4e179c47f4a..ba53e0d7be8 100644 --- a/src/uu/tsort/Cargo.toml +++ b/src/uu/tsort/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tsort" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "tsort ~ (uutils) topologically sort input (partially ordered) pairs" diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index ea5084a34d2..aac0a055fea 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.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 TAOCP +//spell-checker:ignore TAOCP indegree use clap::{crate_version, Arg, Command}; use std::collections::{HashMap, HashSet, VecDeque}; use std::fmt::Display; @@ -75,7 +75,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; // Create the directed graph from pairs of tokens in the input data. - let mut g = Graph::default(); + let mut g = Graph::new(input.clone()); for ab in data.split_whitespace().collect::<Vec<&str>>().chunks(2) { match ab { [a, b] => g.add_edge(a, b), @@ -83,20 +83,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } - match g.run_tsort() { - Err(cycle) => { - show!(TsortError::Loop(input.to_string())); - for node in &cycle { - show!(TsortError::LoopNode(node.to_string())); - } - println!("{}", cycle.join("\n")); - Ok(()) - } - Ok(ordering) => { - println!("{}", ordering.join("\n")); - Ok(()) - } - } + g.run_tsort(); + Ok(()) } pub fn uu_app() -> Command { Command::new(uucore::util_name()) @@ -112,34 +100,45 @@ pub fn uu_app() -> Command { ) } +/// Find the element `x` in `vec` and remove it, returning its index. +fn remove<T>(vec: &mut Vec<T>, x: T) -> Option<usize> +where + T: PartialEq, +{ + vec.iter().position(|item| *item == x).inspect(|i| { + vec.remove(*i); + }) +} + // We use String as a representation of node here // but using integer may improve performance. - +#[derive(Default)] struct Node<'input> { successor_names: Vec<&'input str>, predecessor_count: usize, } impl<'input> Node<'input> { - fn new() -> Self { - Node { - successor_names: Vec::new(), - predecessor_count: 0, - } - } - fn add_successor(&mut self, successor_name: &'input str) { self.successor_names.push(successor_name); } } -#[derive(Default)] + struct Graph<'input> { + name: String, nodes: HashMap<&'input str, Node<'input>>, } impl<'input> Graph<'input> { + fn new(name: String) -> Graph<'input> { + Self { + name, + nodes: HashMap::default(), + } + } + fn add_node(&mut self, name: &'input str) { - self.nodes.entry(name).or_insert_with(Node::new); + self.nodes.entry(name).or_default(); } fn add_edge(&mut self, from: &'input str, to: &'input str) { @@ -154,9 +153,14 @@ impl<'input> Graph<'input> { to_node.predecessor_count += 1; } } + + fn remove_edge(&mut self, u: &'input str, v: &'input str) { + remove(&mut self.nodes.get_mut(u).unwrap().successor_names, v); + self.nodes.get_mut(v).unwrap().predecessor_count -= 1; + } + /// Implementation of algorithm T from TAOCP (Don. Knuth), vol. 1. - fn run_tsort(&mut self) -> Result<Vec<&'input str>, Vec<&'input str>> { - let mut result = Vec::with_capacity(self.nodes.len()); + fn run_tsort(&mut self) { // First, we find a node that have no prerequisites (independent nodes) // If no such node exists, then there is a cycle. let mut independent_nodes_queue: VecDeque<&'input str> = self @@ -173,10 +177,18 @@ impl<'input> Graph<'input> { independent_nodes_queue.make_contiguous().sort_unstable(); // to make sure the resulting ordering is deterministic we need to order independent nodes // FIXME: this doesn't comply entirely with the GNU coreutils implementation. - // we remove each independent node, from the graph, updating each successor predecessor_count variable as we do. - while let Some(name_of_next_node_to_process) = independent_nodes_queue.pop_front() { - result.push(name_of_next_node_to_process); - if let Some(node_to_process) = self.nodes.remove(name_of_next_node_to_process) { + // To make sure the resulting ordering is deterministic we + // need to order independent nodes. + // + // FIXME: this doesn't comply entirely with the GNU coreutils + // implementation. + independent_nodes_queue.make_contiguous().sort_unstable(); + + while !self.nodes.is_empty() { + // Get the next node (breaking any cycles necessary to do so). + let v = self.find_next_node(&mut independent_nodes_queue); + println!("{v}"); + if let Some(node_to_process) = self.nodes.remove(v) { for successor_name in node_to_process.successor_names { let successor_node = self.nodes.get_mut(successor_name).unwrap(); successor_node.predecessor_count -= 1; @@ -187,20 +199,61 @@ impl<'input> Graph<'input> { } } } + } + + /// Get the in-degree of the node with the given name. + fn indegree(&self, name: &str) -> Option<usize> { + self.nodes.get(name).map(|data| data.predecessor_count) + } - // if the graph has no cycle (it's a dependency tree), the graph should be empty now, as all nodes have been deleted. - if self.nodes.is_empty() { - Ok(result) - } else { - // otherwise, we detect and show a cycle to the user (as the GNU coreutils implementation does) - Err(self.detect_cycle()) + // Pre-condition: self.nodes is non-empty. + fn find_next_node(&mut self, frontier: &mut VecDeque<&'input str>) -> &'input str { + // If there are no nodes of in-degree zero but there are still + // un-visited nodes in the graph, then there must be a cycle. + // We need to find the cycle, display it, and then break the + // cycle. + // + // A cycle is guaranteed to be of length at least two. We break + // the cycle by deleting an arbitrary edge (the first). That is + // not necessarily the optimal thing, but it should be enough to + // continue making progress in the graph traversal. + // + // It is possible that deleting the edge does not actually + // result in the target node having in-degree zero, so we repeat + // the process until such a node appears. + loop { + match frontier.pop_front() { + None => self.find_and_break_cycle(frontier), + Some(v) => return v, + } + } + } + + fn find_and_break_cycle(&mut self, frontier: &mut VecDeque<&'input str>) { + let cycle = self.detect_cycle(); + show!(TsortError::Loop(self.name.clone())); + for node in &cycle { + show!(TsortError::LoopNode(node.to_string())); + } + let u = cycle[0]; + let v = cycle[1]; + self.remove_edge(u, v); + if self.indegree(v).unwrap() == 0 { + frontier.push_back(v); } } fn detect_cycle(&self) -> Vec<&'input str> { + // Sort the nodes just to make this function deterministic. + let mut nodes = Vec::new(); + for node in self.nodes.keys() { + nodes.push(node); + } + nodes.sort_unstable(); + let mut visited = HashSet::new(); let mut stack = Vec::with_capacity(self.nodes.len()); - for &node in self.nodes.keys() { + for node in nodes { if !visited.contains(node) && self.dfs(node, &mut visited, &mut stack) { return stack; } diff --git a/src/uu/tty/Cargo.toml b/src/uu/tty/Cargo.toml index 9f81a69cc3c..dac2464d4b7 100644 --- a/src/uu/tty/Cargo.toml +++ b/src/uu/tty/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_tty" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "tty ~ (uutils) display the name of the terminal connected to standard input" diff --git a/src/uu/uname/Cargo.toml b/src/uu/uname/Cargo.toml index 87446827314..5545445a1a0 100644 --- a/src/uu/uname/Cargo.toml +++ b/src/uu/uname/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_uname" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "uname ~ (uutils) display system information" diff --git a/src/uu/unexpand/Cargo.toml b/src/uu/unexpand/Cargo.toml index f6725762d29..b0ed1fa845d 100644 --- a/src/uu/unexpand/Cargo.toml +++ b/src/uu/unexpand/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_unexpand" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "unexpand ~ (uutils) convert input spaces to tabs" diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index 7336376eb6c..1e8cede37dd 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -16,7 +16,7 @@ use std::str::from_utf8; use unicode_width::UnicodeWidthChar; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError}; -use uucore::{crash_if_err, format_usage, help_about, help_usage, show}; +use uucore::{format_usage, help_about, help_usage, show}; const USAGE: &str = help_usage!("unexpand.md"); const ABOUT: &str = help_about!("unexpand.md"); @@ -244,7 +244,7 @@ fn write_tabs( prevtab: bool, init: bool, amode: bool, -) { +) -> UResult<()> { // This conditional establishes the following: // We never turn a single space before a non-blank into // a tab, unless it's at the start of the line. @@ -255,15 +255,16 @@ fn write_tabs( break; } - crash_if_err!(1, output.write_all(b"\t")); + output.write_all(b"\t")?; scol += nts; } } while col > scol { - crash_if_err!(1, output.write_all(b" ")); + output.write_all(b" ")?; scol += 1; } + Ok(()) } #[derive(PartialEq, Eq, Debug)] @@ -325,7 +326,7 @@ fn unexpand_line( options: &Options, lastcol: usize, ts: &[usize], -) -> std::io::Result<()> { +) -> UResult<()> { let mut byte = 0; // offset into the buffer let mut col = 0; // the current column let mut scol = 0; // the start col for the current span, i.e., the already-printed width @@ -335,7 +336,7 @@ fn unexpand_line( while byte < buf.len() { // when we have a finite number of columns, never convert past the last column if lastcol > 0 && col >= lastcol { - write_tabs(output, ts, scol, col, pctype == CharType::Tab, init, true); + write_tabs(output, ts, scol, col, pctype == CharType::Tab, init, true)?; output.write_all(&buf[byte..])?; scol = col; break; @@ -370,7 +371,7 @@ fn unexpand_line( pctype == CharType::Tab, init, options.aflag, - ); + )?; init = false; // no longer at the start of a line col = if ctype == CharType::Other { // use computed width @@ -391,7 +392,7 @@ fn unexpand_line( } // write out anything remaining - write_tabs(output, ts, scol, col, pctype == CharType::Tab, init, true); + write_tabs(output, ts, scol, col, pctype == CharType::Tab, init, true)?; output.flush()?; buf.truncate(0); // clear out the buffer diff --git a/src/uu/uniq/Cargo.toml b/src/uu/uniq/Cargo.toml index 9d23e06b0bd..ace29f4701f 100644 --- a/src/uu/uniq/Cargo.toml +++ b/src/uu/uniq/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_uniq" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "uniq ~ (uutils) filter identical adjacent lines from input" diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 4995f8c198e..1f0b28253e8 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -171,12 +171,9 @@ impl Uniq { // Convert the leftover bytes to UTF-8 for character-based -w // If invalid UTF-8, just compare them as individual bytes (fallback). - let string_after_skip = match std::str::from_utf8(fields_to_check) { - Ok(s) => s, - Err(_) => { - // Fallback: if invalid UTF-8, treat them as single-byte “chars” - return closure(&mut fields_to_check.iter().map(|&b| b as char)); - } + let Ok(string_after_skip) = std::str::from_utf8(fields_to_check) else { + // Fallback: if invalid UTF-8, treat them as single-byte “chars” + return closure(&mut fields_to_check.iter().map(|&b| b as char)); }; let total_chars = string_after_skip.chars().count(); diff --git a/src/uu/unlink/Cargo.toml b/src/uu/unlink/Cargo.toml index 380bddcbe73..3152ccd4597 100644 --- a/src/uu/unlink/Cargo.toml +++ b/src/uu/unlink/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_unlink" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "unlink ~ (uutils) remove a (file system) link to FILE" diff --git a/src/uu/uptime/Cargo.toml b/src/uu/uptime/Cargo.toml index bae87dfd3c8..38126a3e956 100644 --- a/src/uu/uptime/Cargo.toml +++ b/src/uu/uptime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_uptime" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "uptime ~ (uutils) display dynamic system information" diff --git a/src/uu/users/Cargo.toml b/src/uu/users/Cargo.toml index 1f1f5773d28..fa9f4c8271b 100644 --- a/src/uu/users/Cargo.toml +++ b/src/uu/users/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_users" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "users ~ (uutils) display names of currently logged-in users" diff --git a/src/uu/vdir/Cargo.toml b/src/uu/vdir/Cargo.toml index f76f4c96b0f..09fc48c05a8 100644 --- a/src/uu/vdir/Cargo.toml +++ b/src/uu/vdir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_vdir" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "shortcut to ls -l -b" diff --git a/src/uu/wc/Cargo.toml b/src/uu/wc/Cargo.toml index 193e1667915..b3e06e29681 100644 --- a/src/uu/wc/Cargo.toml +++ b/src/uu/wc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_wc" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "wc ~ (uutils) display newline, word, and byte counts for input" diff --git a/src/uu/who/Cargo.toml b/src/uu/who/Cargo.toml index f5aab2ecd24..15bed98b70e 100644 --- a/src/uu/who/Cargo.toml +++ b/src/uu/who/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_who" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "who ~ (uutils) display information about currently logged-in users" diff --git a/src/uu/whoami/Cargo.toml b/src/uu/whoami/Cargo.toml index 4f3f7659905..7b24429b0d4 100644 --- a/src/uu/whoami/Cargo.toml +++ b/src/uu/whoami/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_whoami" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "whoami ~ (uutils) display user name of current effective user ID" diff --git a/src/uu/yes/Cargo.toml b/src/uu/yes/Cargo.toml index 21b5ff7ac33..af1b937b79f 100644 --- a/src/uu/yes/Cargo.toml +++ b/src/uu/yes/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uu_yes" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "yes ~ (uutils) repeatedly display a line with STRING (or 'y')" diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 5e1a065a610..bc10328fbb7 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "uucore" -version = "0.0.28" +version = "0.0.29" authors = ["uutils developers"] license = "MIT" description = "uutils ~ 'core' uutils code library (cross-platform)" @@ -18,6 +18,8 @@ edition = "2021" path = "src/lib/lib.rs" [dependencies] +chrono = { workspace = true } +chrono-tz = { workspace = true } clap = { workspace = true } uucore_procs = { workspace = true } number_prefix = { workspace = true } @@ -25,6 +27,7 @@ dns-lookup = { workspace = true, optional = true } dunce = { version = "1.0.4", optional = true } wild = "2.2.1" glob = { workspace = true } +iana-time-zone = { workspace = true } lazy_static = "1.4.0" # * optional itertools = { workspace = true, optional = true } @@ -52,6 +55,7 @@ sha3 = { workspace = true, optional = true } blake2b_simd = { workspace = true, optional = true } blake3 = { workspace = true, optional = true } sm3 = { workspace = true, optional = true } +crc32fast = { workspace = true, optional = true } regex = { workspace = true, optional = true } [target.'cfg(unix)'.dependencies] @@ -86,7 +90,7 @@ fsxattr = ["xattr"] lines = [] format = ["itertools", "quoting-style"] mode = ["libc"] -perms = ["libc", "walkdir"] +perms = ["entries", "libc", "walkdir"] buf-copy = [] pipes = [] process = ["libc"] @@ -106,10 +110,12 @@ sum = [ "blake2b_simd", "blake3", "sm3", + "crc32fast", ] update-control = [] utf8 = [] utmpx = ["time", "time/macros", "libc", "dns-lookup"] version-cmp = [] wide = [] +custom-tz-fmt = [] tty = [] diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index ef5be724d9f..00079eed886 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -12,6 +12,8 @@ pub mod buf_copy; pub mod checksum; #[cfg(feature = "colors")] pub mod colors; +#[cfg(feature = "custom-tz-fmt")] +pub mod custom_tz_fmt; #[cfg(feature = "encoding")] pub mod encoding; #[cfg(feature = "format")] diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum.rs index 0b3e4e24938..9912bea7454 100644 --- a/src/uucore/src/lib/features/checksum.rs +++ b/src/uucore/src/lib/features/checksum.rs @@ -19,11 +19,11 @@ use std::{ }; use crate::{ - error::{set_exit_code, FromIo, UError, UResult, USimpleError}, + error::{FromIo, UError, UResult, USimpleError}, os_str_as_bytes, os_str_from_bytes, read_os_string_lines, show, show_error, show_warning_caps, sum::{ Blake2b, Blake3, Digest, DigestWriter, Md5, Sha1, Sha224, Sha256, Sha384, Sha3_224, - Sha3_256, Sha3_384, Sha3_512, Sha512, Shake128, Shake256, Sm3, BSD, CRC, SYSV, + Sha3_256, Sha3_384, Sha3_512, Sha512, Shake128, Shake256, Sm3, BSD, CRC, CRC32B, SYSV, }, util_name, }; @@ -32,6 +32,7 @@ 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_SHA3: &str = "sha3"; @@ -46,10 +47,11 @@ 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; 15] = [ +pub const SUPPORTED_ALGORITHMS: [&str; 16] = [ ALGORITHM_OPTIONS_SYSV, ALGORITHM_OPTIONS_BSD, ALGORITHM_OPTIONS_CRC, + ALGORITHM_OPTIONS_CRC32B, ALGORITHM_OPTIONS_MD5, ALGORITHM_OPTIONS_SHA1, ALGORITHM_OPTIONS_SHA3, @@ -128,10 +130,12 @@ impl From<ChecksumError> for LineCheckError { enum FileCheckError { /// a generic UError was encountered in sub-functions UError(Box<dyn UError>), - /// the checksum file is improperly formatted. - ImproperlyFormatted, /// reading of the checksum file failed CantOpenChecksumFile, + /// processing of the file is considered as a failure regarding the + /// provided flags. This however does not stop the processing of + /// further files. + Failed, } impl From<Box<dyn UError>> for FileCheckError { @@ -146,15 +150,57 @@ impl From<ChecksumError> for FileCheckError { } } +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)] +pub enum ChecksumVerbose { + Status, + Quiet, + 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 + } +} + +impl Default for ChecksumVerbose { + fn default() -> Self { + Self::Normal + } +} + /// This struct regroups CLI flags. #[derive(Debug, Default, Clone, Copy)] pub struct ChecksumOptions { pub binary: bool, pub ignore_missing: bool, - pub quiet: bool, - pub status: bool, pub strict: bool, - pub warn: bool, + pub verbose: ChecksumVerbose, } #[derive(Debug, Error)] @@ -183,7 +229,7 @@ pub enum ChecksumError { LengthOnlyForBlake2b, #[error("the --binary and --text options are meaningless when verifying checksums")] BinaryTextConflict, - #[error("--check is not supported with --algorithm={{bsd,sysv,crc}}")] + #[error("--check is not supported with --algorithm={{bsd,sysv,crc,crc32b}}")] AlgorithmNotSupportedWithCheck, #[error("You cannot combine multiple hash algorithms!")] CombineMultipleAlgorithms, @@ -233,20 +279,19 @@ pub fn create_sha3(bits: Option<usize>) -> UResult<HashAlgorithm> { } #[allow(clippy::comparison_chain)] -fn cksum_output(res: &ChecksumResult, status: bool) { +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 !status { - 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 == 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_open_file == 1 { show_warning_caps!("{} listed file could not be read", res.failed_open_file); } else if res.failed_open_file > 1 { @@ -282,10 +327,10 @@ impl FileChecksumResult { /// The cli options might prevent to display on the outcome of the /// comparison on STDOUT. - fn can_display(&self, opts: ChecksumOptions) -> bool { + fn can_display(&self, verbose: ChecksumVerbose) -> bool { match self { - FileChecksumResult::Ok => !opts.status && !opts.quiet, - FileChecksumResult::Failed => !opts.status, + FileChecksumResult::Ok => verbose.over_quiet(), + FileChecksumResult::Failed => verbose.over_status(), FileChecksumResult::CantOpen => true, } } @@ -308,9 +353,9 @@ fn print_file_report<W: Write>( filename: &[u8], result: FileChecksumResult, prefix: &str, - opts: ChecksumOptions, + verbose: ChecksumVerbose, ) { - if result.can_display(opts) { + if result.can_display(verbose) { let _ = write!(w, "{prefix}"); let _ = w.write_all(filename); let _ = writeln!(w, ": {result}"); @@ -334,6 +379,11 @@ pub fn detect_algo(algo: &str, length: Option<usize>) -> UResult<HashAlgorithm> 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())), @@ -582,7 +632,7 @@ fn get_file_to_check( filename_bytes, FileChecksumResult::CantOpen, "", - opts, + opts.verbose, ); }; match File::open(filename) { @@ -641,12 +691,11 @@ fn get_input_file(filename: &OsStr) -> UResult<Box<dyn Read>> { fn identify_algo_name_and_length( line_info: &LineInfo, algo_name_input: Option<&str>, + last_algo: &mut Option<String>, ) -> Option<(String, Option<usize>)> { - let algorithm = line_info - .algo_name - .clone() - .unwrap_or_default() - .to_lowercase(); + let algo_from_line = line_info.algo_name.clone().unwrap_or_default(); + let algorithm = algo_from_line.to_lowercase(); + *last_algo = Some(algo_from_line); // check if we are called with XXXsum (example: md5sum) but we detected a different algo parsing the file // (for example SHA1 (f) = d...) @@ -704,7 +753,7 @@ fn compute_and_check_digest_from_file( filename, FileChecksumResult::from_bool(checksum_correct), prefix, - opts, + opts.verbose, ); if checksum_correct { @@ -719,11 +768,13 @@ fn process_algo_based_line( line_info: &LineInfo, cli_algo_name: Option<&str>, opts: ChecksumOptions, + last_algo: &mut Option<String>, ) -> Result<(), LineCheckError> { let filename_to_check = line_info.filename.as_slice(); - let (algo_name, algo_byte_len) = identify_algo_name_and_length(line_info, cli_algo_name) - .ok_or(LineCheckError::ImproperlyFormatted)?; + let (algo_name, algo_byte_len) = + identify_algo_name_and_length(line_info, cli_algo_name, last_algo) + .ok_or(LineCheckError::ImproperlyFormatted)?; // If the digest bitlen is known, we can check the format of the expected // checksum with it. @@ -782,13 +833,13 @@ fn process_non_algo_based_line( /// matched the expected. /// If the comparison didn't happen, return a `LineChecksumError`. fn process_checksum_line( - filename_input: &OsStr, line: &OsStr, i: usize, cli_algo_name: Option<&str>, cli_algo_length: Option<usize>, opts: ChecksumOptions, cached_regex: &mut Option<LineFormat>, + last_algo: &mut Option<String>, ) -> Result<(), LineCheckError> { let line_bytes = os_str_as_bytes(line)?; @@ -799,34 +850,19 @@ fn process_checksum_line( // Use `LineInfo` to extract the data of a line. // Then, depending on its format, apply a different pre-treatment. - if let Some(line_info) = LineInfo::parse(line, cached_regex) { - if line_info.format == LineFormat::AlgoBased { - process_algo_based_line(&line_info, cli_algo_name, opts) - } else if let Some(cli_algo) = cli_algo_name { - // If we match a non-algo based regex, we expect a cli argument - // to give us the algorithm to use - process_non_algo_based_line(i, &line_info, cli_algo, cli_algo_length, opts) - } else { - // We have no clue of what algorithm to use - return Err(LineCheckError::ImproperlyFormatted); - } - } else { - if opts.warn { - let algo = if let Some(algo_name_input) = cli_algo_name { - algo_name_input.to_uppercase() - } else { - "Unknown algorithm".to_string() - }; - eprintln!( - "{}: {}: {}: improperly formatted {} checksum line", - util_name(), - &filename_input.maybe_quote(), - i + 1, - algo - ); - } + let Some(line_info) = LineInfo::parse(line, cached_regex) else { + return Err(LineCheckError::ImproperlyFormatted); + }; - Err(LineCheckError::ImproperlyFormatted) + if line_info.format == LineFormat::AlgoBased { + process_algo_based_line(&line_info, cli_algo_name, opts, last_algo) + } else if let Some(cli_algo) = cli_algo_name { + // If we match a non-algo based regex, we expect a cli argument + // to give us the algorithm to use + process_non_algo_based_line(i, &line_info, cli_algo, cli_algo_length, opts) + } else { + // We have no clue of what algorithm to use + return Err(LineCheckError::ImproperlyFormatted); } } @@ -849,7 +885,6 @@ fn process_checksum_file( Err(e) => { // Could not read the file, show the error and continue to the next file show_error!("{e}"); - set_exit_code(1); return Err(FileCheckError::CantOpenChecksumFile); } } @@ -861,16 +896,20 @@ fn process_checksum_file( // cached_regex is used to ensure that several non algo-based checksum line // will use the same regex. let mut cached_regex = None; + // last_algo caches the algorithm used in the last line to print a warning + // message for the current line if improperly formatted. + // Behavior tested in gnu_cksum_c::test_warn + let mut last_algo = None; for (i, line) in lines.iter().enumerate() { let line_result = process_checksum_line( - filename_input, line, i, cli_algo_name, cli_algo_length, opts, &mut cached_regex, + &mut last_algo, ); // Match a first time to elude critical UErrors, and increment the total @@ -886,7 +925,26 @@ fn process_checksum_file( match line_result { Ok(()) => res.correct += 1, Err(DigestMismatch) => res.failed_cksum += 1, - Err(ImproperlyFormatted) => res.bad_format += 1, + Err(ImproperlyFormatted) => { + 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()) + } else if let Some(algo) = &last_algo { + Cow::Borrowed(algo.as_str()) + } else { + Cow::Borrowed("Unknown algorithm") + }; + eprintln!( + "{}: {}: {}: improperly formatted {} checksum line", + util_name(), + &filename_input.maybe_quote(), + i + 1, + algo + ); + } + } Err(CantOpenFile | FileIsDirectory) => res.failed_open_file += 1, Err(FileNotFound) if !opts.ignore_missing => res.failed_open_file += 1, _ => continue, @@ -896,36 +954,43 @@ fn process_checksum_file( // not a single line correctly formatted found // return an error if res.total_properly_formatted() == 0 { - if !opts.status { + if opts.verbose.over_status() { log_no_properly_formatted(get_filename_for_output(filename_input, input_is_stdin)); } - set_exit_code(1); - return Err(FileCheckError::ImproperlyFormatted); + return Err(FileCheckError::Failed); } // if any incorrectly formatted line, show it - cksum_output(&res, opts.status); + if opts.verbose.over_status() { + print_cksum_report(&res); + } if opts.ignore_missing && res.correct == 0 { // we have only bad format // and we had ignore-missing - eprintln!( - "{}: {}: no file was verified", - util_name(), - filename_input.maybe_quote(), - ); - set_exit_code(1); + if opts.verbose.over_status() { + eprintln!( + "{}: {}: no file was verified", + util_name(), + filename_input.maybe_quote(), + ); + } + return Err(FileCheckError::Failed); } // strict means that we should have an exit code. if opts.strict && res.bad_format > 0 { - set_exit_code(1); + return Err(FileCheckError::Failed); + } + + // If a file was missing, return an error unless we explicitly ignore it. + if res.failed_open_file > 0 && !opts.ignore_missing { + return Err(FileCheckError::Failed); } - // if we have any failed checksum verification, we set an exit code - // except if we have ignore_missing - if (res.failed_cksum > 0 || res.failed_open_file > 0) && !opts.ignore_missing { - set_exit_code(1); + // Obviously, if a checksum failed at some point, report the error. + if res.failed_cksum > 0 { + return Err(FileCheckError::Failed); } Ok(()) @@ -943,16 +1008,23 @@ pub fn perform_checksum_validation<'a, I>( where I: Iterator<Item = &'a OsStr>, { + let mut failed = false; + // if cksum has several input files, it will print the result for each file for filename_input in files { use FileCheckError::*; match process_checksum_file(filename_input, algo_name_input, length_input, opts) { Err(UError(e)) => return Err(e), - Err(CantOpenChecksumFile | ImproperlyFormatted) | Ok(_) => continue, + Err(Failed | CantOpenChecksumFile) => failed = true, + Ok(_) => continue, } } - Ok(()) + if failed { + Err(USimpleError::new(1, "")) + } else { + Ok(()) + } } pub fn digest_reader<T: Read>( @@ -1409,7 +1481,7 @@ mod tests { for (filename, result, prefix, expected) in cases { let mut buffer: Vec<u8> = vec![]; - print_file_report(&mut buffer, filename, *result, prefix, opts); + print_file_report(&mut buffer, filename, *result, prefix, opts.verbose); assert_eq!(&buffer, expected) } } diff --git a/src/uucore/src/lib/features/custom_tz_fmt.rs b/src/uucore/src/lib/features/custom_tz_fmt.rs new file mode 100644 index 00000000000..132155f540a --- /dev/null +++ b/src/uucore/src/lib/features/custom_tz_fmt.rs @@ -0,0 +1,58 @@ +// 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 chrono::{TimeZone, Utc}; +use chrono_tz::{OffsetName, Tz}; +use iana_time_zone::get_timezone; + +/// Get the alphabetic abbreviation of the current timezone. +/// +/// For example, "UTC" or "CET" or "PDT" +fn timezone_abbreviation() -> String { + let tz = match std::env::var("TZ") { + // TODO Support other time zones... + Ok(s) if s == "UTC0" || s.is_empty() => Tz::Etc__UTC, + _ => match get_timezone() { + Ok(tz_str) => tz_str.parse().unwrap(), + Err(_) => Tz::Etc__UTC, + }, + }; + + let offset = tz.offset_from_utc_date(&Utc::now().date_naive()); + offset.abbreviation().unwrap_or("UTC").to_string() +} + +/// Adapt the given string to be accepted by the chrono library crate. +/// +/// # Arguments +/// +/// fmt: the format of the string +/// +/// # Return +/// +/// A string that can be used as parameter of the chrono functions that use formats +pub fn custom_time_format(fmt: &str) -> String { + // TODO - Revisit when chrono 0.5 is released. https://github.com/chronotope/chrono/issues/970 + // GNU `date` uses `%N` for nano seconds, however the `chrono` crate uses `%f`. + fmt.replace("%N", "%f") + .replace("%Z", timezone_abbreviation().as_ref()) +} + +#[cfg(test)] +mod tests { + use super::{custom_time_format, timezone_abbreviation}; + + #[test] + fn test_custom_time_format() { + assert_eq!(custom_time_format("%Y-%m-%d %H-%M-%S"), "%Y-%m-%d %H-%M-%S"); + assert_eq!(custom_time_format("%d-%m-%Y %H-%M-%S"), "%d-%m-%Y %H-%M-%S"); + assert_eq!(custom_time_format("%Y-%m-%d %H-%M-%S"), "%Y-%m-%d %H-%M-%S"); + assert_eq!( + custom_time_format("%Y-%m-%d %H-%M-%S.%N"), + "%Y-%m-%d %H-%M-%S.%f" + ); + assert_eq!(custom_time_format("%Z"), timezone_abbreviation()); + } +} diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index e2958232f1f..8ef645cfbf9 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -652,14 +652,10 @@ pub fn are_hardlinks_to_same_file(_source: &Path, _target: &Path) -> bool { /// * `bool` - Returns `true` if the paths are hard links to the same file, and `false` otherwise. #[cfg(unix)] pub fn are_hardlinks_to_same_file(source: &Path, target: &Path) -> bool { - let source_metadata = match fs::symlink_metadata(source) { - Ok(metadata) => metadata, - Err(_) => return false, - }; - - let target_metadata = match fs::symlink_metadata(target) { - Ok(metadata) => metadata, - Err(_) => return false, + let (Ok(source_metadata), Ok(target_metadata)) = + (fs::symlink_metadata(source), fs::symlink_metadata(target)) + else { + return false; }; source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev() @@ -682,14 +678,10 @@ pub fn are_hardlinks_or_one_way_symlink_to_same_file(_source: &Path, _target: &P /// * `bool` - Returns `true` if either of above conditions are true, and `false` otherwise. #[cfg(unix)] pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Path) -> bool { - let source_metadata = match fs::metadata(source) { - Ok(metadata) => metadata, - Err(_) => return false, - }; - - let target_metadata = match fs::symlink_metadata(target) { - Ok(metadata) => metadata, - Err(_) => return false, + let (Ok(source_metadata), Ok(target_metadata)) = + (fs::metadata(source), fs::symlink_metadata(target)) + else { + return false; }; source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev() @@ -719,7 +711,7 @@ pub fn path_ends_with_terminator(path: &Path) -> bool { path.as_os_str() .encode_wide() .last() - .map_or(false, |wide| wide == b'/'.into() || wide == b'\\'.into()) + .is_some_and(|wide| wide == b'/'.into() || wide == b'\\'.into()) } /// Checks if the standard input (stdin) is a directory. diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index c161db39fc7..fa961388b62 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -137,6 +137,19 @@ pub struct MountInfo { pub dummy: bool, } +#[cfg(any(target_os = "linux", target_os = "android"))] +fn replace_special_chars(s: String) -> String { + // Replace + // + // * ASCII space with a regular space character, + // * \011 ASCII horizontal tab with a tab character, + // * ASCII backslash with an actual backslash character. + // + s.replace(r#"\040"#, " ") + .replace(r#"\011"#, " ") + .replace(r#"\134"#, r#"\"#) +} + impl MountInfo { #[cfg(any(target_os = "linux", target_os = "android"))] fn new(file_name: &str, raw: &[&str]) -> Option<Self> { @@ -158,14 +171,14 @@ impl MountInfo { dev_name = raw[after_fields + 1].to_string(); fs_type = raw[after_fields].to_string(); mount_root = raw[3].to_string(); - mount_dir = raw[4].to_string(); + mount_dir = replace_special_chars(raw[4].to_string()); mount_option = raw[5].to_string(); } LINUX_MTAB => { dev_name = raw[0].to_string(); fs_type = raw[2].to_string(); mount_root = String::new(); - mount_dir = raw[1].to_string(); + mount_dir = replace_special_chars(raw[1].to_string()); mount_option = raw[3].to_string(); } _ => return None, @@ -587,7 +600,7 @@ impl FsUsage { let mut number_of_free_clusters = 0; let mut total_number_of_clusters = 0; - let success = unsafe { + unsafe { let path = to_nul_terminated_wide_string(path); GetDiskFreeSpaceW( path.as_ptr(), @@ -595,15 +608,7 @@ impl FsUsage { &mut bytes_per_sector, &mut number_of_free_clusters, &mut total_number_of_clusters, - ) - }; - if 0 == success { - // Fails in case of CD for example - // crash!( - // EXIT_ERR, - // "GetDiskFreeSpaceW failed: {}", - // IOError::last_os_error() - // ); + ); } let bytes_per_cluster = sectors_per_cluster as u64 * bytes_per_sector as u64; @@ -1089,4 +1094,28 @@ mod tests { assert_eq!(info.fs_type, "xfs"); assert_eq!(info.dev_name, "/dev/fs0"); } + + #[test] + #[cfg(any(target_os = "linux", target_os = "android"))] + fn test_mountinfo_dir_special_chars() { + let info = MountInfo::new( + LINUX_MOUNTINFO, + &r#"317 61 7:0 / /mnt/f\134\040\011oo rw,relatime shared:641 - ext4 /dev/loop0 rw"# + .split_ascii_whitespace() + .collect::<Vec<_>>(), + ) + .unwrap(); + + assert_eq!(info.mount_dir, r#"/mnt/f\ oo"#); + + let info = MountInfo::new( + LINUX_MTAB, + &r#"/dev/loop0 /mnt/f\134\040\011oo ext4 rw,relatime 0 0"# + .split_ascii_whitespace() + .collect::<Vec<_>>(), + ) + .unwrap(); + + assert_eq!(info.mount_dir, r#"/mnt/f\ oo"#); + } } diff --git a/src/uucore/src/lib/features/fsxattr.rs b/src/uucore/src/lib/features/fsxattr.rs index 3fb626a3039..1913b0669fc 100644 --- a/src/uucore/src/lib/features/fsxattr.rs +++ b/src/uucore/src/lib/features/fsxattr.rs @@ -79,13 +79,10 @@ pub fn apply_xattrs<P: AsRef<Path>>( /// `true` if the file has extended attributes (indicating an ACL), `false` otherwise. pub fn has_acl<P: AsRef<Path>>(file: P) -> bool { // don't use exacl here, it is doing more getxattr call then needed - match xattr::list(file) { - Ok(acl) => { - // if we have extra attributes, we have an acl - acl.count() > 0 - } - Err(_) => false, - } + xattr::list(file).is_ok_and(|acl| { + // if we have extra attributes, we have an acl + acl.count() > 0 + }) } /// Returns the permissions bits of a file or directory which has Access Control List (ACL) entries based on its @@ -132,7 +129,7 @@ pub fn get_acl_perm_bits_from_xattr<P: AsRef<Path>>(source: P) -> u32 { for entry in acl_entries.chunks_exact(4) { // Third byte and fourth byte will be the perm bits - perm = (perm << 3) | entry[2] as u32 | entry[3] as u32; + perm = (perm << 3) | u32::from(entry[2]) | u32::from(entry[3]); } return perm; } diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index 73b84be721f..3879b733710 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -250,6 +250,14 @@ fn is_root(path: &Path, would_traverse_symlink: bool) -> bool { false } +pub fn get_metadata(file: &Path, follow: bool) -> Result<Metadata, std::io::Error> { + if follow { + file.metadata() + } else { + file.symlink_metadata() + } +} + impl ChownExecutor { pub fn exec(&self) -> UResult<()> { let mut ret = 0; @@ -265,18 +273,15 @@ impl ChownExecutor { #[allow(clippy::cognitive_complexity)] fn traverse<P: AsRef<Path>>(&self, root: P) -> i32 { let path = root.as_ref(); - let meta = match self.obtain_meta(path, self.dereference) { - Some(m) => m, - _ => { - if self.verbosity.level == VerbosityLevel::Verbose { - println!( - "failed to change ownership of {} to {}", - path.quote(), - self.raw_owner - ); - } - return 1; + let Some(meta) = self.obtain_meta(path, self.dereference) else { + if self.verbosity.level == VerbosityLevel::Verbose { + println!( + "failed to change ownership of {} to {}", + path.quote(), + self.raw_owner + ); } + return 1; }; if self.recursive @@ -362,17 +367,15 @@ impl ChownExecutor { Ok(entry) => entry, }; let path = entry.path(); - let meta = match self.obtain_meta(path, self.dereference) { - Some(m) => m, - _ => { - ret = 1; - if entry.file_type().is_dir() { - // Instruct walkdir to skip this directory to avoid getting another error - // when walkdir tries to query the children of this directory. - iterator.skip_current_dir(); - } - continue; + + let Some(meta) = self.obtain_meta(path, self.dereference) else { + ret = 1; + if entry.file_type().is_dir() { + // Instruct walkdir to skip this directory to avoid getting another error + // when walkdir tries to query the children of this directory. + iterator.skip_current_dir(); } + continue; }; if self.preserve_root && is_root(path, self.traverse_symlinks == TraverseSymlinks::All) @@ -417,26 +420,18 @@ impl ChownExecutor { fn obtain_meta<P: AsRef<Path>>(&self, path: P, follow: bool) -> Option<Metadata> { let path = path.as_ref(); - let meta = if follow { - path.metadata() - } else { - path.symlink_metadata() - }; - match meta { - Err(e) => { - match self.verbosity.level { - VerbosityLevel::Silent => (), - _ => show_error!( + get_metadata(path, follow) + .inspect_err(|e| { + if self.verbosity.level != VerbosityLevel::Silent { + show_error!( "cannot {} {}: {}", if follow { "dereference" } else { "access" }, path.quote(), - strip_errno(&e) - ), + strip_errno(e) + ); } - None - } - Ok(meta) => Some(meta), - } + }) + .ok() } #[inline] @@ -451,29 +446,21 @@ impl ChownExecutor { fn print_verbose_ownership_retained_as(&self, path: &Path, uid: u32, gid: Option<u32>) { if self.verbosity.level == VerbosityLevel::Verbose { - match (self.dest_uid, self.dest_gid, gid) { - (Some(_), Some(_), Some(gid)) => { - println!( - "ownership of {} retained as {}:{}", - path.quote(), - entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()), - entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()), - ); - } + let ownership = match (self.dest_uid, self.dest_gid, gid) { + (Some(_), Some(_), Some(gid)) => format!( + "{}:{}", + entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()), + entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()) + ), (None, Some(_), Some(gid)) => { - println!( - "ownership of {} retained as {}", - path.quote(), - entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()), - ); - } - (_, _, _) => { - println!( - "ownership of {} retained as {}", - path.quote(), - entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()), - ); + entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()) } + _ => entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()), + }; + if self.verbosity.groups_only { + println!("group of {} retained as {}", path.quote(), ownership); + } else { + println!("ownership of {} retained as {}", path.quote(), ownership); } } } @@ -516,6 +503,45 @@ pub struct GidUidOwnerFilter { } type GidUidFilterOwnerParser = fn(&ArgMatches) -> UResult<GidUidOwnerFilter>; +/// Determines symbolic link traversal and recursion settings based on flags. +/// Returns the updated `dereference` and `traverse_symlinks` values. +pub fn configure_symlink_and_recursion( + matches: &ArgMatches, +) -> Result<(bool, bool, TraverseSymlinks), Box<dyn crate::error::UError>> { + let mut dereference = if matches.get_flag(options::dereference::DEREFERENCE) { + Some(true) // Follow symlinks + } else if matches.get_flag(options::dereference::NO_DEREFERENCE) { + Some(false) // Do not follow symlinks + } else { + None // Default behavior + }; + + let mut traverse_symlinks = if matches.get_flag("L") { + TraverseSymlinks::All + } else if matches.get_flag("H") { + TraverseSymlinks::First + } else { + TraverseSymlinks::None + }; + + let recursive = matches.get_flag(options::RECURSIVE); + if recursive { + if traverse_symlinks == TraverseSymlinks::None { + if dereference == Some(true) { + return Err(USimpleError::new( + 1, + "-R --dereference requires -H or -L".to_string(), + )); + } + dereference = Some(false); + } + } else { + traverse_symlinks = TraverseSymlinks::None; + } + + Ok((recursive, dereference.unwrap_or(true), traverse_symlinks)) +} + /// Base implementation for `chgrp` and `chown`. /// /// An argument called `add_arg_if_not_reference` will be added to `command` if @@ -571,34 +597,7 @@ pub fn chown_base( .unwrap_or_default(); let preserve_root = matches.get_flag(options::preserve_root::PRESERVE); - - let mut dereference = if matches.get_flag(options::dereference::DEREFERENCE) { - Some(true) - } else if matches.get_flag(options::dereference::NO_DEREFERENCE) { - Some(false) - } else { - None - }; - - let mut traverse_symlinks = if matches.get_flag(options::traverse::TRAVERSE) { - TraverseSymlinks::First - } else if matches.get_flag(options::traverse::EVERY) { - TraverseSymlinks::All - } else { - TraverseSymlinks::None - }; - - let recursive = matches.get_flag(options::RECURSIVE); - if recursive { - if traverse_symlinks == TraverseSymlinks::None { - if dereference == Some(true) { - return Err(USimpleError::new(1, "-R --dereference requires -H or -L")); - } - dereference = Some(false); - } - } else { - traverse_symlinks = TraverseSymlinks::None; - } + let (recursive, dereference, traverse_symlinks) = configure_symlink_and_recursion(&matches)?; let verbosity_level = if matches.get_flag(options::verbosity::CHANGES) { VerbosityLevel::Changes @@ -628,7 +627,7 @@ pub fn chown_base( level: verbosity_level, }, recursive, - dereference: dereference.unwrap_or(true), + dereference, preserve_root, files, filter, diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index df9e1673d9d..258cb2362a7 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -207,6 +207,37 @@ impl Digest for CRC { } } +pub struct CRC32B(crc32fast::Hasher); +impl Digest for CRC32B { + fn new() -> Self { + Self(crc32fast::Hasher::new()) + } + + fn hash_update(&mut self, input: &[u8]) { + self.0.update(input); + } + + fn hash_finalize(&mut self, out: &mut [u8]) { + let result = self.0.clone().finalize(); + let slice = result.to_be_bytes(); + out.copy_from_slice(&slice); + } + + fn reset(&mut self) { + self.0.reset(); + } + + fn output_bits(&self) -> usize { + 32 + } + + fn result_str(&mut self) -> String { + let mut out = [0; 4]; + self.hash_finalize(&mut out); + format!("{}", u32::from_be_bytes(out)) + } +} + pub struct BSD { state: u16, } diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 684de8f74e0..da29baf0c70 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -46,6 +46,8 @@ pub use crate::features::buf_copy; pub use crate::features::checksum; #[cfg(feature = "colors")] pub use crate::features::colors; +#[cfg(feature = "custom-tz-fmt")] +pub use crate::features::custom_tz_fmt; #[cfg(feature = "encoding")] pub use crate::features::encoding; #[cfg(feature = "format")] @@ -380,7 +382,10 @@ macro_rules! prompt_yes( eprint!("{}: ", uucore::util_name()); eprint!($($args)+); eprint!(" "); - uucore::crash_if_err!(1, std::io::stderr().flush()); + let res = std::io::stderr().flush().map_err(|err| { + $crate::error::USimpleError::new(1, err.to_string()) + }); + uucore::show_if_err!(res); uucore::read_yes() }) ); diff --git a/src/uucore/src/lib/macros.rs b/src/uucore/src/lib/macros.rs index 6fe60053886..3ef16ab4d5a 100644 --- a/src/uucore/src/lib/macros.rs +++ b/src/uucore/src/lib/macros.rs @@ -20,7 +20,7 @@ //! fully qualified name like this: //! //! ```no_run -//! use uucore::{show, crash}; +//! use uucore::show; //! ``` //! //! Here's an overview of the macros sorted by purpose @@ -30,8 +30,6 @@ //! [`crate::show_if_err!`] //! - From custom messages: [`crate::show_error!`] //! - Print warnings: [`crate::show_warning!`] -//! - Terminate util execution -//! - Crash program: [`crate::crash!`], [`crate::crash_if_err!`] // spell-checker:ignore sourcepath targetpath rustdoc @@ -189,57 +187,3 @@ macro_rules! show_warning_caps( eprintln!($($args)+); }) ); - -/// Display an error and [`std::process::exit`] -/// -/// Displays the provided error message using [`show_error!`], then invokes -/// [`std::process::exit`] with the provided exit code. -/// -/// # Examples -/// -/// ```should_panic -/// # #[macro_use] -/// # extern crate uucore; -/// # fn main() { -/// // outputs <name>: Couldn't apply foo to bar -/// // and terminates execution -/// crash!(1, "Couldn't apply {} to {}", "foo", "bar"); -/// # } -/// ``` -#[macro_export] -macro_rules! crash( - ($exit_code:expr, $($args:tt)+) => ({ - $crate::show_error!($($args)+); - std::process::exit($exit_code); - }) -); - -/// Unwrap a [`std::result::Result`], crashing instead of panicking. -/// -/// If the result is an `Ok`-variant, returns the value contained inside. If it -/// is an `Err`-variant, invokes [`crash!`] with the formatted error instead. -/// -/// # Examples -/// -/// ```should_panic -/// # #[macro_use] -/// # extern crate uucore; -/// # fn main() { -/// let is_ok: Result<u32, &str> = Ok(1); -/// // Does nothing -/// crash_if_err!(1, is_ok); -/// -/// let is_err: Result<u32, &str> = Err("This didn't work..."); -/// // Calls `crash!` -/// crash_if_err!(1, is_err); -/// # } -/// ``` -#[macro_export] -macro_rules! crash_if_err( - ($exit_code:expr, $exp:expr) => ( - match $exp { - Ok(m) => m, - Err(f) => $crate::crash!($exit_code, "{}", f), - } - ) -); diff --git a/src/uucore/src/lib/parser/parse_size.rs b/src/uucore/src/lib/parser/parse_size.rs index 9247ad378e5..b18e1695d70 100644 --- a/src/uucore/src/lib/parser/parse_size.rs +++ b/src/uucore/src/lib/parser/parse_size.rs @@ -8,10 +8,70 @@ use std::error::Error; use std::fmt; -use std::num::IntErrorKind; +#[cfg(target_os = "linux")] +use std::io::BufRead; +use std::num::{IntErrorKind, ParseIntError}; use crate::display::Quotable; +/// Error arising from trying to compute system memory. +enum SystemError { + IOError, + ParseError, + NotFound, +} + +impl From<std::io::Error> for SystemError { + fn from(_: std::io::Error) -> Self { + Self::IOError + } +} + +impl From<ParseIntError> for SystemError { + fn from(_: ParseIntError) -> Self { + Self::ParseError + } +} + +/// Get the total number of bytes of physical memory. +/// +/// The information is read from the `/proc/meminfo` file. +/// +/// # Errors +/// +/// If there is a problem reading the file or finding the appropriate +/// entry in the file. +#[cfg(target_os = "linux")] +fn total_physical_memory() -> Result<u128, SystemError> { + // On Linux, the `/proc/meminfo` file has a table with information + // about memory usage. For example, + // + // MemTotal: 7811500 kB + // MemFree: 1487876 kB + // MemAvailable: 3857232 kB + // ... + // + // We just need to extract the number of `MemTotal` + let table = std::fs::read("/proc/meminfo")?; + for line in table.lines() { + let line = line?; + if line.starts_with("MemTotal:") && line.ends_with("kB") { + let num_kilobytes: u128 = line[9..line.len() - 2].trim().parse()?; + let num_bytes = 1024 * num_kilobytes; + return Ok(num_bytes); + } + } + Err(SystemError::NotFound) +} + +/// Get the total number of bytes of physical memory. +/// +/// TODO Implement this for non-Linux systems. +#[cfg(not(target_os = "linux"))] +fn total_physical_memory() -> Result<u128, SystemError> { + Err(SystemError::NotFound) +} + /// Parser for sizes in SI or IEC units (multiples of 1000 or 1024 bytes). /// /// The [`Parser::parse`] function performs the parse. @@ -133,6 +193,16 @@ impl<'parser> Parser<'parser> { } } + // Special case: for percentage, just compute the given fraction + // of the total physical memory on the machine, if possible. + if unit == "%" { + let number: u128 = Self::parse_number(&numeric_string, 10, size)?; + return match total_physical_memory() { + Ok(total) => Ok((number / 100) * total), + Err(_) => Err(ParseSizeError::PhysicalMem(size.to_string())), + }; + } + // Compute the factor the unit represents. // empty string means the factor is 1. // @@ -199,16 +269,9 @@ impl<'parser> Parser<'parser> { /// Same as `parse()` but tries to return u64 pub fn parse_u64(&self, size: &str) -> Result<u64, ParseSizeError> { - match self.parse(size) { - Ok(num_u128) => { - let num_u64 = match u64::try_from(num_u128) { - Ok(n) => n, - Err(_) => return Err(ParseSizeError::size_too_big(size)), - }; - Ok(num_u64) - } - Err(e) => Err(e), - } + self.parse(size).and_then(|num_u128| { + u64::try_from(num_u128).map_err(|_| ParseSizeError::size_too_big(size)) + }) } /// Same as `parse_u64()`, except returns `u64::MAX` on overflow @@ -327,6 +390,9 @@ pub enum ParseSizeError { /// Overflow SizeTooBig(String), + + /// Could not determine total physical memory size. + PhysicalMem(String), } impl Error for ParseSizeError { @@ -335,6 +401,7 @@ impl Error for ParseSizeError { Self::InvalidSuffix(ref s) => s, Self::ParseFailure(ref s) => s, Self::SizeTooBig(ref s) => s, + Self::PhysicalMem(ref s) => s, } } } @@ -342,7 +409,10 @@ impl Error for ParseSizeError { impl fmt::Display for ParseSizeError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { let s = match self { - Self::InvalidSuffix(s) | Self::ParseFailure(s) | Self::SizeTooBig(s) => s, + Self::InvalidSuffix(s) + | Self::ParseFailure(s) + | Self::SizeTooBig(s) + | Self::PhysicalMem(s) => s, }; write!(f, "{s}") } @@ -681,4 +751,16 @@ mod tests { assert_eq!(Ok(94722), parse_size_u64("0x17202")); assert_eq!(Ok(44251 * 1024), parse_size_u128("0xACDBK")); } + + #[test] + #[cfg(target_os = "linux")] + fn parse_percent() { + assert!(parse_size_u64("0%").is_ok()); + assert!(parse_size_u64("50%").is_ok()); + assert!(parse_size_u64("100%").is_ok()); + assert!(parse_size_u64("100000%").is_ok()); + assert!(parse_size_u64("-1%").is_err()); + assert!(parse_size_u64("1.0%").is_err()); + assert!(parse_size_u64("0x1%").is_err()); + } } diff --git a/src/uucore/src/lib/parser/parse_time.rs b/src/uucore/src/lib/parser/parse_time.rs index 727ee28b1bf..d22d4d372c1 100644 --- a/src/uucore/src/lib/parser/parse_time.rs +++ b/src/uucore/src/lib/parser/parse_time.rs @@ -49,9 +49,8 @@ pub fn from_str(string: &str) -> Result<Duration, String> { if len == 0 { return Err("empty string".to_owned()); } - let slice = match string.get(..len - 1) { - Some(s) => s, - None => return Err(format!("invalid time interval {}", string.quote())), + let Some(slice) = string.get(..len - 1) else { + return Err(format!("invalid time interval {}", string.quote())); }; let (numstr, times) = match string.chars().next_back().unwrap() { 's' => (slice, 1), diff --git a/src/uucore_procs/Cargo.toml b/src/uucore_procs/Cargo.toml index 40ad6cd5b42..196544091d3 100644 --- a/src/uucore_procs/Cargo.toml +++ b/src/uucore_procs/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore uuhelp [package] name = "uucore_procs" -version = "0.0.28" +version = "0.0.29" authors = ["Roy Ivy III <rivy.dev@gmail.com>"] license = "MIT" description = "uutils ~ 'uucore' proc-macros" @@ -19,4 +19,4 @@ proc-macro = true [dependencies] proc-macro2 = "1.0.81" quote = "1.0.36" -uuhelp_parser = { path = "../uuhelp_parser", version = "0.0.28" } +uuhelp_parser = { path = "../uuhelp_parser", version = "0.0.29" } diff --git a/src/uuhelp_parser/Cargo.toml b/src/uuhelp_parser/Cargo.toml index 1df8ae889c6..af46f719595 100644 --- a/src/uuhelp_parser/Cargo.toml +++ b/src/uuhelp_parser/Cargo.toml @@ -1,7 +1,7 @@ # spell-checker:ignore uuhelp [package] name = "uuhelp_parser" -version = "0.0.28" +version = "0.0.29" edition = "2021" license = "MIT" description = "A collection of functions to parse the markdown code of help files" diff --git a/tests/by-util/test_chgrp.rs b/tests/by-util/test_chgrp.rs index eca5ba0edff..cd40d80be5b 100644 --- a/tests/by-util/test_chgrp.rs +++ b/tests/by-util/test_chgrp.rs @@ -56,7 +56,7 @@ fn test_invalid_group() { } #[test] -fn test_1() { +fn test_error_1() { if getegid() != 0 { new_ucmd!().arg("bin").arg(DIR).fails().stderr_contains( // linux fails with "Operation not permitted (os error 1)" @@ -417,3 +417,179 @@ fn test_traverse_symlinks() { ); } } + +#[test] +#[cfg(not(target_vendor = "apple"))] +fn test_from_option() { + use std::os::unix::fs::MetadataExt; + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let groups = nix::unistd::getgroups().unwrap(); + // Skip test if we don't have at least two different groups to work with + if groups.len() < 2 { + return; + } + let (first_group, second_group) = (groups[0], groups[1]); + + at.touch("test_file"); + scene + .ucmd() + .arg(first_group.to_string()) + .arg("test_file") + .succeeds(); + + // Test successful group change with --from + scene + .ucmd() + .arg("--from") + .arg(first_group.to_string()) + .arg(second_group.to_string()) + .arg("test_file") + .succeeds() + .no_stderr(); + + // Verify the group was changed + let new_gid = at.plus("test_file").metadata().unwrap().gid(); + assert_eq!(new_gid, second_group.as_raw()); + + scene + .ucmd() + .arg("--from") + .arg(first_group.to_string()) + .arg(first_group.to_string()) + .arg("test_file") + .succeeds() + .no_stderr(); + + let unchanged_gid = at.plus("test_file").metadata().unwrap().gid(); + assert_eq!(unchanged_gid, second_group.as_raw()); +} + +#[test] +#[cfg(not(any(target_os = "android", target_os = "macos")))] +fn test_from_with_invalid_group() { + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("test_file"); + #[cfg(not(target_os = "android"))] + let err_msg = "chgrp: invalid user: 'nonexistent_group'\n"; + #[cfg(target_os = "android")] + let err_msg = "chgrp: invalid user: 'staff'\n"; + + ucmd.arg("--from") + .arg("nonexistent_group") + .arg("staff") + .arg("test_file") + .fails() + .stderr_is(err_msg); +} + +#[test] +#[cfg(not(target_vendor = "apple"))] +fn test_verbosity_messages() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let groups = nix::unistd::getgroups().unwrap(); + // Skip test if we don't have at least one group to work with + if groups.is_empty() { + return; + } + + at.touch("ref_file"); + at.touch("target_file"); + + scene + .ucmd() + .arg("-v") + .arg("--reference=ref_file") + .arg("target_file") + .succeeds() + .stderr_contains("group of 'target_file' retained as "); +} + +#[test] +#[cfg(not(target_vendor = "apple"))] +fn test_from_with_reference() { + use std::os::unix::fs::MetadataExt; + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let groups = nix::unistd::getgroups().unwrap(); + if groups.len() < 2 { + return; + } + let (first_group, second_group) = (groups[0], groups[1]); + + at.touch("ref_file"); + at.touch("test_file"); + + scene + .ucmd() + .arg(first_group.to_string()) + .arg("test_file") + .succeeds(); + + scene + .ucmd() + .arg(second_group.to_string()) + .arg("ref_file") + .succeeds(); + + // Test --from with --reference + scene + .ucmd() + .arg("--from") + .arg(first_group.to_string()) + .arg("--reference=ref_file") + .arg("test_file") + .succeeds() + .no_stderr(); + + let new_gid = at.plus("test_file").metadata().unwrap().gid(); + let ref_gid = at.plus("ref_file").metadata().unwrap().gid(); + assert_eq!(new_gid, ref_gid); +} + +#[test] +#[cfg(not(target_vendor = "apple"))] +fn test_numeric_group_formats() { + use std::os::unix::fs::MetadataExt; + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let groups = nix::unistd::getgroups().unwrap(); + if groups.len() < 2 { + return; + } + let (first_group, second_group) = (groups[0], groups[1]); + + at.touch("test_file"); + + scene + .ucmd() + .arg(first_group.to_string()) + .arg("test_file") + .succeeds(); + + // Test :gid format in --from + scene + .ucmd() + .arg(format!("--from=:{}", first_group.as_raw())) + .arg(second_group.to_string()) + .arg("test_file") + .succeeds() + .no_stderr(); + + let new_gid = at.plus("test_file").metadata().unwrap().gid(); + assert_eq!(new_gid, second_group.as_raw()); + + // Test :gid format in target group + scene + .ucmd() + .arg(format!("--from={}", second_group.as_raw())) + .arg(format!(":{}", first_group.as_raw())) + .arg("test_file") + .succeeds() + .no_stderr(); + + let final_gid = at.plus("test_file").metadata().unwrap().gid(); + assert_eq!(final_gid, first_group.as_raw()); +} diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index 167e79cd070..6f508afd6ce 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -229,19 +229,52 @@ fn test_chmod_ugoa() { }, ]; run_tests(tests); +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "macos"))] +// TODO fix android, it has 0777 +// We should force for the umask on startup +fn test_chmod_umask_expected() { + let current_umask = uucore::mode::get_umask(); + assert_eq!( + current_umask, + 0o022, + "Unexpected umask value: expected 022 (octal), but got {:03o}. Please adjust the test environment.", + current_umask + ); +} +fn get_expected_symlink_permissions() -> u32 { + #[cfg(any(target_os = "linux", target_os = "android"))] + { + 0o120_777 + } + #[cfg(not(any(target_os = "linux", target_os = "android")))] + { + 0o120_755 + } +} + +#[test] +fn test_chmod_error_permissions() { // check that we print an error if umask prevents us from removing a permission let (at, mut ucmd) = at_and_ucmd!(); + at.touch("file"); set_permissions(at.plus("file"), Permissions::from_mode(0o777)).unwrap(); + ucmd.args(&["-w", "file"]) + .umask(0o022) .fails() .code_is(1) - // spell-checker:disable-next-line - .stderr_is("chmod: file: new permissions are r-xrwxrwx, not r-xr-xr-x\n"); + .stderr_is( + // spell-checker:disable-next-line + "chmod: file: new permissions are r-xrwxrwx, not r-xr-xr-x\n", + ); assert_eq!( metadata(at.plus("file")).unwrap().permissions().mode(), - 0o100577 + 0o100_577 ); } @@ -642,7 +675,10 @@ fn test_chmod_file_symlink_after_non_existing_file() { .stderr_contains(expected_stderr); assert_eq!( at.metadata(test_existing_symlink).permissions().mode(), - 0o100_764 + 0o100_764, + "Expected mode: {:o}, but got: {:o}", + 0o100_764, + at.metadata(test_existing_symlink).permissions().mode() ); } @@ -746,3 +782,192 @@ fn test_gnu_special_options() { scene.ucmd().arg("--").arg("--").arg("file").succeeds(); scene.ucmd().arg("--").arg("--").fails(); } + +#[test] +fn test_chmod_dereference_symlink() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let target = "file"; + let symlink = "symlink"; + + at.touch(target); + set_permissions(at.plus(target), Permissions::from_mode(0o664)).unwrap(); + at.symlink_file(target, symlink); + + // Use --dereference: should modify the target file's permissions + scene + .ucmd() + .arg("--dereference") + .arg("u+x") + .arg(symlink) + .succeeds() + .no_stderr(); + assert_eq!(at.metadata(target).permissions().mode(), 0o100_764); + assert_eq!( + at.symlink_metadata(symlink).permissions().mode(), + get_expected_symlink_permissions() + ); +} + +#[test] +fn test_chmod_no_dereference_symlink() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let target = "file"; + let symlink = "symlink"; + + at.touch(target); + set_permissions(at.plus(target), Permissions::from_mode(0o664)).unwrap(); + at.symlink_file(target, symlink); + + scene + .ucmd() + .arg("--no-dereference") + .arg("u+x") + .arg(symlink) + .succeeds() + .no_stderr(); + assert_eq!(at.metadata(target).permissions().mode(), 0o100_664); + assert_eq!( + at.symlink_metadata(symlink).permissions().mode(), + get_expected_symlink_permissions() + ); +} + +#[test] +fn test_chmod_symlink_to_dangling_target_dereference() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let dangling_target = "nonexistent_file"; + let symlink = "symlink"; + + at.symlink_file(dangling_target, symlink); + + // Use --dereference: should fail due to dangling symlink + scene + .ucmd() + .arg("--dereference") + .arg("u+x") + .arg(symlink) + .fails() + .stderr_contains(format!("cannot operate on dangling symlink '{}'", symlink)); +} + +#[test] +fn test_chmod_symlink_target_no_dereference() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file = "a"; + let symlink = "symlink"; + at.touch(file); + at.symlink_file(file, symlink); + set_permissions(at.plus(file), Permissions::from_mode(0o644)).unwrap(); + + scene + .ucmd() + .arg("--no-dereference") + .arg("755") + .arg(symlink) + .succeeds() + .no_stderr(); + assert_eq!( + at.symlink_metadata(file).permissions().mode(), + 0o100_644, + "Expected symlink permissions: {:o}, but got: {:o}", + 0o100_644, + at.symlink_metadata(file).permissions().mode() + ); +} + +#[test] +fn test_chmod_symlink_to_dangling_recursive() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let dangling_target = "nonexistent_file"; + let symlink = "symlink"; + + at.symlink_file(dangling_target, symlink); + + scene + .ucmd() + .arg("755") + .arg("-R") + .arg(symlink) + .fails() + .stderr_is("chmod: cannot operate on dangling symlink 'symlink'\n"); + assert_eq!( + at.symlink_metadata(symlink).permissions().mode(), + get_expected_symlink_permissions(), + "Expected symlink permissions: {:o}, but got: {:o}", + get_expected_symlink_permissions(), + at.symlink_metadata(symlink).permissions().mode() + ); +} + +#[test] +fn test_chmod_traverse_symlink_combo() { + let scenarios = [ + ( + vec!["-R", "-H"], + 0o100_664, + get_expected_symlink_permissions(), + ), + ( + vec!["-R", "-L"], + 0o100_764, + get_expected_symlink_permissions(), + ), + ( + vec!["-R", "-P"], + 0o100_664, + get_expected_symlink_permissions(), + ), + ]; + + for (flags, expected_target_perms, expected_symlink_perms) in scenarios { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let directory = "dir"; + let target = "file"; + let symlink = "symlink"; + + at.mkdir(directory); + at.touch(target); + at.symlink_file(target, &format!("{directory}/{symlink}")); + + set_permissions(at.plus(target), Permissions::from_mode(0o664)).unwrap(); + + let mut ucmd = scene.ucmd(); + for f in &flags { + ucmd.arg(f); + } + ucmd.arg("u+x") + .umask(0o022) + .arg(directory) + .succeeds() + .no_stderr(); + + let actual_target = at.metadata(target).permissions().mode(); + assert_eq!( + actual_target, expected_target_perms, + "For flags {:?}, expected target perms = {:o}, got = {:o}", + flags, expected_target_perms, actual_target + ); + + let actual_symlink = at + .symlink_metadata(&format!("{directory}/{symlink}")) + .permissions() + .mode(); + assert_eq!( + actual_symlink, expected_symlink_perms, + "For flags {:?}, expected symlink perms = {:o}, got = {:o}", + flags, expected_symlink_perms, actual_symlink + ); + } +} diff --git a/tests/by-util/test_chroot.rs b/tests/by-util/test_chroot.rs index 711dd09435d..022822c6b36 100644 --- a/tests/by-util/test_chroot.rs +++ b/tests/by-util/test_chroot.rs @@ -53,6 +53,22 @@ fn test_no_such_directory() { .code_is(125); } +#[test] +fn test_multiple_group_args() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.mkdir("id"); + + if let Ok(result) = run_ucmd_as_root( + &ts, + &["--groups='invalid ignored'", "--groups=''", "/", "id", "-G"], + ) { + result.success().stdout_is("0"); + } else { + print!("Test skipped; requires root user"); + } +} + #[test] fn test_invalid_user_spec() { let ts = TestScenario::new(util_name!()); diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index b7c11320e11..d2e8ac4c67d 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (words) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb +// spell-checker:ignore (words) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb dont use crate::common::util::TestScenario; @@ -301,7 +301,7 @@ fn test_check_algo() { .arg("lorem_ipsum.txt") .fails() .no_stdout() - .stderr_contains("cksum: --check is not supported with --algorithm={bsd,sysv,crc}") + .stderr_contains("cksum: --check is not supported with --algorithm={bsd,sysv,crc,crc32b}") .code_is(1); new_ucmd!() .arg("-a=sysv") @@ -309,7 +309,7 @@ fn test_check_algo() { .arg("lorem_ipsum.txt") .fails() .no_stdout() - .stderr_contains("cksum: --check is not supported with --algorithm={bsd,sysv,crc}") + .stderr_contains("cksum: --check is not supported with --algorithm={bsd,sysv,crc,crc32b}") .code_is(1); new_ucmd!() .arg("-a=crc") @@ -317,7 +317,15 @@ fn test_check_algo() { .arg("lorem_ipsum.txt") .fails() .no_stdout() - .stderr_contains("cksum: --check is not supported with --algorithm={bsd,sysv,crc}") + .stderr_contains("cksum: --check is not supported with --algorithm={bsd,sysv,crc,crc32b}") + .code_is(1); + new_ucmd!() + .arg("-a=crc32b") + .arg("--check") + .arg("lorem_ipsum.txt") + .fails() + .no_stdout() + .stderr_contains("cksum: --check is not supported with --algorithm={bsd,sysv,crc,crc32b}") .code_is(1); } @@ -1276,6 +1284,18 @@ fn test_several_files_error_mgmt() { .stderr_contains("incorrect: no properly "); } +#[test] +fn test_check_unknown_checksum_file() { + let scene = TestScenario::new(util_name!()); + + scene + .ucmd() + .arg("--check") + .arg("missing") + .fails() + .stderr_only("cksum: missing: No such file or directory\n"); +} + #[test] fn test_check_comment_line() { // A comment in a checksum file shall be discarded unnoticed. @@ -1661,10 +1681,11 @@ mod gnu_cksum_base64 { use super::*; use crate::common::util::log_info; - const PAIRS: [(&str, &str); 11] = [ + const PAIRS: [(&str, &str); 12] = [ ("sysv", "0 0 f"), ("bsd", "00000 0 f"), ("crc", "4294967295 0 f"), + ("crc32b", "0 0 f"), ("md5", "1B2M2Y8AsgTpgAmY7PhCfg=="), ("sha1", "2jmj7l5rSw0yVb/vlWAYkK/YBwk="), ("sha224", "0UoCjCo6K8lHYQK7KII0xBWisB+CjqYqxbPkLw=="), @@ -1693,7 +1714,7 @@ mod gnu_cksum_base64 { } fn output_format(algo: &str, digest: &str) -> String { - if ["sysv", "bsd", "crc"].contains(&algo) { + if ["sysv", "bsd", "crc", "crc32b"].contains(&algo) { digest.to_string() } else { format!("{} (f) = {}", algo.to_uppercase(), digest).replace("BLAKE2B", "BLAKE2b") @@ -1706,6 +1727,7 @@ mod gnu_cksum_base64 { let scene = make_scene(); for (algo, digest) in PAIRS { + log_info("ALGORITHM", algo); scene .ucmd() .arg("--base64") @@ -1724,8 +1746,17 @@ mod gnu_cksum_base64 { let scene = make_scene(); for (algo, digest) in PAIRS { - if ["sysv", "bsd", "crc"].contains(&algo) { + if ["sysv", "bsd", "crc", "crc32b"].contains(&algo) { // These algorithms do not accept `--check` + scene + .ucmd() + .arg("--check") + .arg("-a") + .arg(algo) + .fails() + .stderr_only( + "cksum: --check is not supported with --algorithm={bsd,sysv,crc,crc32b}\n", + ); continue; } @@ -1792,6 +1823,373 @@ mod gnu_cksum_base64 { } } +/// This module reimplements the cksum-c.sh GNU test. +mod gnu_cksum_c { + use super::*; + + const INVALID_SUM: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaafdb57c725157cb40b5aee8d937b8351477e"; + + fn make_scene() -> TestScenario { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("input", "9\n7\n1\n4\n2\n6\n3\n5\n8\n10\n"); + + let algos: &[&[&str]] = &[ + &["-a", "sha384"], + &["-a", "blake2b"], + &["-a", "blake2b", "-l", "384"], + &["-a", "sm3"], + ]; + + for args in algos { + let result = scene.ucmd().args(args).succeeds(); + let stdout = result.stdout(); + at.append_bytes("CHECKSUMS", stdout); + } + + scene + } + + #[test] + #[ignore] + fn test_signed_checksums() { + todo!() + } + + #[test] + fn test_check_individual_digests_in_mixed_file() { + let scene = make_scene(); + + scene + .ucmd() + .arg("--check") + .arg("-a") + .arg("sm3") + .arg("CHECKSUMS") + .succeeds(); + } + + #[test] + fn test_check_against_older_non_hex_formats() { + let scene = make_scene(); + + scene + .ucmd() + .arg("-c") + .arg("-a") + .arg("crc") + .arg("CHECKSUMS") + .fails(); + + let crc_cmd = scene.ucmd().arg("-a").arg("crc").arg("input").succeeds(); + let crc_cmd_out = crc_cmd.stdout(); + scene.fixtures.write_bytes("CHECKSUMS.crc", crc_cmd_out); + + scene.ucmd().arg("-c").arg("CHECKSUMS.crc").fails(); + } + + #[test] + fn test_status() { + let scene = make_scene(); + + scene + .ucmd() + .arg("--status") + .arg("--check") + .arg("CHECKSUMS") + .succeeds() + .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(); + + scene + .ucmd() + .arg("--status") + .arg("--check") + .arg("CHECKSUMS") + .succeeds() + .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(); + + // without strict, succeeds + scene + .ucmd() + .arg("--check") + .arg("CHECKSUMS") + .succeeds() + .stderr_contains("1 line is improperly formatted"); + + // with strict, fails + scene + .ucmd() + .arg("--strict") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stderr_contains("1 line is improperly formatted"); + } + + fn make_scene_with_two_invalid_lines() -> TestScenario { + let scene = make_scene_with_comment(); + + scene + .fixtures + .append("CHECKSUMS", "invalid_line\ninvalid_line\n"); + + scene + } + + #[test] + fn test_check_strict_plural_checks() { + let scene = make_scene_with_two_invalid_lines(); + + scene + .ucmd() + .arg("--strict") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stderr_contains("2 lines are improperly formatted"); + } + + fn make_scene_with_incorrect_checksum() -> TestScenario { + let scene = make_scene_with_two_invalid_lines(); + + scene + .fixtures + .append("CHECKSUMS", &format!("SM3 (input) = {INVALID_SUM}\n")); + + scene + } + + #[test] + fn test_check_with_incorrect_checksum() { + let scene = make_scene_with_incorrect_checksum(); + + scene + .ucmd() + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stdout_contains("input: FAILED") + .stderr_contains("1 computed checksum did NOT match"); + + // also fails with strict + scene + .ucmd() + .arg("--strict") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stdout_contains("input: FAILED") + .stderr_contains("1 computed checksum did NOT match"); + } + + #[test] + fn test_status_with_errors() { + let scene = make_scene_with_incorrect_checksum(); + + scene + .ucmd() + .arg("--status") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .no_output(); + } + + #[test] + fn test_check_with_non_existing_file() { + let scene = make_scene(); + scene + .fixtures + .write("CHECKSUMS2", &format!("SM3 (input2) = {INVALID_SUM}\n")); + + scene + .ucmd() + .arg("--check") + .arg("CHECKSUMS2") + .fails() + .stdout_contains("input2: FAILED open or read") + .stderr_contains("1 listed file could not be read"); + + // also fails with strict + scene + .ucmd() + .arg("--strict") + .arg("--check") + .arg("CHECKSUMS2") + .fails() + .stdout_contains("input2: FAILED open or read") + .stderr_contains("1 listed file could not be read"); + } + + fn make_scene_with_another_improperly_formatted() -> TestScenario { + let scene = make_scene_with_incorrect_checksum(); + + scene.fixtures.append( + "CHECKSUMS", + &format!("BLAKE2b (missing-file) = {INVALID_SUM}\n"), + ); + + scene + } + + #[test] + fn test_warn() { + let scene = make_scene_with_another_improperly_formatted(); + + scene + .ucmd() + .arg("--warn") + .arg("--check") + .arg("CHECKSUMS") + .run() + .stderr_contains("CHECKSUMS: 6: improperly formatted SM3 checksum line") + .stderr_contains("CHECKSUMS: 9: improperly formatted BLAKE2b checksum line"); + } + + fn make_scene_with_checksum_missing() -> TestScenario { + let scene = make_scene_with_another_improperly_formatted(); + + scene.fixtures.write( + "CHECKSUMS-missing", + &format!("SM3 (nonexistent) = {INVALID_SUM}\n"), + ); + + scene + } + + #[test] + fn test_ignore_missing() { + let scene = make_scene_with_checksum_missing(); + + scene + .ucmd() + .arg("--ignore-missing") + .arg("--check") + .arg("CHECKSUMS-missing") + .fails() + .stdout_does_not_contain("nonexistent: No such file or directory") + .stdout_does_not_contain("nonexistent: FAILED open or read") + .stderr_contains("CHECKSUMS-missing: no file was verified"); + } + + #[test] + fn test_status_and_warn() { + let scene = make_scene_with_checksum_missing(); + + // --status before --warn + scene + .ucmd() + .arg("--status") + .arg("--warn") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stderr_contains("CHECKSUMS: 9: improperly formatted BLAKE2b checksum line") + .stderr_contains("WARNING: 3 lines are improperly formatted") + .stderr_contains("WARNING: 1 computed checksum did NOT match"); + + // --warn before --status (status hides the results) + scene + .ucmd() + .arg("--warn") + .arg("--status") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .stderr_does_not_contain("CHECKSUMS: 9: improperly formatted BLAKE2b checksum line") + .stderr_does_not_contain("WARNING: 3 lines are improperly formatted") + .stderr_does_not_contain("WARNING: 1 computed checksum did NOT match"); + } + + #[test] + fn test_status_and_ignore_missing() { + let scene = make_scene_with_checksum_missing(); + + scene + .ucmd() + .arg("--status") + .arg("--ignore-missing") + .arg("--check") + .arg("CHECKSUMS") + .fails() + .no_output(); + } + + #[test] + fn test_status_warn_and_ignore_missing() { + let scene = make_scene_with_checksum_missing(); + + scene + .ucmd() + .arg("--status") + .arg("--warn") + .arg("--ignore-missing") + .arg("--check") + .arg("CHECKSUMS-missing") + .fails() + .stderr_contains("CHECKSUMS-missing: no file was verified") + .stdout_does_not_contain("nonexistent: No such file or directory"); + } + + #[test] + fn test_check_several_files_dont_exist() { + let scene = make_scene(); + + scene + .ucmd() + .arg("--check") + .arg("non-existing-1") + .arg("non-existing-2") + .fails() + .stderr_contains("non-existing-1: No such file or directory") + .stderr_contains("non-existing-2: No such file or directory"); + } + + #[test] + fn test_check_several_files_empty() { + let scene = make_scene(); + scene.fixtures.touch("empty-1"); + scene.fixtures.touch("empty-2"); + + scene + .ucmd() + .arg("--check") + .arg("empty-1") + .arg("empty-2") + .fails() + .stderr_contains("empty-1: no properly formatted checksum lines found") + .stderr_contains("empty-2: no properly formatted checksum lines found"); + } +} + /// The tests in this module check the behavior of cksum when given different /// checksum formats and algorithms in the same file, while specifying an /// algorithm on CLI or not. diff --git a/tests/by-util/test_comm.rs b/tests/by-util/test_comm.rs index b62febf5047..bad00b1290e 100644 --- a/tests/by-util/test_comm.rs +++ b/tests/by-util/test_comm.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (words) defaultcheck nocheck +// spell-checker:ignore (words) defaultcheck nocheck helpb helpz nwordb nwordwordz wordtotal use crate::common::util::TestScenario; @@ -13,111 +13,184 @@ fn test_invalid_arg() { #[test] fn ab_no_args() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + + scene + .ucmd() .args(&["a", "b"]) .succeeds() - .stdout_only_fixture("ab.expected"); + .stdout_is("a\n\tb\n\t\tz\n"); } #[test] fn ab_dash_one() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + + scene + .ucmd() .args(&["a", "b", "-1"]) .succeeds() - .stdout_only_fixture("ab1.expected"); + .stdout_is("b\n\tz\n"); } #[test] fn ab_dash_two() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + + scene + .ucmd() .args(&["a", "b", "-2"]) .succeeds() - .stdout_only_fixture("ab2.expected"); + .stdout_is("a\n\tz\n"); } #[test] fn ab_dash_three() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + + scene + .ucmd() .args(&["a", "b", "-3"]) .succeeds() - .stdout_only_fixture("ab3.expected"); + .stdout_is("a\n\tb\n"); } #[test] fn a_empty() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.touch("empty"); + scene + .ucmd() .args(&["a", "empty"]) .succeeds() - .stdout_only_fixture("aempty.expected"); // spell-checker:disable-line + .stdout_is("a\nz\n"); } #[test] fn empty_empty() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("empty"); + scene + .ucmd() .args(&["empty", "empty"]) .succeeds() - .stdout_only_fixture("emptyempty.expected"); // spell-checker:disable-line + .no_output(); } #[test] fn total() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&["--total", "a", "b"]) .succeeds() - .stdout_is_fixture("ab_total.expected"); + .stdout_is("a\n\tb\n\t\tz\n1\t1\t1\ttotal\n"); } #[test] fn total_with_suppressed_regular_output() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&["--total", "-123", "a", "b"]) .succeeds() - .stdout_is_fixture("ab_total_suppressed_regular_output.expected"); + .stdout_is("1\t1\t1\ttotal\n"); } #[test] fn repeated_flags() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&["--total", "-123123", "--total", "a", "b"]) .succeeds() - .stdout_is_fixture("ab_total_suppressed_regular_output.expected"); + .stdout_is("1\t1\t1\ttotal\n"); } #[test] fn total_with_output_delimiter() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&["--total", "--output-delimiter=word", "a", "b"]) .succeeds() - .stdout_is_fixture("ab_total_delimiter_word.expected"); + .stdout_is("a\nwordb\nwordwordz\n1word1word1wordtotal\n"); } #[test] fn output_delimiter() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&["--output-delimiter=word", "a", "b"]) .succeeds() - .stdout_only_fixture("ab_delimiter_word.expected"); + .stdout_is("a\nwordb\nwordwordz\n"); } #[test] fn output_delimiter_hyphen_one() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&["--output-delimiter", "-1", "a", "b"]) .succeeds() - .stdout_only_fixture("ab_delimiter_hyphen_one.expected"); + .stdout_is("a\n-1b\n-1-1z\n"); } #[test] fn output_delimiter_hyphen_help() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&["--output-delimiter", "--help", "a", "b"]) .succeeds() - .stdout_only_fixture("ab_delimiter_hyphen_help.expected"); + .stdout_is("a\n--helpb\n--help--helpz\n"); } #[test] fn output_delimiter_multiple_identical() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&[ "--output-delimiter=word", "--output-delimiter=word", @@ -125,12 +198,17 @@ fn output_delimiter_multiple_identical() { "b", ]) .succeeds() - .stdout_only_fixture("ab_delimiter_word.expected"); + .stdout_is("a\nwordb\nwordwordz\n"); } #[test] fn output_delimiter_multiple_different() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&[ "--output-delimiter=word", "--output-delimiter=other", @@ -147,7 +225,12 @@ fn output_delimiter_multiple_different() { #[test] #[ignore = "This is too weird; deviate intentionally."] fn output_delimiter_multiple_different_prevents_help() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&[ "--output-delimiter=word", "--output-delimiter=other", @@ -164,59 +247,92 @@ fn output_delimiter_multiple_different_prevents_help() { #[test] fn output_delimiter_nul() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "a\nz\n"); + at.write("b", "b\nz\n"); + scene + .ucmd() .args(&["--output-delimiter=", "a", "b"]) .succeeds() - .stdout_only_fixture("ab_delimiter_nul.expected"); + .stdout_is("a\n\0b\n\0\0z\n"); } #[test] fn zero_terminated() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("a_nul", "a\0z\0"); + at.write("b_nul", "b\0z\0"); for param in ["-z", "--zero-terminated"] { - new_ucmd!() + scene + .ucmd() .args(&[param, "a_nul", "b_nul"]) .succeeds() - .stdout_only_fixture("ab_nul.expected"); + .stdout_is("a\0\tb\0\t\tz\0"); } } #[test] fn zero_terminated_provided_multiple_times() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("a_nul", "a\0z\0"); + at.write("b_nul", "b\0z\0"); for param in ["-z", "--zero-terminated"] { - new_ucmd!() + scene + .ucmd() .args(&[param, param, param, "a_nul", "b_nul"]) .succeeds() - .stdout_only_fixture("ab_nul.expected"); + .stdout_is("a\0\tb\0\t\tz\0"); } } #[test] fn zero_terminated_with_total() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("a_nul", "a\0z\0"); + at.write("b_nul", "b\0z\0"); + for param in ["-z", "--zero-terminated"] { - new_ucmd!() + scene + .ucmd() .args(&[param, "--total", "a_nul", "b_nul"]) .succeeds() - .stdout_only_fixture("ab_nul_total.expected"); + .stdout_is("a\0\tb\0\t\tz\x001\t1\t1\ttotal\0"); } } #[cfg_attr(not(feature = "test_unimplemented"), ignore)] #[test] fn check_order() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("bad_order_1", "e\nd\nb\na\n"); + at.write("bad_order_2", "e\nc\nb\na\n"); + scene + .ucmd() .args(&["--check-order", "bad_order_1", "bad_order_2"]) .fails() - .stdout_is_fixture("bad_order12.check_order.expected") + .stdout_is("\t\te") .stderr_is("error to be defined"); } #[cfg_attr(not(feature = "test_unimplemented"), ignore)] #[test] fn nocheck_order() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("bad_order_1", "e\nd\nb\na\n"); + at.write("bad_order_2", "e\nc\nb\na\n"); new_ucmd!() .args(&["--nocheck-order", "bad_order_1", "bad_order_2"]) .succeeds() - .stdout_only_fixture("bad_order12.nocheck_order.expected"); + .stdout_is("\t\te\n\tc\n\tb\n\ta\nd\nb\na\n"); } // when neither --check-order nor --no-check-order is provided, @@ -225,6 +341,9 @@ fn nocheck_order() { #[cfg_attr(not(feature = "test_unimplemented"), ignore)] #[test] fn defaultcheck_order() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("bad_order_1", "e\nd\nb\na\n"); new_ucmd!() .args(&["a", "bad_order_1"]) .fails() @@ -233,27 +352,26 @@ fn defaultcheck_order() { // * the first: if both files are not in order, the default behavior is the only // behavior that will provide an error message - // * the second: if two rows are paired but are out of order, // it won't matter if all rows in the two files are exactly the same. // This is specified in the documentation - #[test] fn defaultcheck_order_identical_bad_order_files() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("bad_order_1", "e\nd\nb\na\n"); + scene + .ucmd() .args(&["bad_order_1", "bad_order_1"]) .succeeds() - .stdout_only_fixture("bad_order11.defaultcheck_order.expected"); -} - -#[cfg_attr(not(feature = "test_unimplemented"), ignore)] -#[test] -fn defaultcheck_order_two_different_bad_order_files() { - new_ucmd!() - .args(&["bad_order_1", "bad_order_2"]) + .stdout_is("\t\te\n\t\td\n\t\tb\n\t\ta\n"); + scene + .ucmd() + .arg("--check-order") + .args(&["bad_order_1", "bad_order_1"]) .fails() - .stdout_is_fixture("bad_order12.nocheck_order.expected") - .stderr_is("error to be defined"); + .stdout_is("\t\te\n") + .stderr_is("comm: file 1 is not in sorted order\n"); } // * the third: (it is not know whether this is a bug or not) @@ -261,18 +379,21 @@ fn defaultcheck_order_two_different_bad_order_files() { // where both lines are different and one or both file lines being // compared are out of order from the preceding line, // it is ignored and no errors occur. - // * the fourth: (it is not known whether this is a bug or not) // there are additional, not-yet-understood circumstances where an out-of-order // pair is ignored and is not counted against the 1 maximum out-of-order line. - -#[cfg_attr(not(feature = "test_unimplemented"), ignore)] #[test] fn unintuitive_default_behavior_1() { - new_ucmd!() + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("defaultcheck_unintuitive_1", "m\nh\nn\no\nc\np\n"); + at.write("defaultcheck_unintuitive_2", "m\nh\nn\no\np\n"); + // Here, GNU does not fail, but uutils does + scene + .ucmd() .args(&["defaultcheck_unintuitive_1", "defaultcheck_unintuitive_2"]) - .succeeds() - .stdout_only_fixture("defaultcheck_unintuitive.expected"); + .fails() + .stdout_is("\t\tm\n\t\th\n\t\tn\n\t\to\nc\n\t\tp\n"); } #[test] @@ -325,3 +446,132 @@ fn test_is_dir() { .fails() .stderr_only("comm: .: Is a directory\n"); } + +#[test] +fn test_sorted() { + let expected_stderr = + "comm: file 2 is not in sorted order\ncomm: input is not in sorted order\n"; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("comm1", "1\n3"); + at.write("comm2", "3\n2"); + scene + .ucmd() + .args(&["comm1", "comm2"]) + .fails() + .code_is(1) + .stdout_is("1\n\t\t3\n\t2\n") + .stderr_is(expected_stderr); +} + +#[test] +fn test_sorted_check_order() { + let expected_stderr = "comm: file 2 is not in sorted order\n"; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("comm1", "1\n3"); + at.write("comm2", "3\n2"); + scene + .ucmd() + .arg("--check-order") + .args(&["comm1", "comm2"]) + .fails() + .code_is(1) + .stdout_is("1\n\t\t3\n") + .stderr_is(expected_stderr); +} + +#[test] +fn test_both_inputs_out_of_order() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("file_a", "3\n1\n0\n"); + at.write("file_b", "3\n2\n0\n"); + + scene + .ucmd() + .args(&["file_a", "file_b"]) + .fails() + .code_is(1) + .stdout_is("\t\t3\n1\n0\n\t2\n\t0\n") + .stderr_is( + "comm: file 1 is not in sorted order\n\ + comm: file 2 is not in sorted order\n\ + comm: input is not in sorted order\n", + ); +} + +#[test] +fn test_both_inputs_out_of_order_last_pair() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("file_a", "3\n1\n"); + at.write("file_b", "3\n2\n"); + + scene + .ucmd() + .args(&["file_a", "file_b"]) + .fails() + .code_is(1) + .stdout_is("\t\t3\n1\n\t2\n") + .stderr_is( + "comm: file 1 is not in sorted order\n\ + comm: file 2 is not in sorted order\n\ + comm: input is not in sorted order\n", + ); +} + +#[test] +fn test_first_input_out_of_order_extended() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("file_a", "0\n3\n1\n"); + at.write("file_b", "2\n3\n"); + + scene + .ucmd() + .args(&["file_a", "file_b"]) + .fails() + .code_is(1) + .stdout_is("0\n\t2\n\t\t3\n1\n") + .stderr_is( + "comm: file 1 is not in sorted order\n\ + comm: input is not in sorted order\n", + ); +} + +#[test] +fn test_out_of_order_input_nocheck() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create input files + at.write("file_a", "1\n3\n"); + at.write("file_b", "3\n2\n"); + + scene + .ucmd() + .arg("--nocheck-order") + .args(&["file_a", "file_b"]) + .succeeds() + .stdout_is("1\n\t\t3\n\t2\n") + .no_stderr(); +} + +#[test] +fn test_both_inputs_out_of_order_but_identical() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("file_a", "2\n1\n0\n"); + at.write("file_b", "2\n1\n0\n"); + + scene + .ucmd() + .args(&["file_a", "file_b"]) + .succeeds() + .stdout_is("\t\t2\n\t\t1\n\t\t0\n") + .no_stderr(); +} diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index fc56dc3160e..e44f35b8797 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -2422,6 +2422,17 @@ fn test_cp_reflink_bad() { .stderr_contains("error: invalid value 'bad' for '--reflink[=<WHEN>]'"); } +#[test] +fn test_cp_conflicting_update() { + new_ucmd!() + .arg("-b") + .arg("--update=none") + .arg("a") + .arg("b") + .fails() + .stderr_contains("--backup is mutually exclusive with -n or --update=none-fail"); +} + #[test] #[cfg(any(target_os = "linux", target_os = "android"))] fn test_cp_reflink_insufficient_permission() { @@ -2513,7 +2524,7 @@ fn test_cp_sparse_always_non_empty() { const BUFFER_SIZE: usize = 4096 * 16 + 3; let (at, mut ucmd) = at_and_ucmd!(); - let mut buf: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; + let mut buf = vec![0; BUFFER_SIZE].into_boxed_slice(); let blocks_to_touch = [buf.len() / 3, 2 * (buf.len() / 3)]; for i in blocks_to_touch { @@ -2529,7 +2540,7 @@ fn test_cp_sparse_always_non_empty() { let touched_block_count = blocks_to_touch.len() as u64 * at.metadata("dst_file_sparse").blksize() / 512; - assert_eq!(at.read_bytes("dst_file_sparse"), buf); + assert_eq!(at.read_bytes("dst_file_sparse").into_boxed_slice(), buf); assert_eq!(at.metadata("dst_file_sparse").blocks(), touched_block_count); } @@ -3460,15 +3471,9 @@ fn test_same_file_force_backup() { } /// Test for copying the contents of a FIFO as opposed to the FIFO object itself. -#[cfg(all(unix, not(target_os = "freebsd"), not(target_os = "openbsd")))] +#[cfg(unix)] #[test] fn test_copy_contents_fifo() { - // TODO this test should work on FreeBSD, but the command was - // causing an error: - // - // cp: 'fifo' -> 'outfile': the source path is neither a regular file nor a symlink to a regular file - // - // the underlying `std::fs:copy` doesn't support copying fifo on freeBSD let scenario = TestScenario::new(util_name!()); let at = &scenario.fixtures; @@ -6026,3 +6031,19 @@ fn test_cp_preserve_xattr_readonly_source() { "Extended attributes were not preserved" ); } + +#[test] +#[cfg(unix)] +fn test_cp_from_stdin() { + let (at, mut ucmd) = at_and_ucmd!(); + let target = "target"; + let test_string = "Hello, World!\n"; + + ucmd.arg("/dev/fd/0") + .arg(target) + .pipe_in(test_string) + .succeeds(); + + assert!(at.file_exists(target)); + assert_eq!(at.read(target), test_string); +} diff --git a/tests/by-util/test_csplit.rs b/tests/by-util/test_csplit.rs index 2315715228d..e062b6d551f 100644 --- a/tests/by-util/test_csplit.rs +++ b/tests/by-util/test_csplit.rs @@ -469,14 +469,14 @@ fn test_up_to_match_offset_option_suppress_matched() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.args(&["numbers50.txt", "--suppress-matched", "/10/+4"]) .succeeds() - .stdout_only("27\n111\n"); + .stdout_only("30\n108\n"); let count = glob(&at.plus_as_string("xx*")) .expect("there should be splits created") .count(); assert_eq!(count, 2); - assert_eq!(at.read("xx00"), generate(1, 10) + &generate(11, 14)); - assert_eq!(at.read("xx01"), generate(14, 51)); + assert_eq!(at.read("xx00"), generate(1, 14)); + assert_eq!(at.read("xx01"), generate(15, 51)); } #[test] @@ -484,14 +484,14 @@ fn test_up_to_match_negative_offset_option_suppress_matched() { let (at, mut ucmd) = at_and_ucmd!(); ucmd.args(&["numbers50.txt", "--suppress-matched", "/10/-4"]) .succeeds() - .stdout_only("10\n128\n"); + .stdout_only("10\n129\n"); let count = glob(&at.plus_as_string("xx*")) .expect("there should be splits created") .count(); assert_eq!(count, 2); assert_eq!(at.read("xx00"), generate(1, 6)); - assert_eq!(at.read("xx01"), generate(6, 10) + &generate(11, 51)); + assert_eq!(at.read("xx01"), generate(7, 51)); } #[test] @@ -1376,10 +1376,10 @@ fn zero_error() { #[test] fn no_such_file() { - let (_, mut ucmd) = at_and_ucmd!(); - ucmd.args(&["in", "0"]) + new_ucmd!() + .args(&["in", "0"]) .fails() - .stderr_contains("cannot access 'in': No such file or directory"); + .stderr_contains("cannot open 'in' for reading: No such file or directory"); } #[test] @@ -1417,3 +1417,52 @@ fn repeat_everything() { assert_eq!(at.read("xxz_004"), generate(37, 44 + 1)); assert_eq!(at.read("xxz_005"), generate(46, 50 + 1)); } + +#[cfg(unix)] +#[test] +fn test_named_pipe_input_file() { + let (at, mut ucmd) = at_and_ucmd!(); + + let mut fifo_writer = + create_named_pipe_with_writer(&at.plus_as_string("fifo"), &generate(1, 51)); + + let result = ucmd.args(&["fifo", "10"]).succeeds(); + fifo_writer.kill().unwrap(); + fifo_writer.wait().unwrap(); + result.stdout_only("18\n123\n"); + + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 10)); + assert_eq!(at.read("xx01"), generate(10, 51)); +} + +#[cfg(unix)] +fn create_named_pipe_with_writer(path: &str, data: &str) -> std::process::Child { + // cSpell:ignore IRWXU + nix::unistd::mkfifo(path, nix::sys::stat::Mode::S_IRWXU).unwrap(); + std::process::Command::new("sh") + .arg("-c") + .arg(format!("printf '{}' > {path}", data)) + .spawn() + .unwrap() +} + +#[test] +fn test_directory_input_file() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("test_directory"); + + #[cfg(unix)] + ucmd.args(&["test_directory", "1"]) + .fails() + .code_is(1) + .stderr_only("csplit: read error: Is a directory\n"); + #[cfg(windows)] + ucmd.args(&["test_directory", "1"]) + .fails() + .code_is(1) + .stderr_only("csplit: cannot open 'test_directory' for reading: Permission denied\n"); +} diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 553414af853..ac16fe83145 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -144,11 +144,12 @@ fn test_date_utc() { #[test] fn test_date_utc_issue_6495() { new_ucmd!() + .env("TZ", "UTC0") .arg("-u") .arg("-d") .arg("@0") .succeeds() - .stdout_is("Thu Jan 1 00:00:00 1970\n"); + .stdout_is("Thu Jan 1 00:00:00 UTC 1970\n"); } #[test] @@ -423,16 +424,18 @@ fn test_invalid_date_string() { #[test] fn test_date_one_digit_date() { new_ucmd!() + .env("TZ", "UTC0") .arg("-d") .arg("2000-1-1") .succeeds() - .stdout_contains("Sat Jan 1 00:00:00 2000"); + .stdout_only("Sat Jan 1 00:00:00 UTC 2000\n"); new_ucmd!() + .env("TZ", "UTC0") .arg("-d") .arg("2000-1-4") .succeeds() - .stdout_contains("Tue Jan 4 00:00:00 2000"); + .stdout_only("Tue Jan 4 00:00:00 UTC 2000\n"); } #[test] @@ -464,6 +467,7 @@ fn test_date_parse_from_format() { #[test] fn test_date_from_stdin() { new_ucmd!() + .env("TZ", "UTC0") .arg("-f") .arg("-") .pipe_in( @@ -473,8 +477,17 @@ fn test_date_from_stdin() { ) .succeeds() .stdout_is( - "Mon Mar 27 08:30:00 2023\n\ - Sat Apr 1 12:00:00 2023\n\ - Sat Apr 15 18:30:00 2023\n", + "Mon Mar 27 08:30:00 UTC 2023\n\ + Sat Apr 1 12:00:00 UTC 2023\n\ + Sat Apr 15 18:30:00 UTC 2023\n", ); } + +#[test] +fn test_date_empty_tz() { + new_ucmd!() + .env("TZ", "") + .arg("+%Z") + .succeeds() + .stdout_only("UTC\n"); +} diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 64ca7603bf7..57a2933201e 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -1728,7 +1728,26 @@ fn test_iflag_directory_fails_when_file_is_passed_via_std_in() { .args(&["iflag=directory", "count=0"]) .set_stdin(std::process::Stdio::from(File::open(filename).unwrap())) .fails() - .stderr_contains("standard input: not a directory"); + .stderr_only("dd: setting flags for 'standard input': Not a directory\n"); +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_iflag_directory_passes_when_dir_is_redirected() { + new_ucmd!() + .args(&["iflag=directory", "count=0"]) + .set_stdin(std::process::Stdio::from(File::open(".").unwrap())) + .succeeds(); +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_iflag_directory_fails_when_file_is_piped_via_std_in() { + new_ucmd!() + .arg("iflag=directory") + .pipe_in("") + .fails() + .stderr_only("dd: setting flags for 'standard input': Not a directory\n"); } #[test] diff --git a/tests/by-util/test_df.rs b/tests/by-util/test_df.rs index c67af5cba1b..f629944c373 100644 --- a/tests/by-util/test_df.rs +++ b/tests/by-util/test_df.rs @@ -357,6 +357,10 @@ fn test_include_exclude_same_type() { ); } +#[cfg_attr( + all(target_arch = "aarch64", target_os = "linux"), + ignore = "Issue #7158 - Test not supported on ARM64 Linux" +)] #[test] fn test_total() { // Example output: diff --git a/tests/by-util/test_dircolors.rs b/tests/by-util/test_dircolors.rs index 06d490c4a02..ffabe2923df 100644 --- a/tests/by-util/test_dircolors.rs +++ b/tests/by-util/test_dircolors.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore overridable +// spell-checker:ignore overridable colorterm use crate::common::util::TestScenario; use dircolors::{guess_syntax, OutputFmt, StrUtils}; @@ -253,3 +253,14 @@ fn test_repeated() { new_ucmd!().arg(arg).arg(arg).succeeds().no_stderr(); } } + +#[test] +fn test_colorterm_empty_with_wildcard() { + new_ucmd!() + .env("COLORTERM", "") + .pipe_in("COLORTERM ?*\nowt 40;33\n") + .args(&["-b", "-"]) + .succeeds() + .stdout_is("LS_COLORS='';\nexport LS_COLORS\n") + .no_stderr(); +} diff --git a/tests/by-util/test_echo.rs b/tests/by-util/test_echo.rs index dd6b412a429..d4430d05655 100644 --- a/tests/by-util/test_echo.rs +++ b/tests/by-util/test_echo.rs @@ -390,3 +390,55 @@ fn slash_eight_off_by_one() { .succeeds() .stdout_only(r"\8"); } + +mod posixly_correct { + use super::*; + + #[test] + fn ignore_options() { + for arg in ["--help", "--version", "-E -n 'foo'", "-nE 'foo'"] { + new_ucmd!() + .env("POSIXLY_CORRECT", "1") + .arg(arg) + .succeeds() + .stdout_only(format!("{arg}\n")); + } + } + + #[test] + fn process_n_option() { + new_ucmd!() + .env("POSIXLY_CORRECT", "1") + .args(&["-n", "foo"]) + .succeeds() + .stdout_only("foo"); + + // ignore -E & process escapes + new_ucmd!() + .env("POSIXLY_CORRECT", "1") + .args(&["-n", "-E", "foo\\cbar"]) + .succeeds() + .stdout_only("foo"); + } + + #[test] + fn process_escapes() { + new_ucmd!() + .env("POSIXLY_CORRECT", "1") + .arg("foo\\n") + .succeeds() + .stdout_only("foo\n\n"); + + new_ucmd!() + .env("POSIXLY_CORRECT", "1") + .arg("foo\\tbar") + .succeeds() + .stdout_only("foo\tbar\n"); + + new_ucmd!() + .env("POSIXLY_CORRECT", "1") + .arg("foo\\ctbar") + .succeeds() + .stdout_only("foo"); + } +} diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 2b33f725dbe..2a532029576 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -80,6 +80,15 @@ fn test_env_version() { .stdout_contains(util_name!()); } +#[test] +fn test_env_permissions() { + new_ucmd!() + .arg(".") + .fails() + .code_is(126) + .stderr_is("env: '.': Permission denied\n"); +} + #[test] fn test_echo() { #[cfg(target_os = "windows")] @@ -941,7 +950,7 @@ mod tests_split_iterator { | '*' | '?' | '[' | '#' | '˜' | '=' | '%' => { special = true; } - _ => continue, + _ => (), } } diff --git a/tests/by-util/test_kill.rs b/tests/by-util/test_kill.rs index ba2b963518d..b42e34828f6 100644 --- a/tests/by-util/test_kill.rs +++ b/tests/by-util/test_kill.rs @@ -63,7 +63,7 @@ fn test_kill_list_all_signals() { .stdout_contains("KILL") .stdout_contains("TERM") .stdout_contains("HUP") - .stdout_does_not_contain("EXIT"); + .stdout_contains("EXIT"); } #[test] @@ -80,15 +80,16 @@ fn test_kill_list_all_signals_as_table() { .succeeds() .stdout_contains("KILL") .stdout_contains("TERM") - .stdout_contains("HUP"); + .stdout_contains("HUP") + .stdout_contains("EXIT"); } #[test] -fn test_kill_table_starts_at_1() { +fn test_kill_table_starts_at_0() { new_ucmd!() .arg("-t") .succeeds() - .stdout_matches(&Regex::new("^\\s?1\\sHUP").unwrap()); + .stdout_matches(&Regex::new("^\\s?0\\sEXIT").unwrap()); } #[test] @@ -104,6 +105,7 @@ fn test_kill_table_lists_all_vertically() { assert!(signals.contains(&"KILL")); assert!(signals.contains(&"TERM")); assert!(signals.contains(&"HUP")); + assert!(signals.contains(&"EXIT")); } #[test] @@ -143,6 +145,7 @@ fn test_kill_list_all_vertically() { assert!(signals.contains(&"KILL")); assert!(signals.contains(&"TERM")); assert!(signals.contains(&"HUP")); + assert!(signals.contains(&"EXIT")); } #[test] @@ -195,12 +198,24 @@ fn test_kill_with_signal_number_old_form() { #[test] fn test_kill_with_signal_name_old_form() { - let mut target = Target::new(); + for arg in ["-Kill", "-KILL"] { + let mut target = Target::new(); + new_ucmd!() + .arg(arg) + .arg(format!("{}", target.pid())) + .succeeds(); + assert_eq!(target.wait_for_signal(), Some(libc::SIGKILL)); + } +} + +#[test] +fn test_kill_with_lower_case_signal_name_old_form() { + let target = Target::new(); new_ucmd!() - .arg("-KILL") + .arg("-kill") .arg(format!("{}", target.pid())) - .succeeds(); - assert_eq!(target.wait_for_signal(), Some(libc::SIGKILL)); + .fails() + .stderr_contains("unexpected argument"); } #[test] diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 88ca08320c4..715f18a1eaf 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -1096,13 +1096,16 @@ fn test_ls_long() { let at = &scene.fixtures; at.touch(at.plus_as_string("test-long")); + #[cfg(not(windows))] + let regex = r"[-bcCdDlMnpPsStTx?]([r-][w-][xt-]){3}.*"; + #[cfg(windows)] + let regex = r"[-dl](r[w-]x){3}.*"; + + let re = &Regex::new(regex).unwrap(); + for arg in LONG_ARGS { let result = scene.ucmd().arg(arg).arg("test-long").succeeds(); - #[cfg(not(windows))] - result.stdout_matches(&Regex::new(r"[-bcCdDlMnpPsStTx?]([r-][w-][xt-]){3}.*").unwrap()); - - #[cfg(windows)] - result.stdout_matches(&Regex::new(r"[-dl](r[w-]x){3}.*").unwrap()); + result.stdout_matches(re); } } @@ -1115,23 +1118,30 @@ fn test_ls_long_format() { at.touch(at.plus_as_string("test-long-dir/test-long-file")); at.mkdir(at.plus_as_string("test-long-dir/test-long-dir")); - for arg in LONG_ARGS { - // Assuming sane username do not have spaces within them. - // A line of the output should be: - // One of the characters -bcCdDlMnpPsStTx? - // rwx, with - for missing permissions, thrice. - // Zero or one "." for indicating a file with security context - // A number, preceded by column whitespace, and followed by a single space. - // A username, currently [^ ], followed by column whitespace, twice (or thrice for Hurd). - // A number, followed by a single space. - // A month, followed by a single space. - // A day, preceded by column whitespace, and followed by a single space. - // Either a year or a time, currently [0-9:]+, preceded by column whitespace, - // and followed by a single space. - // Whatever comes after is irrelevant to this specific test. - scene.ucmd().arg(arg).arg("test-long-dir").succeeds().stdout_matches(&Regex::new( + // Assuming sane username do not have spaces within them. + // A line of the output should be: + // One of the characters -bcCdDlMnpPsStTx? + // rwx, with - for missing permissions, thrice. + // Zero or one "." for indicating a file with security context + // A number, preceded by column whitespace, and followed by a single space. + // A username, currently [^ ], followed by column whitespace, twice (or thrice for Hurd). + // A number, followed by a single space. + // A month, followed by a single space. + // A day, preceded by column whitespace, and followed by a single space. + // Either a year or a time, currently [0-9:]+, preceded by column whitespace, + // and followed by a single space. + // Whatever comes after is irrelevant to this specific test. + let re = &Regex::new( r"\n[-bcCdDlMnpPsStTx?]([r-][w-][xt-]){3}\.? +\d+ [^ ]+ +[^ ]+( +[^ ]+)? +\d+ [A-Z][a-z]{2} {0,2}\d{0,2} {0,2}[0-9:]+ " - ).unwrap()); + ).unwrap(); + + for arg in LONG_ARGS { + scene + .ucmd() + .arg(arg) + .arg("test-long-dir") + .succeeds() + .stdout_matches(re); } // This checks for the line with the .. entry. The uname and group should be digits. @@ -5618,3 +5628,14 @@ fn test_non_unicode_names() { .succeeds() .stdout_is_bytes(b"\xC0.dir\n\xC0.file\n"); } + +#[test] +fn test_time_style_timezone_name() { + let re_custom_format = Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* UTC f\n").unwrap(); + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("f"); + ucmd.env("TZ", "UTC0") + .args(&["-l", "--time-style=+%Z"]) + .succeeds() + .stdout_matches(&re_custom_format); +} diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 1419be4e940..6441357f12e 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -1119,6 +1119,30 @@ fn test_mv_arg_update_older_dest_older() { assert_eq!(at.read(old), new_content); } +#[test] +fn test_mv_arg_update_older_dest_older_interactive() { + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "old"; + let new = "new"; + let old_content = "file1 content\n"; + let new_content = "file2 content\n"; + + let mut f = at.make_file(old); + f.write_all(old_content.as_bytes()).unwrap(); + f.set_modified(std::time::UNIX_EPOCH).unwrap(); + + at.write(new, new_content); + + ucmd.arg(new) + .arg(old) + .arg("--interactive") + .arg("--update=older") + .fails() + .stderr_contains("overwrite 'old'?") + .no_stdout(); +} + #[test] fn test_mv_arg_update_short_overwrite() { // same as --update=older diff --git a/tests/by-util/test_numfmt.rs b/tests/by-util/test_numfmt.rs index 1ef588f26a9..7569465ea18 100644 --- a/tests/by-util/test_numfmt.rs +++ b/tests/by-util/test_numfmt.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 (paths) gnutest +// spell-checker:ignore (paths) gnutest ronna quetta use crate::common::util::TestScenario; @@ -32,7 +32,7 @@ fn test_from_si() { new_ucmd!() .args(&["--from=si"]) .pipe_in("1000\n1.1M\n0.1G") - .run() + .succeeds() .stdout_is("1000\n1100000\n100000000\n"); } @@ -41,7 +41,7 @@ fn test_from_iec() { new_ucmd!() .args(&["--from=iec"]) .pipe_in("1024\n1.1M\n0.1G") - .run() + .succeeds() .stdout_is("1024\n1153434\n107374183\n"); } @@ -50,7 +50,7 @@ fn test_from_iec_i() { new_ucmd!() .args(&["--from=iec-i"]) .pipe_in("1.1Mi\n0.1Gi") - .run() + .succeeds() .stdout_is("1153434\n107374183\n"); } @@ -74,7 +74,7 @@ fn test_from_auto() { new_ucmd!() .args(&["--from=auto"]) .pipe_in("1K\n1Ki") - .run() + .succeeds() .stdout_is("1000\n1024\n"); } @@ -83,7 +83,7 @@ fn test_to_si() { new_ucmd!() .args(&["--to=si"]) .pipe_in("1000\n1100000\n100000000") - .run() + .succeeds() .stdout_is("1.0K\n1.1M\n100M\n"); } @@ -92,7 +92,7 @@ fn test_to_iec() { new_ucmd!() .args(&["--to=iec"]) .pipe_in("1024\n1153434\n107374182") - .run() + .succeeds() .stdout_is("1.0K\n1.2M\n103M\n"); } @@ -101,7 +101,7 @@ fn test_to_iec_i() { new_ucmd!() .args(&["--to=iec-i"]) .pipe_in("1024\n1153434\n107374182") - .run() + .succeeds() .stdout_is("1.0Ki\n1.2Mi\n103Mi\n"); } @@ -109,7 +109,7 @@ fn test_to_iec_i() { fn test_input_from_free_arguments() { new_ucmd!() .args(&["--from=si", "1K", "1.1M", "0.1G"]) - .run() + .succeeds() .stdout_is("1000\n1100000\n100000000\n"); } @@ -118,7 +118,7 @@ fn test_padding() { new_ucmd!() .args(&["--from=si", "--padding=8"]) .pipe_in("1K\n1.1M\n0.1G") - .run() + .succeeds() .stdout_is(" 1000\n 1100000\n100000000\n"); } @@ -127,7 +127,7 @@ fn test_negative_padding() { new_ucmd!() .args(&["--from=si", "--padding=-8"]) .pipe_in("1K\n1.1M\n0.1G") - .run() + .succeeds() .stdout_is("1000 \n1100000 \n100000000\n"); } @@ -136,7 +136,7 @@ fn test_header() { new_ucmd!() .args(&["--from=si", "--header=2"]) .pipe_in("header\nheader2\n1K\n1.1M\n0.1G") - .run() + .succeeds() .stdout_is("header\nheader2\n1000\n1100000\n100000000\n"); } @@ -145,7 +145,7 @@ fn test_header_default() { new_ucmd!() .args(&["--from=si", "--header"]) .pipe_in("header\n1K\n1.1M\n0.1G") - .run() + .succeeds() .stdout_is("header\n1000\n1100000\n100000000\n"); } @@ -153,7 +153,7 @@ fn test_header_default() { fn test_header_error_if_non_numeric() { new_ucmd!() .args(&["--header=two"]) - .run() + .fails() .stderr_is("numfmt: invalid header value 'two'\n"); } @@ -161,7 +161,7 @@ fn test_header_error_if_non_numeric() { fn test_header_error_if_0() { new_ucmd!() .args(&["--header=0"]) - .run() + .fails() .stderr_is("numfmt: invalid header value '0'\n"); } @@ -169,7 +169,7 @@ fn test_header_error_if_0() { fn test_header_error_if_negative() { new_ucmd!() .args(&["--header=-3"]) - .run() + .fails() .stderr_is("numfmt: invalid header value '-3'\n"); } @@ -178,25 +178,28 @@ fn test_negative() { new_ucmd!() .args(&["--from=si"]) .pipe_in("-1000\n-1.1M\n-0.1G") - .run() + .succeeds() .stdout_is("-1000\n-1100000\n-100000000\n"); new_ucmd!() .args(&["--to=iec-i"]) .pipe_in("-1024\n-1153434\n-107374182") - .run() + .succeeds() .stdout_is("-1.0Ki\n-1.2Mi\n-103Mi\n"); } #[test] fn test_negative_zero() { - new_ucmd!().pipe_in("-0\n-0.0").run().stdout_is("0\n0.0\n"); + new_ucmd!() + .pipe_in("-0\n-0.0") + .succeeds() + .stdout_is("0\n0.0\n"); } #[test] fn test_no_op() { new_ucmd!() .pipe_in("1024\n1234567") - .run() + .succeeds() .stdout_is("1024\n1234567\n"); } @@ -205,7 +208,7 @@ fn test_normalize() { new_ucmd!() .args(&["--from=si", "--to=si"]) .pipe_in("10000000K\n0.001K") - .run() + .succeeds() .stdout_is("10G\n1\n"); } @@ -213,7 +216,7 @@ fn test_normalize() { fn test_si_to_iec() { new_ucmd!() .args(&["--from=si", "--to=iec", "15334263563K"]) - .run() + .succeeds() .stdout_is("14T\n"); } @@ -222,7 +225,7 @@ fn test_should_report_invalid_empty_number_on_empty_stdin() { new_ucmd!() .args(&["--from=auto"]) .pipe_in("\n") - .run() + .fails() .stderr_is("numfmt: invalid number: ''\n"); } @@ -231,28 +234,41 @@ fn test_should_report_invalid_empty_number_on_blank_stdin() { new_ucmd!() .args(&["--from=auto"]) .pipe_in(" \t \n") - .run() + .fails() .stderr_is("numfmt: invalid number: ''\n"); } #[test] -fn test_should_report_invalid_suffix_on_stdin() { - for c in b'a'..=b'z' { - new_ucmd!() - .args(&["--from=auto"]) - .pipe_in(format!("1{}", c as char)) - .run() - .stderr_is(format!( - "numfmt: invalid suffix in input: '1{}'\n", - c as char - )); +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'*/]; + + // TODO implement special handling of 'K' + for c in ('A'..='Z').chain('a'..='z') { + let args = ["--from=si", "--to=si", &format!("1{c}")]; + + if valid_suffixes.contains(&c) { + new_ucmd!() + .args(&args) + .succeeds() + .stdout_only(format!("1.0{c}\n")); + } else { + new_ucmd!() + .args(&args) + .fails() + .code_is(2) + .stderr_only(format!("numfmt: invalid suffix in input: '1{c}'\n")); + } } +} +#[test] +fn test_should_report_invalid_suffix_on_nan() { // GNU numfmt reports this one as “invalid number” new_ucmd!() .args(&["--from=auto"]) .pipe_in("NaN") - .run() + .fails() .stderr_is("numfmt: invalid suffix in input: 'NaN'\n"); } @@ -262,7 +278,7 @@ fn test_should_report_invalid_number_with_interior_junk() { new_ucmd!() .args(&["--from=auto"]) .pipe_in("1x0K") - .run() + .fails() .stderr_is("numfmt: invalid number: '1x0K'\n"); } @@ -271,14 +287,14 @@ fn test_should_skip_leading_space_from_stdin() { new_ucmd!() .args(&["--from=auto"]) .pipe_in(" 2Ki") - .run() + .succeeds() .stdout_is("2048\n"); // multi-line new_ucmd!() .args(&["--from=auto"]) .pipe_in("\t1Ki\n 2K") - .run() + .succeeds() .stdout_is("1024\n2000\n"); } @@ -287,7 +303,7 @@ fn test_should_convert_only_first_number_in_line() { new_ucmd!() .args(&["--from=auto"]) .pipe_in("1Ki 2M 3G") - .run() + .succeeds() .stdout_is("1024 2M 3G\n"); } @@ -296,13 +312,13 @@ fn test_leading_whitespace_should_imply_padding() { new_ucmd!() .args(&["--from=auto"]) .pipe_in(" 1K") - .run() + .succeeds() .stdout_is(" 1000\n"); new_ucmd!() .args(&["--from=auto"]) .pipe_in(" 202Ki") - .run() + .succeeds() .stdout_is(" 206848\n"); } @@ -311,7 +327,7 @@ fn test_should_calculate_implicit_padding_per_line() { new_ucmd!() .args(&["--from=auto"]) .pipe_in(" 1Ki\n 2K") - .run() + .succeeds() .stdout_is(" 1024\n 2000\n"); } @@ -319,7 +335,7 @@ fn test_should_calculate_implicit_padding_per_line() { fn test_leading_whitespace_in_free_argument_should_imply_padding() { new_ucmd!() .args(&["--from=auto", " 1Ki"]) - .run() + .succeeds() .stdout_is(" 1024\n"); } @@ -327,7 +343,7 @@ fn test_leading_whitespace_in_free_argument_should_imply_padding() { fn test_should_calculate_implicit_padding_per_free_argument() { new_ucmd!() .args(&["--from=auto", " 1Ki", " 2K"]) - .run() + .succeeds() .stdout_is(" 1024\n 2000\n"); } diff --git a/tests/by-util/test_seq.rs b/tests/by-util/test_seq.rs index 3ff9227345c..d8ab71842a4 100644 --- a/tests/by-util/test_seq.rs +++ b/tests/by-util/test_seq.rs @@ -698,7 +698,7 @@ fn test_parse_error_hex() { new_ucmd!() .arg("0xlmnop") .fails() - .usage_error("invalid hexadecimal argument: '0xlmnop'"); + .usage_error("invalid floating point argument: '0xlmnop'"); } #[test] @@ -826,3 +826,92 @@ fn test_parse_scientific_zero() { .succeeds() .stdout_only("0\n1\n"); } + +#[test] +fn test_parse_valid_hexadecimal_float_two_args() { + let test_cases = [ + (["0x1p-1", "2"], "0.5\n1.5\n"), + (["0x.8p16", "32768"], "32768\n"), + (["0xffff.4p-4", "4096"], "4095.95\n"), + (["0xA.A9p-1", "6"], "5.33008\n"), + (["0xa.a9p-1", "6"], "5.33008\n"), + (["0xffffffffffp-30", "1024"], "1024\n"), // spell-checker:disable-line + ]; + + for (input_arguments, expected_output) in &test_cases { + new_ucmd!() + .args(input_arguments) + .succeeds() + .stdout_only(expected_output); + } +} + +#[test] +fn test_parse_valid_hexadecimal_float_three_args() { + let test_cases = [ + (["0x3.4p-1", "0x4p-1", "4"], "1.625\n3.625\n"), + ( + ["-0x.ep-3", "-0x.1p-3", "-0x.fp-3"], + "-0.109375\n-0.117188\n", + ), + ]; + + for (input_arguments, expected_output) in &test_cases { + new_ucmd!() + .args(input_arguments) + .succeeds() + .stdout_only(expected_output); + } +} + +#[test] +fn test_parse_float_gnu_coreutils() { + // some values from GNU coreutils tests + new_ucmd!() + .args(&[".89999", "1e-7", ".8999901"]) + .succeeds() + .stdout_only("0.8999900\n0.8999901\n"); + + new_ucmd!() + .args(&["0", "0.000001", "0.000003"]) + .succeeds() + .stdout_only("0.000000\n0.000001\n0.000002\n0.000003\n"); +} + +#[test] +fn test_parse_out_of_bounds_exponents() { + // The value 1e-9223372036854775808 is used in GNU Coreutils and BigDecimal tests to verify + // overflows and undefined behavior. Let's test the value too. + new_ucmd!() + .args(&["1e-9223372036854775808"]) + .succeeds() + .stdout_only(""); +} + +#[ignore] +#[test] +fn test_parse_valid_hexadecimal_float_format_issues() { + // These tests detect differences in the representation of floating-point values with GNU seq. + // There are two key areas to investigate: + // + // 1. GNU seq uses long double (80-bit) types for values, while the current implementation + // relies on f64 (64-bit). This can lead to differences due to varying precision. However, it's + // likely not the primary cause, as even double (64-bit) values can differ when compared to + // f64. + // + // 2. GNU seq uses the %Lg format specifier for printing (see the "get_default_format" function + // ). It appears that Rust lacks a direct equivalent for this format. Additionally, %Lg + // can use %f (floating) or %e (scientific) depending on the precision. There also seem to be + // some differences in the behavior of C and Rust when displaying floating-point or scientific + // notation, at least without additional configuration. + // + // It makes sense to begin by experimenting with formats and attempting to replicate + // the printf("%Lg",...) behavior. Another area worth investigating is glibc, as reviewing its + // code may help uncover additional corner cases or test data that could reveal more issues. + + //Test output: 0.00000000992804416455328464508056640625 + new_ucmd!() + .args(&["0xa.a9p-30", "1"]) + .succeeds() + .stdout_only("9.92804e-09\n1\n"); +} diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 62aa07dae5d..a134cbc2078 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -29,6 +29,10 @@ fn test_helper(file_name: &str, possible_args: &[&str]) { #[test] fn test_buffer_sizes() { + #[cfg(target_os = "linux")] + let buffer_sizes = ["0", "50K", "50k", "1M", "100M", "0%", "10%"]; + // TODO Percentage sizes are not yet supported beyond Linux. + #[cfg(not(target_os = "linux"))] let buffer_sizes = ["0", "50K", "50k", "1M", "100M"]; for buffer_size in &buffer_sizes { TestScenario::new(util_name!()) @@ -73,6 +77,15 @@ fn test_invalid_buffer_size() { .code_is(2) .stderr_only("sort: invalid suffix in --buffer-size argument '100f'\n"); + // TODO Percentage sizes are not yet supported beyond Linux. + #[cfg(target_os = "linux")] + new_ucmd!() + .arg("-S") + .arg("0x123%") + .fails() + .code_is(2) + .stderr_only("sort: invalid --buffer-size argument '0x123%'\n"); + new_ucmd!() .arg("-n") .arg("-S") @@ -1302,3 +1315,30 @@ fn test_same_sort_mode_twice() { fn test_args_override() { new_ucmd!().args(&["-f", "-f"]).pipe_in("foo").succeeds(); } + +#[test] +fn test_k_overflow() { + let input = "2\n1\n"; + let output = "1\n2\n"; + new_ucmd!() + .args(&["-k", "18446744073709551616"]) + .pipe_in(input) + .succeeds() + .stdout_is(output); +} + +#[test] +fn test_human_blocks_r_and_q() { + let input = "1Q\n1R\n"; + let output = "1R\n1Q\n"; + new_ucmd!() + .args(&["-h"]) + .pipe_in(input) + .succeeds() + .stdout_is(output); +} + +#[test] +fn test_args_check_conflict() { + new_ucmd!().arg("-c").arg("-C").fails(); +} diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index e2390dba0ab..e6e91ccccc1 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -1973,3 +1973,20 @@ fn test_split_separator_same_multiple() { .args(&["-t:", "-t:", "-t,", "fivelines.txt"]) .fails(); } + +#[test] +fn test_long_lines() { + let (at, mut ucmd) = at_and_ucmd!(); + let line1 = format!("{:131070}\n", ""); + let line2 = format!("{:1}\n", ""); + let line3 = format!("{:131071}\n", ""); + let infile = [line1, line2, line3].concat(); + ucmd.args(&["-C", "131072"]) + .pipe_in(infile) + .succeeds() + .no_output(); + assert_eq!(at.read("xaa").len(), 131_071); + assert_eq!(at.read("xab").len(), 2); + assert_eq!(at.read("xac").len(), 131_072); + assert!(!at.plus("xad").exists()); +} diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index cbd36832f48..cd74767283a 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -184,9 +184,74 @@ fn test_char() { ]; let ts = TestScenario::new(util_name!()); let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str(); + eprintln!("{expected_stdout}"); ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout); } +#[cfg(target_os = "linux")] +#[test] +fn test_printf_mtime_precision() { + // TODO Higher precision numbers (`%.3Y`, `%.4Y`, etc.) are + // formatted correctly, but we are not precise enough when we do + // some `mtime` computations, so we get `.7640` instead of + // `.7639`. This can be fixed by being more careful when + // transforming the number from `Metadata::mtime_nsec()` to the form + // used in rendering. + let args = ["-c", "%.0Y %.1Y %.2Y", "/dev/pts/ptmx"]; + let ts = TestScenario::new(util_name!()); + let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str(); + eprintln!("{expected_stdout}"); + ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout); +} + +#[cfg(feature = "touch")] +#[test] +fn test_timestamp_format() { + let ts = TestScenario::new(util_name!()); + + // Create a file with a specific timestamp for testing + ts.ccmd("touch") + .args(&["-d", "1970-01-01 18:43:33.023456789", "k"]) + .succeeds() + .no_stderr(); + + let test_cases = vec![ + // Basic timestamp formats + ("%Y", "67413"), + ("%.Y", "67413.023456789"), + ("%.1Y", "67413.0"), + ("%.3Y", "67413.023"), + ("%.6Y", "67413.023456"), + ("%.9Y", "67413.023456789"), + // Width and padding tests + ("%13.6Y", " 67413.023456"), + ("%013.6Y", "067413.023456"), + ("%-13.6Y", "67413.023456 "), + // Longer width/precision combinations + ("%18.10Y", " 67413.0234567890"), + ("%I18.10Y", " 67413.0234567890"), + ("%018.10Y", "0067413.0234567890"), + ("%-18.10Y", "67413.0234567890 "), + ]; + + for (format_str, expected) in test_cases { + let result = ts + .ucmd() + .args(&["-c", format_str, "k"]) + .succeeds() + .stdout_move_str(); + + assert_eq!( + result, + format!("{expected}\n"), + "Format '{}' failed.\nExpected: '{}'\nGot: '{}'", + format_str, + expected, + result, + ); + } +} + #[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))] #[test] fn test_date() { diff --git a/tests/by-util/test_stdbuf.rs b/tests/by-util/test_stdbuf.rs index a86f893e084..4bee30fab14 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -10,6 +10,26 @@ fn invalid_input() { new_ucmd!().arg("-/").fails().code_is(125); } +#[test] +fn test_permission() { + new_ucmd!() + .arg("-o1") + .arg(".") + .fails() + .code_is(126) + .stderr_contains("Permission denied"); +} + +#[test] +fn test_no_such() { + new_ucmd!() + .arg("-o1") + .arg("no_such") + .fails() + .code_is(127) + .stderr_contains("No such file or directory"); +} + #[cfg(all(not(target_os = "windows"), not(target_os = "openbsd")))] #[test] fn test_stdbuf_unbuffered_stdout() { diff --git a/tests/by-util/test_timeout.rs b/tests/by-util/test_timeout.rs index 1ba6445c81b..cc7c04565d4 100644 --- a/tests/by-util/test_timeout.rs +++ b/tests/by-util/test_timeout.rs @@ -65,13 +65,11 @@ fn test_zero_timeout() { new_ucmd!() .args(&["-v", "0", "sleep", ".1"]) .succeeds() - .no_stderr() - .no_stdout(); + .no_output(); new_ucmd!() .args(&["-v", "0", "-s0", "-k0", "sleep", ".1"]) .succeeds() - .no_stderr() - .no_stdout(); + .no_output(); } #[test] @@ -82,15 +80,27 @@ fn test_command_empty_args() { .stderr_contains("timeout: empty string"); } +#[test] +fn test_foreground() { + for arg in ["-f", "--foreground"] { + new_ucmd!() + .args(&[arg, ".1", "sleep", "10"]) + .fails() + .code_is(124) + .no_output(); + } +} + #[test] fn test_preserve_status() { - new_ucmd!() - .args(&["--preserve-status", ".1", "sleep", "10"]) - .fails() - // 128 + SIGTERM = 128 + 15 - .code_is(128 + 15) - .no_stderr() - .no_stdout(); + for arg in ["-p", "--preserve-status"] { + new_ucmd!() + .args(&[arg, ".1", "sleep", "10"]) + .fails() + // 128 + SIGTERM = 128 + 15 + .code_is(128 + 15) + .no_output(); + } } #[test] @@ -102,8 +112,7 @@ fn test_preserve_status_even_when_send_signal() { .args(&["-s", cont_spelling, "--preserve-status", ".1", "sleep", "2"]) .succeeds() .code_is(0) - .no_stderr() - .no_stdout(); + .no_output(); } } @@ -113,14 +122,12 @@ fn test_dont_overflow() { .args(&["9223372036854775808d", "sleep", "0"]) .succeeds() .code_is(0) - .no_stderr() - .no_stdout(); + .no_output(); new_ucmd!() .args(&["-k", "9223372036854775808d", "10", "sleep", "0"]) .succeeds() .code_is(0) - .no_stderr() - .no_stdout(); + .no_output(); } #[test] @@ -153,8 +160,7 @@ fn test_kill_after_long() { new_ucmd!() .args(&["--kill-after=1", "1", "sleep", "0"]) .succeeds() - .no_stdout() - .no_stderr(); + .no_output(); } #[test] diff --git a/tests/by-util/test_tsort.rs b/tests/by-util/test_tsort.rs index f86add2947f..299a8f0bb50 100644 --- a/tests/by-util/test_tsort.rs +++ b/tests/by-util/test_tsort.rs @@ -83,3 +83,31 @@ fn test_split_on_any_whitespace() { .succeeds() .stdout_only("a\nb\n"); } + +#[test] +fn test_cycle() { + // The graph looks like: a --> b <==> c --> d + new_ucmd!() + .pipe_in("a b b c c d c b") + .fails() + .code_is(1) + .stdout_is("a\nc\nd\nb\n") + .stderr_is("tsort: -: input contains a loop:\ntsort: b\ntsort: c\n"); +} + +#[test] +fn test_two_cycles() { + // The graph looks like: + // + // a + // | + // V + // c <==> b <==> d + // + new_ucmd!() + .pipe_in("a b b c c b b d d b") + .fails() + .code_is(1) + .stdout_is("a\nc\nd\nb\n") + .stderr_is("tsort: -: input contains a loop:\ntsort: b\ntsort: c\ntsort: -: input contains a loop:\ntsort: b\ntsort: d\n"); +} diff --git a/tests/by-util/test_uptime.rs b/tests/by-util/test_uptime.rs index 12c3a3d42f4..b45fcbc3eb9 100644 --- a/tests/by-util/test_uptime.rs +++ b/tests/by-util/test_uptime.rs @@ -99,6 +99,10 @@ fn test_uptime_with_non_existent_file() { // This will pass #[test] #[cfg(not(any(target_os = "openbsd", target_os = "macos")))] +#[cfg_attr( + all(target_arch = "aarch64", target_os = "linux"), + ignore = "Issue #7159 - Test not supported on ARM64 Linux" +)] #[allow(clippy::too_many_lines, clippy::items_after_statements)] fn test_uptime_with_file_containing_valid_boot_time_utmpx_record() { // This test will pass for freebsd but we currently don't support changing the utmpx file for diff --git a/tests/by-util/test_who.rs b/tests/by-util/test_who.rs index 36325fe7c57..2d438902e09 100644 --- a/tests/by-util/test_who.rs +++ b/tests/by-util/test_who.rs @@ -26,6 +26,10 @@ fn test_count() { #[cfg(unix)] #[test] #[cfg(not(target_os = "openbsd"))] +#[cfg_attr( + all(target_arch = "aarch64", target_os = "linux"), + ignore = "Issue #7174 - Test not supported on ARM64 Linux" +)] fn test_boot() { let ts = TestScenario::new(util_name!()); for opt in ["-b", "--boot", "--b"] { diff --git a/tests/fixtures/comm/a b/tests/fixtures/comm/a deleted file mode 100644 index 58f424cdb4f..00000000000 --- a/tests/fixtures/comm/a +++ /dev/null @@ -1,2 +0,0 @@ -a -z diff --git a/tests/fixtures/comm/a_nul b/tests/fixtures/comm/a_nul deleted file mode 100644 index 3142c801ccd..00000000000 Binary files a/tests/fixtures/comm/a_nul and /dev/null differ diff --git a/tests/fixtures/comm/ab.expected b/tests/fixtures/comm/ab.expected deleted file mode 100644 index 0efac0666b0..00000000000 --- a/tests/fixtures/comm/ab.expected +++ /dev/null @@ -1,3 +0,0 @@ -a - b - z diff --git a/tests/fixtures/comm/ab1.expected b/tests/fixtures/comm/ab1.expected deleted file mode 100644 index 7c81c8ba7cb..00000000000 --- a/tests/fixtures/comm/ab1.expected +++ /dev/null @@ -1,2 +0,0 @@ -b - z diff --git a/tests/fixtures/comm/ab2.expected b/tests/fixtures/comm/ab2.expected deleted file mode 100644 index c98cd0da66d..00000000000 --- a/tests/fixtures/comm/ab2.expected +++ /dev/null @@ -1,2 +0,0 @@ -a - z diff --git a/tests/fixtures/comm/ab3.expected b/tests/fixtures/comm/ab3.expected deleted file mode 100644 index 1898bc804f2..00000000000 --- a/tests/fixtures/comm/ab3.expected +++ /dev/null @@ -1,2 +0,0 @@ -a - b diff --git a/tests/fixtures/comm/ab_delimiter_hyphen_help.expected b/tests/fixtures/comm/ab_delimiter_hyphen_help.expected deleted file mode 100644 index c245aa27f13..00000000000 --- a/tests/fixtures/comm/ab_delimiter_hyphen_help.expected +++ /dev/null @@ -1,3 +0,0 @@ -a ---helpb ---help--helpz diff --git a/tests/fixtures/comm/ab_delimiter_hyphen_one.expected b/tests/fixtures/comm/ab_delimiter_hyphen_one.expected deleted file mode 100644 index 458f98dd42f..00000000000 --- a/tests/fixtures/comm/ab_delimiter_hyphen_one.expected +++ /dev/null @@ -1,3 +0,0 @@ -a --1b --1-1z diff --git a/tests/fixtures/comm/ab_delimiter_nul.expected b/tests/fixtures/comm/ab_delimiter_nul.expected deleted file mode 100644 index e56ec5cd789..00000000000 Binary files a/tests/fixtures/comm/ab_delimiter_nul.expected and /dev/null differ diff --git a/tests/fixtures/comm/ab_delimiter_word.expected b/tests/fixtures/comm/ab_delimiter_word.expected deleted file mode 100644 index a0c510e2071..00000000000 --- a/tests/fixtures/comm/ab_delimiter_word.expected +++ /dev/null @@ -1,3 +0,0 @@ -a -wordb -wordwordz diff --git a/tests/fixtures/comm/ab_nul.expected b/tests/fixtures/comm/ab_nul.expected deleted file mode 100644 index f826106bb19..00000000000 Binary files a/tests/fixtures/comm/ab_nul.expected and /dev/null differ diff --git a/tests/fixtures/comm/ab_nul_total.expected b/tests/fixtures/comm/ab_nul_total.expected deleted file mode 100644 index 9f46f9558b1..00000000000 Binary files a/tests/fixtures/comm/ab_nul_total.expected and /dev/null differ diff --git a/tests/fixtures/comm/ab_total.expected b/tests/fixtures/comm/ab_total.expected deleted file mode 100644 index 430354a6620..00000000000 --- a/tests/fixtures/comm/ab_total.expected +++ /dev/null @@ -1,4 +0,0 @@ -a - b - z -1 1 1 total diff --git a/tests/fixtures/comm/ab_total_delimiter_word.expected b/tests/fixtures/comm/ab_total_delimiter_word.expected deleted file mode 100644 index f7215ea28a3..00000000000 --- a/tests/fixtures/comm/ab_total_delimiter_word.expected +++ /dev/null @@ -1,4 +0,0 @@ -a -wordb -wordwordz -1word1word1wordtotal diff --git a/tests/fixtures/comm/ab_total_suppressed_regular_output.expected b/tests/fixtures/comm/ab_total_suppressed_regular_output.expected deleted file mode 100644 index 733c1d344e5..00000000000 --- a/tests/fixtures/comm/ab_total_suppressed_regular_output.expected +++ /dev/null @@ -1 +0,0 @@ -1 1 1 total diff --git a/tests/fixtures/comm/aempty.expected b/tests/fixtures/comm/aempty.expected deleted file mode 100644 index 58f424cdb4f..00000000000 --- a/tests/fixtures/comm/aempty.expected +++ /dev/null @@ -1,2 +0,0 @@ -a -z diff --git a/tests/fixtures/comm/b b/tests/fixtures/comm/b deleted file mode 100644 index 63d7d705891..00000000000 --- a/tests/fixtures/comm/b +++ /dev/null @@ -1,2 +0,0 @@ -b -z diff --git a/tests/fixtures/comm/b_nul b/tests/fixtures/comm/b_nul deleted file mode 100644 index ca4569669d4..00000000000 Binary files a/tests/fixtures/comm/b_nul and /dev/null differ diff --git a/tests/fixtures/comm/bad_order11.defaultcheck_order.expected b/tests/fixtures/comm/bad_order11.defaultcheck_order.expected deleted file mode 100644 index 81aa48629c9..00000000000 --- a/tests/fixtures/comm/bad_order11.defaultcheck_order.expected +++ /dev/null @@ -1,4 +0,0 @@ - e - d - b - a diff --git a/tests/fixtures/comm/bad_order12.check_order.expected b/tests/fixtures/comm/bad_order12.check_order.expected deleted file mode 100644 index 3930ff03699..00000000000 --- a/tests/fixtures/comm/bad_order12.check_order.expected +++ /dev/null @@ -1 +0,0 @@ - e diff --git a/tests/fixtures/comm/bad_order12.nocheck_order.expected b/tests/fixtures/comm/bad_order12.nocheck_order.expected deleted file mode 100644 index b5711801391..00000000000 --- a/tests/fixtures/comm/bad_order12.nocheck_order.expected +++ /dev/null @@ -1,7 +0,0 @@ - e - c - b - a -d -b -a diff --git a/tests/fixtures/comm/bad_order_1 b/tests/fixtures/comm/bad_order_1 deleted file mode 100644 index 2ebde65a6a1..00000000000 --- a/tests/fixtures/comm/bad_order_1 +++ /dev/null @@ -1,4 +0,0 @@ -e -d -b -a diff --git a/tests/fixtures/comm/bad_order_2 b/tests/fixtures/comm/bad_order_2 deleted file mode 100644 index 76603aeded1..00000000000 --- a/tests/fixtures/comm/bad_order_2 +++ /dev/null @@ -1,4 +0,0 @@ -e -c -b -a diff --git a/tests/fixtures/comm/defaultcheck_unintuitive.expected b/tests/fixtures/comm/defaultcheck_unintuitive.expected deleted file mode 100644 index 132eab5b250..00000000000 --- a/tests/fixtures/comm/defaultcheck_unintuitive.expected +++ /dev/null @@ -1,6 +0,0 @@ - m - h - n - o -c - p diff --git a/tests/fixtures/comm/defaultcheck_unintuitive_1 b/tests/fixtures/comm/defaultcheck_unintuitive_1 deleted file mode 100644 index f32fc1fd758..00000000000 --- a/tests/fixtures/comm/defaultcheck_unintuitive_1 +++ /dev/null @@ -1,6 +0,0 @@ -m -h -n -o -c -p diff --git a/tests/fixtures/comm/defaultcheck_unintuitive_2 b/tests/fixtures/comm/defaultcheck_unintuitive_2 deleted file mode 100644 index 212de88c4d0..00000000000 --- a/tests/fixtures/comm/defaultcheck_unintuitive_2 +++ /dev/null @@ -1,5 +0,0 @@ -m -h -n -o -p diff --git a/tests/fixtures/comm/empty b/tests/fixtures/comm/empty deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/fixtures/comm/emptyempty.expected b/tests/fixtures/comm/emptyempty.expected deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/fixtures/comm/lowercase_uppercase b/tests/fixtures/comm/lowercase_uppercase deleted file mode 100644 index 768ebc2b947..00000000000 --- a/tests/fixtures/comm/lowercase_uppercase +++ /dev/null @@ -1,2 +0,0 @@ -a -A \ No newline at end of file diff --git a/tests/fixtures/comm/lowercase_uppercase.c.expected b/tests/fixtures/comm/lowercase_uppercase.c.expected deleted file mode 100644 index 8458d7960bc..00000000000 --- a/tests/fixtures/comm/lowercase_uppercase.c.expected +++ /dev/null @@ -1 +0,0 @@ - a diff --git a/tests/fixtures/comm/lowercase_uppercase.en_us.expected b/tests/fixtures/comm/lowercase_uppercase.en_us.expected deleted file mode 100644 index 3992b34d039..00000000000 --- a/tests/fixtures/comm/lowercase_uppercase.en_us.expected +++ /dev/null @@ -1,2 +0,0 @@ - a - A diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 38df18daeb2..2f904427a75 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -2,7 +2,7 @@ # `build-gnu.bash` ~ builds GNU coreutils (from supplied sources) # -# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules xstrtol ; (vars/env) SRCDIR vdir rcexp xpart dired OSTYPE ; (utils) gnproc greadlink gsed multihardlink +# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules xstrtol ; (vars/env) SRCDIR vdir rcexp xpart dired OSTYPE ; (utils) gnproc greadlink gsed multihardlink texinfo set -e @@ -60,7 +60,7 @@ fi ### -release_tag_GNU="v9.5" +release_tag_GNU="v9.6" if test ! -d "${path_GNU}"; then echo "Could not find GNU coreutils (expected at '${path_GNU}')" @@ -94,6 +94,19 @@ if [ "$(uname)" == "Linux" ]; then export SELINUX_ENABLED=1 fi +# Set up quilt for patch management +export QUILT_PATCHES="${ME_dir}/gnu-patches/" +cd "$path_GNU" + +# Check if all patches are already applied +if [ "$(quilt applied | wc -l)" -eq "$(quilt series | wc -l)" ]; then + echo "All patches are already applied" +else + # Push all patches + quilt push -a || { echo "Failed to apply patches"; exit 1; } +fi +cd - + "${MAKE}" PROFILE="${UU_MAKE_PROFILE}" cp "${UU_BUILD_DIR}/install" "${UU_BUILD_DIR}/ginstall" # The GNU tests rename this script before running, to avoid confusion with the make target @@ -122,6 +135,8 @@ if test -f gnu-built; then echo "'rm -f $(pwd)/gnu-built' to force the build" echo "Note: the customization of the tests will still happen" else + # Disable useless checks + sed -i 's|check-texinfo: $(syntax_checks)|check-texinfo:|' doc/local.mk ./bootstrap --skip-po ./configure --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references #Add timeout to to protect against hangs @@ -132,70 +147,36 @@ else # Use a better diff sed -i 's|diff -c|diff -u|g' tests/Coreutils.pm "${MAKE}" -j "$("${NPROC}")" - touch gnu-built -fi -# Handle generated factor tests -t_first=00 -t_max=36 -# t_max_release=20 -# if test "${UU_MAKE_PROFILE}" != "debug"; then -# # Generate the factor tests, so they can be fixed -# # * reduced to 20 to decrease log size (down from 36 expected by GNU) -# # * only for 'release', skipped for 'debug' as redundant and too time consuming (causing timeout errors) -# seq=$( -# i=${t_first} -# while test "${i}" -le "${t_max_release}"; do -# printf '%02d ' $i -# i=$((i + 1)) -# done -# ) -# for i in ${seq}; do -# "${MAKE}" "tests/factor/t${i}.sh" -# done -# cat -# sed -i -e 's|^seq |/usr/bin/seq |' -e 's|sha1sum |/usr/bin/sha1sum |' tests/factor/t*.sh -# t_first=$((t_max_release + 1)) -# fi -# strip all (debug) or just the longer (release) factor tests from Makefile -seq=$( - i=${t_first} - while test "${i}" -le "${t_max}"; do - printf '%02d ' ${i} - i=$((i + 1)) + # Handle generated factor tests + t_first=00 + t_max=37 + seq=$( + i=${t_first} + while test "${i}" -le "${t_max}"; do + printf '%02d ' ${i} + i=$((i + 1)) + done + ) + for i in ${seq}; do + echo "strip t${i}.sh from Makefile" + sed -i -e "s/\$(tf)\/t${i}.sh//g" Makefile done -) -for i in ${seq}; do - echo "strip t${i}.sh from Makefile" - sed -i -e "s/\$(tf)\/t${i}.sh//g" Makefile -done -grep -rl 'path_prepend_' tests/* | xargs sed -i 's| path_prepend_ ./src||' - -# Remove tests checking for --version & --help -# Not really interesting for us and logs are too big -sed -i -e '/tests\/help\/help-version.sh/ D' \ - -e '/tests\/help\/help-version-getopt.sh/ D' \ - Makefile - -# logs are clotted because of this test -sed -i -e '/tests\/seq\/seq-precision.sh/ D' \ - Makefile + # Remove tests checking for --version & --help + # Not really interesting for us and logs are too big + sed -i -e '/tests\/help\/help-version.sh/ D' \ + -e '/tests\/help\/help-version-getopt.sh/ D' \ + Makefile + touch gnu-built +fi -# printf doesn't limit the values used in its arg, so this produced ~2GB of output -sed -i '/INT_OFLOW/ D' tests/printf/printf.sh +grep -rl 'path_prepend_' tests/* | xargs sed -i 's| path_prepend_ ./src||' # Use the system coreutils where the test fails due to error in a util that is not the one being tested -sed -i 's|stat|/usr/bin/stat|' tests/touch/60-seconds.sh tests/sort/sort-compress-proc.sh -sed -i 's|ls -|/usr/bin/ls -|' tests/cp/same-file.sh tests/misc/mknod.sh tests/mv/part-symlink.sh -sed -i 's|chmod |/usr/bin/chmod |' tests/du/inacc-dir.sh tests/tail/tail-n0f.sh tests/cp/fail-perm.sh tests/mv/i-2.sh tests/shuf/shuf.sh -sed -i 's|sort |/usr/bin/sort |' tests/ls/hyperlink.sh tests/test/test-N.sh -sed -i 's|split |/usr/bin/split |' tests/factor/factor-parallel.sh -sed -i 's|id -|/usr/bin/id -|' tests/runcon/runcon-no-reorder.sh 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/cp/reflink-perm.sh tests/ls/block-size.sh tests/mv/update.sh tests/ls/ls-time.sh tests/stat/stat-nanoseconds.sh tests/misc/time-style.sh tests/test/test-N.sh tests/ls/abmon-align.sh -sed -i 's|ln -|/usr/bin/ln -|' tests/cp/link-deref.sh +sed -i 's|touch |/usr/bin/touch |' tests/mv/update.sh tests/ls/ls-time.sh tests/misc/time-style.sh tests/test/test-N.sh tests/ls/abmon-align.sh # our messages are better sed -i "s|cannot stat 'symlink': Permission denied|not writing through dangling symlink 'symlink'|" tests/cp/fail-perm.sh @@ -207,13 +188,8 @@ sed -i "s|cannot create regular file 'no-such/': Not a directory|'no-such/' is n # Our message is better sed -i "s|warning: unrecognized escape|warning: incomplete hex escape|" tests/stat/stat-printf.pl -sed -i 's|cp |/usr/bin/cp |' tests/mv/hard-2.sh -sed -i 's|paste |/usr/bin/paste |' tests/od/od-endian.sh sed -i 's|timeout |'"${SYSTEM_TIMEOUT}"' |' tests/tail/follow-stdin.sh -# Add specific timeout to tests that currently hang to limit time spent waiting -sed -i 's|\(^\s*\)seq \$|\1'"${SYSTEM_TIMEOUT}"' 0.1 seq \$|' tests/seq/seq-precision.sh tests/seq/seq-long-double.sh - # 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' @@ -223,8 +199,6 @@ grep -rlE '/usr/local/bin/\s?/usr/local/bin' init.cfg tests/* | xargs -r sed -Ei # we should not regress our project just to match what GNU is going. # So, do some changes on the fly -eval cat "$path_UUTILS/util/gnu-patches/*.patch" | patch -N -r - -d "$path_GNU" -p 1 -i - || true - sed -i -e "s|rm: cannot remove 'e/slink'|rm: cannot remove 'e'|g" tests/rm/fail-eacces.sh sed -i -e "s|rm: cannot remove 'a/b/file'|rm: cannot remove 'a'|g" tests/rm/cycle.sh @@ -302,7 +276,7 @@ sed -i -e "s/ginstall: creating directory/install: creating directory/g" tests/i "${SED}" -i -Ez "s/\n([^\n#]*pad-3\.2[^\n]*)\n([^\n]*)\n([^\n]*)/\n# uutils\/numfmt supports padding = LONG_MIN\n#\1\n#\2\n#\3/" tests/misc/numfmt.pl # Update the GNU error message to match the one generated by clap -sed -i -e "s/\$prog: multiple field specifications/error: The argument '--field <FIELDS>' was provided more than once, but cannot be used multiple times\n\nUsage: numfmt [OPTION]... [NUMBER]...\n\n\nFor more information try '--help'/g" tests/misc/numfmt.pl +sed -i -e "s/\$prog: multiple field specifications/error: the argument '--field <FIELDS>' 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 <files>...\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 '<files>...' 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 diff --git a/util/gnu-patches/series b/util/gnu-patches/series new file mode 100644 index 00000000000..f303e89c42b --- /dev/null +++ b/util/gnu-patches/series @@ -0,0 +1,10 @@ +tests_factor_factor.pl.patch +tests_cksum_base64.patch +tests_comm.pl.patch +tests_cut_error_msg.patch +tests_dup_source.patch +tests_env_env-S.pl.patch +tests_invalid_opt.patch +tests_ls_no_cap.patch +tests_sort_merge.pl.patch +tests_tsort.patch diff --git a/util/gnu-patches/tests_cksum_base64.patch b/util/gnu-patches/tests_cksum_base64.patch index 2a8ed0af40e..ea6bf92e164 100644 --- a/util/gnu-patches/tests_cksum_base64.patch +++ b/util/gnu-patches/tests_cksum_base64.patch @@ -1,8 +1,8 @@ -diff --git a/tests/cksum/cksum-base64.pl b/tests/cksum/cksum-base64.pl -index a037a1628..c6d87d447 100755 ---- a/tests/cksum/cksum-base64.pl -+++ b/tests/cksum/cksum-base64.pl -@@ -91,8 +91,8 @@ my $prog = 'cksum'; +Index: gnu/tests/cksum/cksum-base64.pl +=================================================================== +--- gnu.orig/tests/cksum/cksum-base64.pl ++++ gnu/tests/cksum/cksum-base64.pl +@@ -92,8 +92,8 @@ my $prog = 'cksum'; my $fail = run_tests ($program_name, $prog, \@Tests, $save_temps, $verbose); # Ensure hash names from cksum --help match those in @pairs above. diff --git a/util/gnu-patches/tests_comm.pl.patch b/util/gnu-patches/tests_comm.pl.patch index d3d5595a2c5..602071f483a 100644 --- a/util/gnu-patches/tests_comm.pl.patch +++ b/util/gnu-patches/tests_comm.pl.patch @@ -1,7 +1,7 @@ -diff --git a/tests/misc/comm.pl b/tests/misc/comm.pl -index 5bd5f56d7..8322d92ba 100755 ---- a/tests/misc/comm.pl -+++ b/tests/misc/comm.pl +Index: gnu/tests/misc/comm.pl +=================================================================== +--- gnu.orig/tests/misc/comm.pl ++++ gnu/tests/misc/comm.pl @@ -73,18 +73,24 @@ my @Tests = # invalid missing command line argument (1) diff --git a/util/gnu-patches/tests_cut_error_msg.patch b/util/gnu-patches/tests_cut_error_msg.patch index 3f57d204813..1b1673fef72 100644 --- a/util/gnu-patches/tests_cut_error_msg.patch +++ b/util/gnu-patches/tests_cut_error_msg.patch @@ -1,7 +1,7 @@ -diff --git a/tests/cut/cut.pl b/tests/cut/cut.pl -index 1670db02e..ed633792a 100755 ---- a/tests/cut/cut.pl -+++ b/tests/cut/cut.pl +Index: gnu/tests/cut/cut.pl +=================================================================== +--- gnu.orig/tests/cut/cut.pl ++++ gnu/tests/cut/cut.pl @@ -29,13 +29,15 @@ my $mb_locale = $ENV{LOCALE_FR_UTF8}; my $prog = 'cut'; diff --git a/util/gnu-patches/tests_dup_source.patch b/util/gnu-patches/tests_dup_source.patch index 44e33723bc1..4c24498253d 100644 --- a/util/gnu-patches/tests_dup_source.patch +++ b/util/gnu-patches/tests_dup_source.patch @@ -1,8 +1,8 @@ -diff --git a/tests/mv/dup-source.sh b/tests/mv/dup-source.sh -index 7bcd82fc3..0f9005296 100755 ---- a/tests/mv/dup-source.sh -+++ b/tests/mv/dup-source.sh -@@ -83,7 +83,7 @@ $i: cannot stat 'a': No such file or directory +Index: gnu/tests/mv/dup-source.sh +=================================================================== +--- gnu.orig/tests/mv/dup-source.sh ++++ gnu/tests/mv/dup-source.sh +@@ -83,7 +83,7 @@ $i: cannot stat 'a': No such file or dir $i: cannot stat 'a': No such file or directory $i: cannot stat 'b': No such file or directory $i: cannot move './b' to a subdirectory of itself, 'b/b' diff --git a/util/gnu-patches/tests_env_env-S.pl.patch b/util/gnu-patches/tests_env_env-S.pl.patch index 404a00ca60e..4a1ae939a6b 100644 --- a/util/gnu-patches/tests_env_env-S.pl.patch +++ b/util/gnu-patches/tests_env_env-S.pl.patch @@ -1,7 +1,7 @@ -diff --git a/tests/env/env-S.pl b/tests/env/env-S.pl -index 710ca82cf..af7cf6efa 100755 ---- a/tests/env/env-S.pl -+++ b/tests/env/env-S.pl +Index: gnu/tests/env/env-S.pl +=================================================================== +--- gnu.orig/tests/env/env-S.pl ++++ gnu/tests/env/env-S.pl @@ -209,27 +209,28 @@ my @Tests = {ERR=>"$prog: no terminating quote in -S string\n"}], ['err5', q[-S'A=B\\q'], {EXIT=>125}, diff --git a/util/gnu-patches/tests_factor_factor.pl.patch b/util/gnu-patches/tests_factor_factor.pl.patch index 731abcc9117..892d526964e 100644 --- a/util/gnu-patches/tests_factor_factor.pl.patch +++ b/util/gnu-patches/tests_factor_factor.pl.patch @@ -1,7 +1,7 @@ -diff --git a/tests/factor/factor.pl b/tests/factor/factor.pl -index b1406c266..3d97cd6a5 100755 ---- a/tests/factor/factor.pl -+++ b/tests/factor/factor.pl +Index: gnu/tests/factor/factor.pl +=================================================================== +--- gnu.orig/tests/factor/factor.pl ++++ gnu/tests/factor/factor.pl @@ -61,12 +61,14 @@ my @Tests = # Map newer glibc diagnostic to expected. # Also map OpenBSD 5.1's "unknown option" to expected "invalid option". diff --git a/util/gnu-patches/tests_invalid_opt.patch b/util/gnu-patches/tests_invalid_opt.patch index 1c70bc8c92a..c23476674e7 100644 --- a/util/gnu-patches/tests_invalid_opt.patch +++ b/util/gnu-patches/tests_invalid_opt.patch @@ -1,7 +1,7 @@ -diff --git a/tests/misc/invalid-opt.pl b/tests/misc/invalid-opt.pl -index 4b9c4c184..4ccd89482 100755 ---- a/tests/misc/invalid-opt.pl -+++ b/tests/misc/invalid-opt.pl +Index: gnu/tests/misc/invalid-opt.pl +=================================================================== +--- gnu.orig/tests/misc/invalid-opt.pl ++++ gnu/tests/misc/invalid-opt.pl @@ -74,23 +74,13 @@ foreach my $prog (@built_programs) defined $out or $out = ''; diff --git a/util/gnu-patches/tests_ls_no_cap.patch b/util/gnu-patches/tests_ls_no_cap.patch index 5944e3f5661..8e36512ae9c 100644 --- a/util/gnu-patches/tests_ls_no_cap.patch +++ b/util/gnu-patches/tests_ls_no_cap.patch @@ -1,9 +1,9 @@ diff --git a/tests/ls/no-cap.sh b/tests/ls/no-cap.sh -index 3d84c74ff..d1f60e70a 100755 +index 99f0563bc..f7b9e7885 100755 --- a/tests/ls/no-cap.sh +++ b/tests/ls/no-cap.sh -@@ -21,13 +21,13 @@ print_ver_ ls - require_strace_ capget +@@ -27,11 +27,11 @@ setcap 'cap_net_bind_service=ep' file || + skip_ "setcap doesn't work" LS_COLORS=ca=1; export LS_COLORS -strace -e capget ls --color=always > /dev/null 2> out || fail=1 @@ -11,8 +11,6 @@ index 3d84c74ff..d1f60e70a 100755 +strace -e llistxattr ls --color=always > /dev/null 2> out || fail=1 +$EGREP 'llistxattr\(' out || skip_ "your ls doesn't call llistxattr" - rm -f out - LS_COLORS=ca=:; export LS_COLORS -strace -e capget ls --color=always > /dev/null 2> out || fail=1 -$EGREP 'capget\(' out && fail=1 diff --git a/util/gnu-patches/tests_sort_merge.pl.patch b/util/gnu-patches/tests_sort_merge.pl.patch index a19677a6d0a..d6db2e09c60 100644 --- a/util/gnu-patches/tests_sort_merge.pl.patch +++ b/util/gnu-patches/tests_sort_merge.pl.patch @@ -1,7 +1,7 @@ -diff --git a/tests/sort/sort-merge.pl b/tests/sort/sort-merge.pl -index 89eed0c64..c2f5aa7e5 100755 ---- a/tests/sort/sort-merge.pl -+++ b/tests/sort/sort-merge.pl +Index: gnu/tests/sort/sort-merge.pl +=================================================================== +--- gnu.orig/tests/sort/sort-merge.pl ++++ gnu/tests/sort/sort-merge.pl @@ -43,22 +43,22 @@ my @Tests = # check validation of --batch-size option ['nmerge-0', "-m --batch-size=0", @inputs, diff --git a/util/gnu-patches/tests_tsort.patch b/util/gnu-patches/tests_tsort.patch index 40c612c288f..1084f97043a 100644 --- a/util/gnu-patches/tests_tsort.patch +++ b/util/gnu-patches/tests_tsort.patch @@ -1,7 +1,7 @@ -diff --git a/tests/misc/tsort.pl b/tests/misc/tsort.pl -index 70bdc474c..4fd420a4e 100755 ---- a/tests/misc/tsort.pl -+++ b/tests/misc/tsort.pl +Index: gnu/tests/misc/tsort.pl +=================================================================== +--- gnu.orig/tests/misc/tsort.pl ++++ gnu/tests/misc/tsort.pl @@ -54,8 +54,10 @@ my @Tests = ['only-one', {IN => {f => ""}}, {IN => {g => ""}}, diff --git a/util/update-version.sh b/util/update-version.sh index 47e1695ae29..237beb1008d 100755 --- a/util/update-version.sh +++ b/util/update-version.sh @@ -17,8 +17,8 @@ # 10) Create the release on github https://github.com/uutils/coreutils/releases/new # 11) Make sure we have good release notes -FROM="0.0.27" -TO="0.0.28" +FROM="0.0.28" +TO="0.0.29" PROGS=$(ls -1d src/uu/*/Cargo.toml src/uu/stdbuf/src/libstdbuf/Cargo.toml src/uucore/Cargo.toml Cargo.toml) diff --git a/util/why-error.md b/util/why-error.md new file mode 100644 index 00000000000..b0c2e0b040d --- /dev/null +++ b/util/why-error.md @@ -0,0 +1,72 @@ +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 +* gnu/tests/dd/stderr.sh +* gnu/tests/du/long-from-unreadable.sh +* gnu/tests/du/move-dir-while-traversing.sh +* gnu/tests/expr/expr-multibyte.pl +* gnu/tests/expr/expr.pl +* gnu/tests/fmt/goal-option.sh +* gnu/tests/fmt/non-space.sh +* gnu/tests/head/head-elide-tail.pl +* gnu/tests/head/head-pos.sh +* gnu/tests/head/head-write-error.sh +* gnu/tests/help/help-version-getopt.sh +* gnu/tests/help/help-version.sh +* gnu/tests/ls/ls-misc.pl +* gnu/tests/ls/stat-free-symlinks.sh +* gnu/tests/misc/close-stdout.sh +* gnu/tests/misc/comm.pl +* gnu/tests/misc/kill.sh - https://github.com/uutils/coreutils/issues/7066 https://github.com/uutils/coreutils/issues/7067 +* gnu/tests/misc/nohup.sh +* gnu/tests/misc/numfmt.pl +* gnu/tests/misc/stdbuf.sh - https://github.com/uutils/coreutils/issues/7072 +* gnu/tests/misc/tee.sh - https://github.com/uutils/coreutils/issues/7073 +* gnu/tests/misc/time-style.sh +* gnu/tests/misc/tsort.pl - https://github.com/uutils/coreutils/issues/7074 +* gnu/tests/misc/write-errors.sh +* gnu/tests/mv/hard-link-1.sh +* gnu/tests/mv/mv-special-1.sh - https://github.com/uutils/coreutils/issues/7076 +* gnu/tests/mv/part-fail.sh +* gnu/tests/mv/part-hardlink.sh +* gnu/tests/od/od-N.sh +* gnu/tests/od/od-float.sh +* gnu/tests/printf/printf-cov.pl +* gnu/tests/printf/printf-indexed.sh +* gnu/tests/printf/printf-mb.sh +* gnu/tests/printf/printf-quote.sh +* gnu/tests/printf/printf.sh +* gnu/tests/ptx/ptx-overrun.sh +* gnu/tests/ptx/ptx.pl +* gnu/tests/rm/empty-inacc.sh - https://github.com/uutils/coreutils/issues/7033 +* gnu/tests/rm/ir-1.sh +* gnu/tests/rm/one-file-system.sh - https://github.com/uutils/coreutils/issues/7011 +* gnu/tests/rm/rm1.sh +* gnu/tests/rm/rm2.sh +* gnu/tests/shred/shred-passes.sh +* gnu/tests/sort/sort-continue.sh +* gnu/tests/sort/sort-debug-keys.sh +* gnu/tests/sort/sort-debug-warn.sh +* gnu/tests/sort/sort-files0-from.pl +* gnu/tests/sort/sort-float.sh +* gnu/tests/sort/sort-h-thousands-sep.sh +* gnu/tests/sort/sort-merge-fdlimit.sh +* gnu/tests/sort/sort-month.sh +* gnu/tests/sort/sort.pl +* gnu/tests/stat/stat-nanoseconds.sh +* gnu/tests/tac/tac-2-nonseekable.sh +* gnu/tests/tail/end-of-device.sh +* gnu/tests/tail/follow-stdin.sh +* gnu/tests/tail/inotify-rotate-resources.sh +* gnu/tests/tail/symlink.sh +* gnu/tests/touch/obsolescent.sh +* gnu/tests/tty/tty-eof.pl diff --git a/util/why-error.txt b/util/why-error.txt deleted file mode 100644 index bbfb0508c0f..00000000000 --- a/util/why-error.txt +++ /dev/null @@ -1,86 +0,0 @@ -This file documents why some tests are failing: - -gnu/tests/chgrp/from.sh - https://github.com/uutils/coreutils/issues/7039 -gnu/tests/chmod/symlinks.sh - https://github.com/uutils/coreutils/pull/7025 -gnu/tests/chroot/chroot-credentials.sh - https://github.com/uutils/coreutils/issues/7040 -gnu/tests/cp/cp-i.sh -gnu/tests/cp/preserve-gid.sh -gnu/tests/csplit/csplit-suppress-matched.pl -gnu/tests/date/date-debug.sh -gnu/tests/date/date-next-dow.pl -gnu/tests/date/date-tz.sh -gnu/tests/date/date.pl -gnu/tests/dd/direct.sh -gnu/tests/dd/no-allocate.sh -gnu/tests/dd/nocache_eof.sh -gnu/tests/dd/skip-seek-past-file.sh -gnu/tests/dd/stderr.sh -gnu/tests/df/over-mount-device.sh -gnu/tests/du/long-from-unreadable.sh -gnu/tests/du/move-dir-while-traversing.sh -gnu/tests/expr/expr-multibyte.pl -gnu/tests/expr/expr.pl -gnu/tests/fmt/goal-option.sh -gnu/tests/fmt/non-space.sh -gnu/tests/head/head-elide-tail.pl -gnu/tests/head/head-pos.sh -gnu/tests/head/head-write-error.sh -gnu/tests/help/help-version-getopt.sh -gnu/tests/help/help-version.sh -gnu/tests/id/setgid.sh -gnu/tests/ls/ls-misc.pl -gnu/tests/ls/stat-free-symlinks.sh -gnu/tests/misc/close-stdout.sh -gnu/tests/misc/comm.pl -gnu/tests/misc/dircolors.pl -gnu/tests/misc/echo.sh -gnu/tests/misc/kill.sh -gnu/tests/misc/nohup.sh -gnu/tests/misc/numfmt.pl -gnu/tests/misc/stdbuf.sh -gnu/tests/misc/tee.sh -gnu/tests/misc/time-style.sh -gnu/tests/misc/tsort.pl -gnu/tests/misc/write-errors.sh -gnu/tests/misc/xattr.sh - https://github.com/uutils/coreutils/pull/7009 -gnu/tests/mv/hard-link-1.sh -gnu/tests/mv/mv-special-1.sh -gnu/tests/mv/part-fail.sh -gnu/tests/mv/part-hardlink.sh -gnu/tests/od/od-N.sh -gnu/tests/od/od-float.sh -gnu/tests/printf/printf-cov.pl -gnu/tests/printf/printf-indexed.sh -gnu/tests/printf/printf-mb.sh -gnu/tests/printf/printf-quote.sh -gnu/tests/printf/printf.sh -gnu/tests/ptx/ptx-overrun.sh -gnu/tests/ptx/ptx.pl -gnu/tests/rm/empty-inacc.sh - https://github.com/uutils/coreutils/issues/7033 -gnu/tests/rm/ir-1.sh -gnu/tests/rm/one-file-system.sh - https://github.com/uutils/coreutils/issues/7011 -gnu/tests/rm/rm1.sh -gnu/tests/rm/rm2.sh -gnu/tests/seq/seq-precision.sh -gnu/tests/shred/shred-passes.sh -gnu/tests/sort/sort-continue.sh -gnu/tests/sort/sort-debug-keys.sh -gnu/tests/sort/sort-debug-warn.sh -gnu/tests/sort/sort-files0-from.pl -gnu/tests/sort/sort-float.sh -gnu/tests/sort/sort-h-thousands-sep.sh -gnu/tests/sort/sort-merge-fdlimit.sh -gnu/tests/sort/sort-month.sh -gnu/tests/sort/sort.pl -gnu/tests/split/line-bytes.sh -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/now-owned-by-other.sh -gnu/tests/touch/obsolescent.sh -gnu/tests/truncate/truncate-owned-by-other.sh -gnu/tests/tty/tty-eof.pl -gnu/tests/uniq/uniq.pl diff --git a/util/why-skip.txt b/util/why-skip.md similarity index 100% rename from util/why-skip.txt rename to util/why-skip.md