diff --git a/.cargo/config.toml b/.cargo/config.toml index 32631f684..f3a0a563e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -15,9 +15,4 @@ LIBSTDBUF_DIR = "/usr/local/libexec/coreutils" # remove me [build] -rustflags = [ - "-A", - "clippy::collapsible_if", - "-A", - "clippy::manual_is_multiple_of", -] +rustflags = ["-A", "clippy::collapsible_if"] diff --git a/.clippy.toml b/.clippy.toml index 113f003ad..f5d31e363 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -2,3 +2,4 @@ avoid-breaking-exported-api = false check-private-items = true cognitive-complexity-threshold = 24 missing-docs-in-crate-items = true +allow-mixed-uninlined-format-args = false diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 8361e0b7a..31dcb14f6 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -5,7 +5,7 @@ name: CICD # spell-checker:ignore (jargon) SHAs deps dequote softprops subshell toolchain fuzzers dedupe devel profdata # spell-checker:ignore (people) Peltoche rivy dtolnay Anson dawidd # spell-checker:ignore (shell/tools) binutils choco clippy dmake esac fakeroot fdesc fdescfs gmake grcov halium lcov libclang libfuse libssl limactl mkdir nextest nocross pacman popd printf pushd redoxer rsync rustc rustfmt rustup shopt sccache utmpdump xargs zstd -# spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils defconfig DESTDIR gecos getenforce gnueabihf issuecomment maint manpages msys multisize noconfirm nofeatures nullglob onexitbegin onexitend pell runtest Swatinem tempfile testsuite toybox uutils libsystemd codspeed +# spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils defconfig DESTDIR gecos getenforce gnueabihf issuecomment maint manpages msys multisize noconfirm nofeatures nullglob onexitbegin onexitend pell runtest Swatinem tempfile testsuite toybox uutils libsystemd codspeed wasip env: PROJECT_NAME: coreutils @@ -322,13 +322,13 @@ jobs: disable_search: true flags: makefile,${{ matrix.job.os }} fail_ci_if_error: false - - name: "`make install PROG_PREFIX=uu- PROFILE=release-fast COMPLETIONS=n MANPAGES=n LOCALES=n`" + - name: "`make install PROG_PREFIX=uu- PROFILE=release-small COMPLETIONS=n MANPAGES=n LOCALES=n`" shell: bash run: | set -x - DESTDIR=/tmp/ make install PROG_PREFIX=uu- PROFILE=release-fast COMPLETIONS=n MANPAGES=n LOCALES=n + DESTDIR=/tmp/ make install PROG_PREFIX=uu- PROFILE=release-small COMPLETIONS=n MANPAGES=n LOCALES=n # Check that utils are built with given profile - ./target/release-fast/true + ./target/release-small/true # Check that the progs have prefix test -f /tmp/usr/local/bin/uu-tty test -f /tmp/usr/local/libexec/uu-coreutils/libstdbuf.* @@ -501,10 +501,10 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.9 - - name: "`make install PROFILE=release-fast`" + - name: "`make install PROFILE=release`" shell: bash run: | - export CARGO_TARGET_DIR=cargo-target RUSTFLAGS="${RUSTFLAGS} -C strip=symbols" PROFILE=release-fast MANPAGES=n COMPLETIONS=n LOCALES=n + export CARGO_TARGET_DIR=cargo-target RUSTFLAGS="${RUSTFLAGS} -C strip=symbols" PROFILE=release MANPAGES=n COMPLETIONS=n LOCALES=n mkdir -p "${CARGO_TARGET_DIR}" && sudo mount -t tmpfs -o noatime,size=16G tmpfs "${CARGO_TARGET_DIR}" make install DESTDIR=target/size-release/ make install COMPLETIONS=n MULTICALL=y LN="ln -vf" DESTDIR=target/size-multi-release/ @@ -631,7 +631,7 @@ jobs: - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,uudoc" , use-cross: no, workspace-tests: true } - { os: ubuntu-latest , target: x86_64-unknown-linux-musl , features: feat_os_unix_musl , use-cross: use-cross } - { os: ubuntu-latest , target: x86_64-unknown-redox , features: feat_os_unix_redox , use-cross: redoxer , skip-tests: true } - - { os: ubuntu-latest , target: wasm32-unknown-unknown , default-features: false, features: uucore/format, skip-tests: true, skip-package: true, skip-publish: true } + - { os: ubuntu-latest , target: wasm32-wasip1, default-features: false, features: "basenc,cksum", skip-tests: true } - { os: macos-latest , target: aarch64-apple-darwin , features: feat_os_macos, workspace-tests: true } # M1 CPU # PR #7964: Mac should still build even if the feature is not enabled. Do not publish this. - { os: macos-latest , target: aarch64-apple-darwin , workspace-tests: true, skip-publish: true } # M1 CPU @@ -672,7 +672,11 @@ jobs: STAGING='_staging' outputs STAGING # determine EXE suffix - EXE_suffix="" ; case '${{ matrix.job.target }}' in *-pc-windows-*) EXE_suffix=".exe" ;; esac; + unset EXE_suffix + case '${{ matrix.job.target }}' in + *-pc-windows-*) EXE_suffix=".exe" ;; + *wasm-*) EXE_suffix=".wasm" ;; + esac; outputs EXE_suffix # parse commit reference info echo GITHUB_REF=${GITHUB_REF} @@ -868,7 +872,7 @@ jobs: run: | ## Package artifact(s) # binaries - cp 'target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}${{ steps.vars.outputs.EXE_suffix }}' '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/' + cp 'target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}'* '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/' cp 'target/${{ matrix.job.target }}/release/uudoc${{ steps.vars.outputs.EXE_suffix }}' '${{ steps.vars.outputs.STAGING }}/${{ steps.vars.outputs.PKG_BASENAME }}/' || : # README and LICENSE # * spell-checker:ignore EADME ICENSE diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 000000000..d30fb0453 --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,15 @@ +name: Security audit + +# spell-checker:ignore (misc) rustsec + +on: + schedule: + - cron: "0 0 * * *" +jobs: + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: rustsec/audit-check@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 9793d9dc3..53f830a79 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -42,7 +42,7 @@ jobs: - name: Get current version from Cargo.toml id: version run: | - VERSION=$(awk '/\[workspace\.package\]/{flag=1; next} flag && /version = /{gsub(/.*= "/, ""); gsub(/".*/, ""); print; exit}' Cargo.toml) + VERSION=$(awk '/\[workspace\.package\]/{flag=1; next} flag && /^version = /{gsub(/.*= "/, ""); gsub(/".*/, ""); print; exit}' Cargo.toml) echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Detected version: $VERSION" diff --git a/Cargo.lock b/Cargo.lock index 3c3e94dd5..dabc2e4fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,19 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy 0.8.39", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -667,8 +680,6 @@ dependencies = [ "uucore", "uutests", "walkdir", - "wincode", - "wincode-derive", "zip", ] @@ -755,7 +766,7 @@ dependencies = [ "mio", "parking_lot", "rustix", - "signal-hook 0.3.18", + "signal-hook", "signal-hook-mio", "winapi", ] @@ -812,41 +823,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core", - "quote", - "syn", -] - [[package]] name = "data-encoding" version = "2.10.0" @@ -1130,12 +1106,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "foldhash" version = "0.1.5" @@ -1605,12 +1575,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e10b0e5e87a2c84bd5fa407705732052edebe69291d347d0c3033785470edbf" -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "indexmap" version = "2.13.0" @@ -1808,9 +1772,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "libloading" @@ -1903,9 +1867,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" @@ -2200,12 +2164,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "pastey" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" - [[package]] name = "phf" version = "0.13.1" @@ -2768,16 +2726,6 @@ dependencies = [ "signal-hook-registry", ] -[[package]] -name = "signal-hook" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b57709da74f9ff9f4a27dce9526eec25ca8407c45a7887243b031a58935fb8e" -dependencies = [ - "libc", - "signal-hook-registry", -] - [[package]] name = "signal-hook-mio" version = "0.2.5" @@ -2786,7 +2734,7 @@ checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", - "signal-hook 0.3.18", + "signal-hook", ] [[package]] @@ -3413,7 +3361,6 @@ dependencies = [ "gcd", "libc", "nix", - "signal-hook 0.4.3", "tempfile", "thiserror 2.0.18", "uucore", @@ -4087,6 +4034,7 @@ dependencies = [ name = "uu_sort" version = "0.6.0" dependencies = [ + "ahash", "bigdecimal", "binary-heap-plus", "clap", @@ -4094,7 +4042,6 @@ dependencies = [ "compare", "ctrlc", "fluent", - "fnv", "itertools 0.14.0", "memchr", "nix", @@ -4659,30 +4606,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "wincode" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f27315388539620cef3386d77b05305dbfb7fccbd32eead533a7a3779313532" -dependencies = [ - "pastey", - "proc-macro2", - "quote", - "thiserror 2.0.18", -] - -[[package]] -name = "wincode-derive" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89833c7743abd1a831b7eb0fdfd7210bd8bedc297435e9426aad9b63d7c7f77" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-core" version = "0.62.2" diff --git a/Cargo.toml b/Cargo.toml index 171abefa8..2e2d0a960 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,21 @@ # coreutils (uutils) # * see the repository LICENSE, README, and CONTRIBUTING files for more information -# spell-checker:ignore (libs) bigdecimal datetime serde wincode gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested logind cfgs interner +# spell-checker:ignore (libs) ahash bigdecimal datetime serde gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested logind cfgs interner [package] name = "coreutils" description = "coreutils ~ GNU coreutils (updated); implemented as universal (cross-platform) utils, written in Rust" default-run = "coreutils" repository = "https://github.com/uutils/coreutils" -rust-version = "1.88.0" +edition.workspace = true +rust-version.workspace = true version.workspace = true authors.workspace = true license.workspace = true homepage.workspace = true keywords.workspace = true categories.workspace = true -edition.workspace = true [package.metadata.docs.rs] all-features = true @@ -303,6 +303,7 @@ members = [ authors = ["uutils developers"] categories = ["command-line-utilities"] edition = "2024" +rust-version = "1.88.0" homepage = "https://github.com/uutils/coreutils" keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] license = "MIT" @@ -310,6 +311,7 @@ readme = "README.package.md" version = "0.6.0" [workspace.dependencies] +ahash = "0.8.12" ansi-width = "0.1.0" bigdecimal = "0.4" binary-heap-plus = "0.5.0" @@ -328,7 +330,6 @@ dns-lookup = { version = "3.0.0" } exacl = "0.12.0" file_diff = "1.0.0" filetime = "0.2.23" -fnv = "1.0.7" fs_extra = "1.3.0" fts-sys = "0.2.16" gcd = "2.3" @@ -376,7 +377,6 @@ same-file = "1.0.6" self_cell = "1.0.4" selinux = "=0.6.0" string-interner = "0.19.0" -signal-hook = "0.4.1" tempfile = "3.15.0" terminal_size = "0.4.0" textwrap = { version = "0.16.1", features = ["terminal_size"] } @@ -587,11 +587,6 @@ nix = { workspace = true, features = [ ] } rlimit = { workspace = true } -# Used in test_uptime::test_uptime_with_file_containing_valid_boot_time_utmpx_record -# to deserialize an utmpx struct into a binary file -[target.'cfg(all(target_family= "unix",not(target_os = "macos")))'.dev-dependencies] -wincode = "0.3.1" -wincode-derive = "0.3.1" [build-dependencies] phf_codegen.workspace = true @@ -605,18 +600,17 @@ name = "uudoc" path = "src/bin/uudoc.rs" required-features = ["uudoc"] -# The default release profile with some optimizations. [profile.release] lto = true panic = "abort" - -[profile.release-fast] -inherits = "release" codegen-units = 1 +# FIXME: https://github.com/uutils/coreutils/issues/10654 +[profile.release.package.uu_expand] +codegen-units = 16 # A release-like profile that is as small as possible. [profile.release-small] -inherits = "release-fast" +inherits = "release" opt-level = "z" strip = true @@ -642,8 +636,7 @@ unexpected_cfgs = { level = "warn", check-cfg = [ unused_qualifications = "warn" [workspace.lints.clippy] -manual_is_multiple_of = { level = "allow", priority = 127 } # remove me -collapsible_if = { level = "allow", priority = 127 } # remove me +collapsible_if = { level = "allow", priority = 127 } # remove me # The counts were generated with this command: # cargo clippy --all-targets --workspace --message-format=json --quiet \ # | jq -r '.message.code.code | select(. != null and startswith("clippy::"))' \ diff --git a/README.md b/README.md index c9e2f66b0..6708d9dca 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ other Rust program: cargo build --release ``` -Replace `--release` with `--profile=release-fast` or `--profile=release-small` to use all optimizations or save binary size. +Replace `--release` with `--profile=release-small` to optimize binary size. This command builds the most portable common core set of uutils into a multicall (BusyBox-type) binary, named 'coreutils', on most Rust-supported platforms. @@ -154,10 +154,10 @@ To simply build all available utilities (with debug profile): make ``` -In release-fast mode: +In release mode: ```shell -make PROFILE=release-fast +make PROFILE=release ``` To build all but a few of the available utilities: @@ -191,18 +191,12 @@ manpages or shell completion to work, use `GNU Make` or see ### Install with GNU Make -To install all available utilities: +To install all available utilities (PROFILE=release by default): ```shell make install ``` -To install all utilities with all possible optimizations: - -```shell -make PROFILE=release-fast install -``` - To install using `sudo` switch `-E` must be used: ```shell diff --git a/build.rs b/build.rs index 3c4da1edb..9534a79b8 100644 --- a/build.rs +++ b/build.rs @@ -72,26 +72,37 @@ pub fn main() { .unwrap(); let mut phf_map = phf_codegen::OrderedMap::<&str>::new(); + let mut entries = Vec::new(); + for krate in &crates { let map_value = format!("({krate}::uumain, {krate}::uu_app)"); match krate.as_ref() { // 'test' is named uu_test to avoid collision with rust core crate 'test'. // It can also be invoked by name '[' for the '[ expr ] syntax'. "uu_test" => { - phf_map.entry("test", map_value.clone()); - phf_map.entry("[", map_value.clone()); + entries.push(("test", map_value.clone())); + entries.push(("[", map_value.clone())); } k if k.starts_with(OVERRIDE_PREFIX) => { - phf_map.entry(&k[OVERRIDE_PREFIX.len()..], map_value.clone()); + entries.push((&k[OVERRIDE_PREFIX.len()..], map_value.clone())); } "false" | "true" => { - phf_map.entry(krate, format!("(r#{krate}::uumain, r#{krate}::uu_app)")); + entries.push(( + krate.as_str(), + format!("(r#{krate}::uumain, r#{krate}::uu_app)"), + )); } _ => { - phf_map.entry(krate, map_value.clone()); + entries.push((krate.as_str(), map_value.clone())); } } } + entries.sort_by_key(|(name, _)| *name); + + for (name, value) in entries { + phf_map.entry(name, value); + } + write!(mf, "{}", phf_map.build()).unwrap(); mf.write_all(b"\n}\n").unwrap(); diff --git a/deny.toml b/deny.toml index 242227509..abc0acbd2 100644 --- a/deny.toml +++ b/deny.toml @@ -104,8 +104,6 @@ skip = [ { name = "zerocopy", version = "0.7.35" }, # zerocopy { name = "zerocopy-derive", version = "0.7.35" }, - # crossterm - { name = "signal-hook", version = "0.3.18" }, ] # spell-checker: enable diff --git a/docs/src/packaging.md b/docs/src/packaging.md index ed664863e..2ea169668 100644 --- a/docs/src/packaging.md +++ b/docs/src/packaging.md @@ -55,17 +55,10 @@ view the full documentation in the We provide three release profiles out of the box, though you may want to tweak them: -- `release`: This is the standard Rust release profile, but with link-time - optimization enabled. It is a balance between compile time, performance and a - reasonable amount of debug info. The main drawback of this profile is that the - binary is quite large (roughly 2x the GNU coreutils). -- `release-fast`: Every setting is tuned for the best performance, at the cost - of compile time. This binary is still quite large. -- `release-small`: Generates the smallest binary possible. This strips _all_ - debug info from the binary, resulting in less informative backtraces. The performance of - this profile is also really good as it is close to the `release-fast` profile, - but with all debuginfo stripped. +- `release`: The profile with all performance optimization enabled. +- `release-small`: Optimize binary size. +They include panic abort which removes stack traces on old rust [https://blog.rust-lang.org/2025/12/11/Rust-1.92.0/]. For the precise definition of these profiles, you can look at the root [`Cargo.toml`](https://github.com/uutils/coreutils/blob/main/Cargo.toml). diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index c4d05638d..bfc7f4383 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -8,6 +8,19 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -207,9 +220,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.54" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "jobserver", @@ -242,18 +255,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.54" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" dependencies = [ "anstream", "anstyle", @@ -518,9 +531,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixed_decimal" @@ -535,9 +548,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -588,12 +601,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "generic-array" version = "0.14.7" @@ -647,9 +654,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -974,9 +981,9 @@ checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992" [[package]] name = "jiff" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +checksum = "d89a5b5e10d5a9ad6e5d1f4bd58225f655d6fe9767575a5e8ac5a6fe64e04495" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -1000,9 +1007,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +checksum = "ff7a39c8862fc1369215ccf0a8f12dd4598c7f6484704359f0351bd617034dbf" dependencies = [ "proc-macro2", "quote", @@ -1055,9 +1062,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.180" +version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "libfuzzer-sys" @@ -1105,9 +1112,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miniz_oxide" @@ -1261,15 +1268,15 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" dependencies = [ "portable-atomic", ] @@ -1393,9 +1400,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" [[package]] name = "rust-ini" @@ -1820,13 +1827,13 @@ dependencies = [ name = "uu_sort" version = "0.6.0" dependencies = [ + "ahash", "bigdecimal", "binary-heap-plus", "clap", "compare", "ctrlc", "fluent", - "fnv", "itertools", "memchr", "nix", @@ -2271,18 +2278,18 @@ checksum = "9b3a41ce106832b4da1c065baa4c31cf640cf965fa1483816402b7f6b96f0a64" [[package]] name = "zerocopy" -version = "0.8.34" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.34" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 6d5e2d4d6..81edb902c 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -4,6 +4,7 @@ version = "0.0.0" description = "uutils ~ 'core' uutils fuzzers" repository = "https://github.com/uutils/coreutils/tree/main/fuzz/" edition.workspace = true +rust-version.workspace = true license.workspace = true publish = false @@ -13,6 +14,7 @@ members = ["."] [workspace.package] edition = "2024" +rust-version = "1.88.0" license = "MIT" [package.metadata] diff --git a/fuzz/uufuzz/Cargo.toml b/fuzz/uufuzz/Cargo.toml index 5e66b0b49..3c2240998 100644 --- a/fuzz/uufuzz/Cargo.toml +++ b/fuzz/uufuzz/Cargo.toml @@ -5,6 +5,7 @@ description = "uutils ~ 'core' uutils fuzzing library" repository = "https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz" version = "0.6.0" edition.workspace = true +rust-version.workspace = true license.workspace = true [dependencies] diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 0ea01c778..59634849a 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -30,7 +30,7 @@ fn usage(utils: &UtilityMap, name: &str) { println!("Options:"); println!(" --list lists all defined functions, one per row\n"); println!("Currently defined functions:\n"); - let display_list = utils.keys().copied().sorted_unstable().join(", "); + let display_list = utils.keys().copied().join(", "); let width = cmp::min(textwrap::termwidth(), 100) - 4 * 2; // (opinion/heuristic) max 100 chars wide with 4 character side indentions println!( "{}", @@ -81,8 +81,7 @@ fn main() { usage(&utils, binary_as_util); process::exit(0); } - let mut utils: Vec<_> = utils.keys().collect(); - utils.sort(); + let utils: Vec<_> = utils.keys().collect(); for util in utils { println!("{util}"); } diff --git a/src/common/validation.rs b/src/common/validation.rs index 41f686e8d..81ae1351c 100644 --- a/src/common/validation.rs +++ b/src/common/validation.rs @@ -32,8 +32,7 @@ pub fn not_found(util: &OsStr) -> ! { /// Prints an "unrecognized option" error and exits pub fn unrecognized_option(binary_name: &str, option: &OsStr) -> ! { eprintln!( - "{}: unrecognized option '{}'", - binary_name, + "{binary_name}: unrecognized option '{}'", option.to_string_lossy() ); process::exit(1); diff --git a/src/uu/arch/Cargo.toml b/src/uu/arch/Cargo.toml index 39a2410ba..b906a3929 100644 --- a/src/uu/arch/Cargo.toml +++ b/src/uu/arch/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/b2sum/Cargo.toml b/src/uu/b2sum/Cargo.toml index 81f6f51fc..866d3558f 100644 --- a/src/uu/b2sum/Cargo.toml +++ b/src/uu/b2sum/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/base32/Cargo.toml b/src/uu/base32/Cargo.toml index 2318911b5..bec73709c 100644 --- a/src/uu/base32/Cargo.toml +++ b/src/uu/base32/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/base64/Cargo.toml b/src/uu/base64/Cargo.toml index 67442ec22..38742d357 100644 --- a/src/uu/base64/Cargo.toml +++ b/src/uu/base64/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/basename/Cargo.toml b/src/uu/basename/Cargo.toml index 9fe4adc03..00230ea76 100644 --- a/src/uu/basename/Cargo.toml +++ b/src/uu/basename/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/basenc/Cargo.toml b/src/uu/basenc/Cargo.toml index f68a0f110..c36eb9c23 100644 --- a/src/uu/basenc/Cargo.toml +++ b/src/uu/basenc/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/cat/Cargo.toml b/src/uu/cat/Cargo.toml index 7dbd1ecbf..880972fa6 100644 --- a/src/uu/cat/Cargo.toml +++ b/src/uu/cat/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/chcon/Cargo.toml b/src/uu/chcon/Cargo.toml index b18da48f8..71e2c70e1 100644 --- a/src/uu/chcon/Cargo.toml +++ b/src/uu/chcon/Cargo.toml @@ -9,6 +9,7 @@ license.workspace = true homepage.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/checksum_common/Cargo.toml b/src/uu/checksum_common/Cargo.toml index b8f99e821..428e7c425 100644 --- a/src/uu/checksum_common/Cargo.toml +++ b/src/uu/checksum_common/Cargo.toml @@ -8,6 +8,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/chgrp/Cargo.toml b/src/uu/chgrp/Cargo.toml index 2a629542b..f6989c88c 100644 --- a/src/uu/chgrp/Cargo.toml +++ b/src/uu/chgrp/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/chmod/Cargo.toml b/src/uu/chmod/Cargo.toml index e1be6896f..7ac7d8323 100644 --- a/src/uu/chmod/Cargo.toml +++ b/src/uu/chmod/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index 78dccffdc..127464dc1 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -318,19 +318,13 @@ impl Chmoder { if new_mode != old_mode { println!( - "mode of {} changed from {:04o} ({}) to {:04o} ({})", + "mode of {} changed from {old_mode:04o} ({current_permissions}) to {new_mode:04o} ({new_permissions})", file_path.quote(), - old_mode, - current_permissions, - new_mode, - new_permissions ); } else if self.verbose { println!( - "mode of {} retained as {:04o} ({})", + "mode of {} retained as {old_mode:04o} ({current_permissions})", file_path.quote(), - old_mode, - current_permissions ); } } @@ -600,9 +594,8 @@ impl Chmoder { if let Err(_e) = dir_fd.chmod_at(entry_name, new_mode, follow_symlinks) { if self.verbose { println!( - "failed to change mode of {} to {:o}", + "failed to change mode of {} to {new_mode:o}", file_path.quote(), - new_mode ); } return Err(ChmodError::PermissionDenied(file_path.into()).into()); diff --git a/src/uu/chown/Cargo.toml b/src/uu/chown/Cargo.toml index d11be8c44..211475c3f 100644 --- a/src/uu/chown/Cargo.toml +++ b/src/uu/chown/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/chroot/Cargo.toml b/src/uu/chroot/Cargo.toml index d251c9823..4b91c1a1f 100644 --- a/src/uu/chroot/Cargo.toml +++ b/src/uu/chroot/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/cksum/Cargo.toml b/src/uu/cksum/Cargo.toml index 5f509e313..37212eec8 100644 --- a/src/uu/cksum/Cargo.toml +++ b/src/uu/cksum/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/cksum/benches/cksum_bench.rs b/src/uu/cksum/benches/cksum_bench.rs index c316ec274..81f70bbb3 100644 --- a/src/uu/cksum/benches/cksum_bench.rs +++ b/src/uu/cksum/benches/cksum_bench.rs @@ -57,7 +57,7 @@ macro_rules! bench_shake_algorithm { let data = text_data::generate_by_size(100, 80); bencher.bench(|| { - let mut shake = Shake128::new(); + let mut shake = Shake128::with_output_bits(256); shake.hash_update(&data); // SHAKE algorithms can output any length, use 256 bits (32 bytes) for meaningful comparison @@ -76,7 +76,7 @@ macro_rules! bench_shake_algorithm { let data = text_data::generate_by_size(100, 80); bencher.bench(|| { - let mut shake = Shake256::new(); + let mut shake = Shake256::with_output_bits(512); shake.hash_update(&data); // SHAKE algorithms can output any length, use 256 bits (32 bytes) for meaningful comparison diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 5c78af2c6..51dd83d3a 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -100,6 +100,16 @@ fn maybe_sanitize_length( sanitize_sha2_sha3_length_str(algo, s_len).map(Some) } + // SHAKE128 and SHAKE256 algorithms optionally take a bit length. No + // validation is performed on this length, any value is valid. If the + // given length is not a multiple of 8, the last byte of the output + // will have its extra bits set to zero. + (Some(AlgoKind::Shake128 | AlgoKind::Shake256), Some(len)) => match len.parse::() { + Ok(0) => Ok(None), + Ok(l) => Ok(Some(l)), + Err(_) => Err(ChecksumError::InvalidLength(len.into()).into()), + }, + // For BLAKE2b, if a length is provided, validate it. (Some(AlgoKind::Blake2b), Some(len)) => calculate_blake2b_length_str(len), diff --git a/src/uu/comm/Cargo.toml b/src/uu/comm/Cargo.toml index 8f8f6fba7..5f1436e44 100644 --- a/src/uu/comm/Cargo.toml +++ b/src/uu/comm/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index 592b9cba9..79e00d6bb 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/cp/src/copydir.rs b/src/uu/cp/src/copydir.rs index 42e30b452..7f63b5e7e 100644 --- a/src/uu/cp/src/copydir.rs +++ b/src/uu/cp/src/copydir.rs @@ -31,6 +31,16 @@ use crate::{ copy_file, }; +/// Represents a directory that needs permission fixup after copying its contents. +struct DirNeedingPermissions { + /// Absolute path to the source directory + source: PathBuf, + /// Path to the destination directory + dest: PathBuf, + /// Whether this directory was freshly created by the copy operation + was_created: bool, +} + /// Ensure a Windows path starts with a `\\?`. #[cfg(target_os = "windows")] fn adjust_canonicalization(p: &Path) -> Cow<'_, Path> { @@ -237,6 +247,12 @@ impl Entry { #[allow(clippy::too_many_arguments)] /// Copy a single entry during a directory traversal. +/// +/// # Returns +/// +/// Returns `Ok(true)` if this function created a new directory, `Ok(false)` otherwise. +/// This information is used to determine whether default directory permissions should +/// be preserved during attribute copying. fn copy_direntry( progress_bar: Option<&ProgressBar>, entry: &Entry, @@ -248,7 +264,7 @@ fn copy_direntry( copied_destinations: &HashSet, copied_files: &mut HashMap, created_parent_dirs: &mut HashSet, -) -> CopyResult<()> { +) -> CopyResult { let source_is_symlink = entry_is_symlink; let source_is_dir = if source_is_symlink && !options.dereference { false @@ -276,7 +292,7 @@ fn copy_direntry( context_for(&entry.source_relative, &entry.local_to_target) ); } - Ok(()) + Ok(true) }; } @@ -326,7 +342,7 @@ fn copy_direntry( // In any other case, there is nothing to do, so we just return to // continue the traversal. - Ok(()) + Ok(false) } /// Read the contents of the directory `root` and recursively copy the @@ -421,7 +437,7 @@ pub(crate) fn copy_directory( let mut last_iter: Option = None; // Keep track of all directories we've created that need permission fixes - let mut dirs_needing_permissions: Vec<(PathBuf, PathBuf)> = Vec::new(); + let mut dirs_needing_permissions: Vec = Vec::new(); // Traverse the contents of the directory, copying each one. for direntry_result in WalkDir::new(root) @@ -442,7 +458,7 @@ pub(crate) fn copy_directory( }; let entry = Entry::new(&context, direntry_path, options.no_target_dir)?; - copy_direntry( + let created = copy_direntry( progress_bar, &entry, entry_is_symlink, @@ -475,12 +491,16 @@ pub(crate) fn copy_directory( &entry.source_absolute, &entry.local_to_target, &options.attributes, + false, )?; continue; } // Add this directory to our list for permission fixing later - dirs_needing_permissions - .push((entry.source_absolute.clone(), entry.local_to_target.clone())); + dirs_needing_permissions.push(DirNeedingPermissions { + source: entry.source_absolute.clone(), + dest: entry.local_to_target.clone(), + was_created: created, + }); // If true, last_iter is not a parent of this iter. // The means we just exited a directory. @@ -513,6 +533,7 @@ pub(crate) fn copy_directory( &entry.source_absolute, &entry.local_to_target, &options.attributes, + false, )?; } } @@ -528,8 +549,8 @@ pub(crate) fn copy_directory( // Fix permissions for all directories we created // This ensures that even sibling directories get their permissions fixed - for (source_path, dest_path) in dirs_needing_permissions { - copy_attributes(&source_path, &dest_path, &options.attributes)?; + for dir in dirs_needing_permissions { + copy_attributes(&dir.source, &dir.dest, &options.attributes, dir.was_created)?; } // Also fix permissions for parent directories, @@ -538,7 +559,7 @@ pub(crate) fn copy_directory( let dest = target.join(root.file_name().unwrap()); for (x, y) in aligned_ancestors(root, dest.as_path()) { if let Ok(src) = canonicalize(x, MissingHandling::Normal, ResolveMode::Physical) { - copy_attributes(&src, y, &options.attributes)?; + copy_attributes(&src, y, &options.attributes, false)?; } } } diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 8fb5629fa..db38d5c3e 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1344,7 +1344,7 @@ fn show_error_if_needed(error: &CpError) { // Format IoErrContext using strip_errno to remove "(os error N)" suffix // for GNU-compatible output CpError::IoErrContext(io_err, context) => { - show_error!("{}: {}", context, uucore::error::strip_errno(io_err)); + show_error!("{context}: {}", uucore::error::strip_errno(io_err)); } _ => { show_error!("{error}"); @@ -1550,7 +1550,7 @@ fn copy_source( if options.parents { for (x, y) in aligned_ancestors(source, dest.as_path()) { if let Ok(src) = canonicalize(x, MissingHandling::Normal, ResolveMode::Physical) { - copy_attributes(&src, y, &options.attributes)?; + copy_attributes(&src, y, &options.attributes, false)?; } } } @@ -1717,11 +1717,21 @@ pub(crate) fn copy_attributes( source: &Path, dest: &Path, attributes: &Attributes, + dest_is_freshly_created_dir: bool, ) -> CopyResult<()> { let context = &*format!("{} -> {}", source.quote(), dest.quote()); let source_metadata = fs::symlink_metadata(source).map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; + let mode_explicitly_disabled = matches!(attributes.mode, Preserve::No { explicit: true }); + + // preserve is true by default if the destination is created by us and it's a directory + let mode = if !mode_explicitly_disabled && dest_is_freshly_created_dir { + Preserve::Yes { required: false } + } else { + attributes.mode + }; + // Ownership must be changed first to avoid interfering with mode change. #[cfg(unix)] handle_preserve(attributes.ownership, || -> CopyResult<()> { @@ -1759,7 +1769,7 @@ pub(crate) fn copy_attributes( Ok(()) })?; - handle_preserve(attributes.mode, || -> CopyResult<()> { + handle_preserve(mode, || -> CopyResult<()> { // The `chmod()` system call that underlies the // `fs::set_permissions()` call is unable to change the // permissions of a symbolic link. In that case, we just @@ -2573,14 +2583,14 @@ fn copy_file( .ok() .filter(|p| p.exists()) .unwrap_or_else(|| source.to_path_buf()); - copy_attributes(&src_for_attrs, dest, &options.attributes)?; + copy_attributes(&src_for_attrs, dest, &options.attributes, false)?; } else if source_is_stream && !source.exists() { // Some stream files may not exist after we have copied it, // like anonymous pipes. Thus, we can't really copy its // attributes. However, this is already handled in the stream // copy function (see `copy_stream` under platform/linux.rs). } else { - copy_attributes(source, dest, &options.attributes)?; + copy_attributes(source, dest, &options.attributes, false)?; } #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] @@ -2762,7 +2772,7 @@ fn copy_link( delete_path(dest, options)?; } symlink_file(&link, dest, symlinked_files)?; - copy_attributes(source, dest, &options.attributes) + copy_attributes(source, dest, &options.attributes, false) } /// Generate an error message if `target` is not the correct `target_type` diff --git a/src/uu/csplit/Cargo.toml b/src/uu/csplit/Cargo.toml index b07c47648..e2aeab2ab 100644 --- a/src/uu/csplit/Cargo.toml +++ b/src/uu/csplit/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/cut/Cargo.toml b/src/uu/cut/Cargo.toml index f7ea5b203..e45672e54 100644 --- a/src/uu/cut/Cargo.toml +++ b/src/uu/cut/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index 3cf27595e..c1a604035 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -10,6 +10,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/dd/Cargo.toml b/src/uu/dd/Cargo.toml index 5d5819c30..e92ac0f0f 100644 --- a/src/uu/dd/Cargo.toml +++ b/src/uu/dd/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] @@ -32,8 +33,7 @@ thiserror = { workspace = true } fluent = { workspace = true } [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] -nix = { workspace = true, features = ["fs"] } -signal-hook = { workspace = true } +nix = { workspace = true, features = ["fs", "signal"] } [[bin]] name = "dd" diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 48f7ef1b9..e1d7b0ad6 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -23,6 +23,8 @@ use nix::fcntl::OFlag; use parseargs::Parser; use progress::ProgUpdateType; use progress::{ProgUpdate, ReadStat, StatusLevel, WriteStat, gen_prog_updater}; +#[cfg(target_os = "linux")] +use progress::{check_and_reset_sigusr1, install_sigusr1_handler}; use uucore::io::OwnedFileDescriptorOrHandle; use uucore::translate; @@ -90,7 +92,7 @@ struct Settings { /// /// After being constructed with [`Alarm::with_interval`], [`Alarm::get_trigger`] /// will return [`ALARM_TRIGGER_TIMER`] once per the given [`Duration`]. -/// Alarm can be manually triggered with closure returned by [`Alarm::manual_trigger_fn`]. +/// Alarm can be manually triggered with [`Alarm::manual_trigger`]. /// [`Alarm::get_trigger`] will return [`ALARM_TRIGGER_SIGNAL`] in this case. /// /// Can be cloned, but the trigger status is shared across all instances so only @@ -122,18 +124,9 @@ impl Alarm { Self { interval, trigger } } - /// Returns a closure that allows to manually trigger the alarm - /// - /// This is useful for cases where more than one alarm even source exists - /// In case of `dd` there is the SIGUSR1/SIGINFO case where we want to - /// trigger an manual progress report. - pub fn manual_trigger_fn(&self) -> Box { - let weak_trigger = Arc::downgrade(&self.trigger); - Box::new(move || { - if let Some(trigger) = weak_trigger.upgrade() { - trigger.store(ALARM_TRIGGER_SIGNAL, Relaxed); - } - }) + /// Manually trigger the alarm as a signal event + pub fn manual_trigger(&self) { + self.trigger.store(ALARM_TRIGGER_SIGNAL, Relaxed); } /// Use this function to poll for any pending alarm event @@ -748,7 +741,7 @@ fn handle_o_direct_write(f: &mut File, buf: &[u8], original_error: io::Error) -> // Log any restoration errors without failing the operation if let Err(os_err) = fcntl(&mut *f, FcntlArg::F_SETFL(oflags)) { // Just log the error, don't fail the whole operation - show_error!("Failed to restore O_DIRECT flag: {}", os_err); + show_error!("Failed to restore O_DIRECT flag: {os_err}"); } write_result @@ -1183,14 +1176,9 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { // This avoids the need to query the OS monotonic clock for every block. let alarm = Alarm::with_interval(Duration::from_secs(1)); - // The signal handler spawns an own thread that waits for signals. - // When the signal is received, it calls a handler function. - // We inject a handler function that manually triggers the alarm. #[cfg(target_os = "linux")] - let signal_handler = progress::SignalHandler::install_signal_handler(alarm.manual_trigger_fn()); - #[cfg(target_os = "linux")] - if let Err(e) = &signal_handler { - if Some(StatusLevel::None) != i.settings.status { + if let Err(e) = install_sigusr1_handler() { + if i.settings.status != Some(StatusLevel::None) { eprintln!("{}\n\t{e}", translate!("dd-warning-signal-handler")); } } @@ -1270,6 +1258,10 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { // error. rstat += rstat_update; wstat += wstat_update; + #[cfg(target_os = "linux")] + if check_and_reset_sigusr1() { + alarm.manual_trigger(); + } match alarm.get_trigger() { ALARM_TRIGGER_NONE => {} t @ (ALARM_TRIGGER_TIMER | ALARM_TRIGGER_SIGNAL) => { diff --git a/src/uu/dd/src/progress.rs b/src/uu/dd/src/progress.rs index c7aef3387..cc5527f55 100644 --- a/src/uu/dd/src/progress.rs +++ b/src/uu/dd/src/progress.rs @@ -10,13 +10,10 @@ //! [`gen_prog_updater`] function can be used to implement a progress //! updater that runs in its own thread. use std::io::Write; +#[cfg(target_os = "linux")] +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc; -#[cfg(target_os = "linux")] -use std::thread::JoinHandle; use std::time::Duration; - -#[cfg(target_os = "linux")] -use signal_hook::iterator::Handle; use uucore::{ error::{UResult, set_exit_code}, format::num_format::{FloatVariant, Formatter}, @@ -448,47 +445,22 @@ pub(crate) fn gen_prog_updater( } } -/// signal handler listens for SIGUSR1 signal and runs provided closure. #[cfg(target_os = "linux")] -pub(crate) struct SignalHandler { - handle: Handle, - thread: Option>, +static SIGUSR1_RECEIVED: AtomicBool = AtomicBool::new(false); + +#[cfg(target_os = "linux")] +pub(crate) fn check_and_reset_sigusr1() -> bool { + SIGUSR1_RECEIVED.swap(false, Ordering::Relaxed) } #[cfg(target_os = "linux")] -impl SignalHandler { - pub(crate) fn install_signal_handler( - f: Box, - ) -> Result { - use signal_hook::consts::signal::SIGUSR1; - use signal_hook::iterator::Signals; - - let mut signals = Signals::new([SIGUSR1])?; - let handle = signals.handle(); - let thread = std::thread::spawn(move || { - for signal in &mut signals { - match signal { - SIGUSR1 => (*f)(), - _ => unreachable!(), - } - } - }); - - Ok(Self { - handle, - thread: Some(thread), - }) - } +extern "C" fn sigusr1_handler(_: std::os::raw::c_int) { + SIGUSR1_RECEIVED.store(true, Ordering::Relaxed); } #[cfg(target_os = "linux")] -impl Drop for SignalHandler { - fn drop(&mut self) { - self.handle.close(); - if let Some(thread) = std::mem::take(&mut self.thread) { - thread.join().unwrap(); - } - } +pub(crate) fn install_sigusr1_handler() -> Result<(), nix::errno::Errno> { + uucore::signals::install_signal_handler(nix::sys::signal::Signal::SIGUSR1, sigusr1_handler) } /// Return a closure that can be used in its own thread to print progress info. diff --git a/src/uu/df/Cargo.toml b/src/uu/df/Cargo.toml index 0b0df7268..48b793145 100644 --- a/src/uu/df/Cargo.toml +++ b/src/uu/df/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/df/src/blocks.rs b/src/uu/df/src/blocks.rs index 328090ffd..24ebbf442 100644 --- a/src/uu/df/src/blocks.rs +++ b/src/uu/df/src/blocks.rs @@ -107,7 +107,7 @@ pub(crate) fn to_magnitude_and_suffix( if quot >= 100 && rem > 0 { format!("{}{suffix}", quot + 1) - } else if rem % (bases[i] / 10) == 0 { + } else if rem.is_multiple_of(bases[i] / 10) { format!("{quot}.{tenths_place}{suffix}") } else if tenths_place + 1 == 10 || quot >= 10 { let quot = quot + 1; diff --git a/src/uu/dir/Cargo.toml b/src/uu/dir/Cargo.toml index 9bbec793b..79cac6e99 100644 --- a/src/uu/dir/Cargo.toml +++ b/src/uu/dir/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/dircolors/Cargo.toml b/src/uu/dircolors/Cargo.toml index 077d4e59e..393b3f42f 100644 --- a/src/uu/dircolors/Cargo.toml +++ b/src/uu/dircolors/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/dirname/Cargo.toml b/src/uu/dirname/Cargo.toml index 85fc99a1c..76db2b955 100644 --- a/src/uu/dirname/Cargo.toml +++ b/src/uu/dirname/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/du/Cargo.toml b/src/uu/du/Cargo.toml index 99bb5a5de..06f996edc 100644 --- a/src/uu/du/Cargo.toml +++ b/src/uu/du/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index a1b3596cc..46c51440d 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -963,38 +963,61 @@ fn read_files_from(file_name: &OsStr) -> Result, std::io::Error> { Ok(paths) } -fn get_block_size_arg_index_if_present(matches: &ArgMatches, flag: &str) -> Option { - if matches.get_flag(flag) { - // Indices of returns index even if flag is not present, thats why we need to if guard it +fn get_size_format_flag_arg_index_if_present(matches: &ArgMatches, arg: &str) -> Option { + if let Some(clap::parser::ValueSource::CommandLine) = matches.value_source(arg) { matches - .indices_of(flag) + .indices_of(arg) .and_then(|mut indices| indices.next_back()) } else { None } } -fn handle_block_size_arg_override(matches: &ArgMatches) -> Option { +fn parse_block_size_arg_or_default_fallback(matches: &ArgMatches) -> UResult { + let block_size_str = matches.get_one::(options::BLOCK_SIZE); + let block_size = read_block_size(block_size_str.map(AsRef::as_ref))?; + if block_size == 0 { + return Err(std::io::Error::other(translate!("du-error-invalid-block-size-argument", "option" => options::BLOCK_SIZE, "value" => block_size_str.map_or("???BUG", |v| v).quote())) + .into()); + } + Ok(SizeFormat::BlockSize(block_size)) +} + +fn parse_size_format(matches: &ArgMatches) -> UResult { + let block_size_value_or_default_fallback = parse_block_size_arg_or_default_fallback(matches)?; let candidates = [ ( SizeFormat::BlockSize(1), - get_block_size_arg_index_if_present(matches, options::BYTES), + get_size_format_flag_arg_index_if_present(matches, options::BYTES), ), ( SizeFormat::BlockSize(1024), - get_block_size_arg_index_if_present(matches, options::BLOCK_SIZE_1K), + get_size_format_flag_arg_index_if_present(matches, options::BLOCK_SIZE_1K), ), ( SizeFormat::BlockSize(1024 * 1024), - get_block_size_arg_index_if_present(matches, options::BLOCK_SIZE_1M), + get_size_format_flag_arg_index_if_present(matches, options::BLOCK_SIZE_1M), + ), + ( + SizeFormat::HumanBinary, + get_size_format_flag_arg_index_if_present(matches, options::HUMAN_READABLE), + ), + ( + SizeFormat::HumanDecimal, + get_size_format_flag_arg_index_if_present(matches, options::SI), + ), + ( + block_size_value_or_default_fallback.clone(), + get_size_format_flag_arg_index_if_present(matches, options::BLOCK_SIZE), ), ]; - candidates + Ok(candidates .into_iter() .filter(|(_, idx)| idx.is_some()) .max_by_key(|&(_, idx)| idx.unwrap_or(0)) .map(|(size_format, _)| size_format) + .unwrap_or(block_size_value_or_default_fallback)) } #[uucore::main] @@ -1048,21 +1071,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map_or(MetadataTimeField::Modification, |s| s.as_str().into()) }); - let size_format = if matches.get_flag(options::HUMAN_READABLE) { - SizeFormat::HumanBinary - } else if matches.get_flag(options::SI) { - SizeFormat::HumanDecimal - } else if let Some(size_format) = handle_block_size_arg_override(&matches) { - size_format - } else { - let block_size_str = matches.get_one::(options::BLOCK_SIZE); - let block_size = read_block_size(block_size_str.map(AsRef::as_ref))?; - if block_size == 0 { - return Err(std::io::Error::other(translate!("du-error-invalid-block-size-argument", "option" => options::BLOCK_SIZE, "value" => block_size_str.map_or("???BUG", |v| v).quote())) - .into()); - } - SizeFormat::BlockSize(block_size) - }; + let size_format = parse_size_format(&matches)?; let traversal_options = TraversalOptions { all: matches.get_flag(options::ALL), diff --git a/src/uu/echo/Cargo.toml b/src/uu/echo/Cargo.toml index 30ce94bbe..09babded4 100644 --- a/src/uu/echo/Cargo.toml +++ b/src/uu/echo/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/env/Cargo.toml b/src/uu/env/Cargo.toml index b2e4208b9..b6b5a8cce 100644 --- a/src/uu/env/Cargo.toml +++ b/src/uu/env/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 5e56fbd04..aab1ef482 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -24,18 +24,20 @@ use nix::sys::signal::{ SigHandler::{SigDfl, SigIgn}, SigSet, SigmaskHow, Signal, signal, sigprocmask, }; +#[cfg(unix)] +use nix::unistd::execvp; use std::borrow::Cow; #[cfg(unix)] use std::collections::{BTreeMap, BTreeSet}; use std::env; +#[cfg(unix)] +use std::ffi::CString; use std::ffi::{OsStr, OsString}; use std::io; use std::io::Write as _; use std::io::stderr; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; -#[cfg(unix)] -use std::os::unix::process::CommandExt; use uucore::display::{Quotable, print_all_env_vars}; use uucore::error::{ExitCode, UError, UResult, USimpleError, UUsageError}; @@ -784,16 +786,40 @@ impl EnvAppData { #[cfg(unix)] { - // Execute the program using exec, which replaces the current process. - let err = std::process::Command::new(&*prog) - .arg0(&*arg0) - .args(args) - .exec(); + // Use execvp() directly to preserve signal handlers set by apply_signal_action(). + // Command::exec() would reset SIGPIPE, interfering with --ignore-signal=PIPE. - // exec() only returns if there was an error - match err.kind() { - io::ErrorKind::NotFound => Err(self.make_error_no_such_file_or_dir(&prog)), - io::ErrorKind::PermissionDenied => { + // Convert program name to CString. + let prog_os: &OsStr = prog.as_ref(); + let Ok(prog_cstring) = CString::new(prog_os.as_bytes()) else { + return Err(self.make_error_no_such_file_or_dir(&prog)); + }; + + // Prepare arguments for execvp. + let mut argv = Vec::new(); + + // Convert arg0 to CString. + let arg0_os: &OsStr = arg0.as_ref(); + let Ok(arg0_cstring) = CString::new(arg0_os.as_bytes()) else { + return Err(self.make_error_no_such_file_or_dir(&prog)); + }; + argv.push(arg0_cstring); + + // Convert remaining arguments to CString. + for arg in args { + let arg_os = arg; + let Ok(arg_cstring) = CString::new(arg_os.as_bytes()) else { + return Err(self.make_error_no_such_file_or_dir(&prog)); + }; + argv.push(arg_cstring); + } + + // Execute the program using execvp. this replaces the current + // process. The execvp function takes care of appending a NULL + // argument to the argument list so that we don't have to. + match execvp(&prog_cstring, &argv) { + Err(nix::errno::Errno::ENOENT) => Err(self.make_error_no_such_file_or_dir(&prog)), + Err(nix::errno::Errno::EACCES) => { uucore::show_error!( "{}", translate!( @@ -803,16 +829,19 @@ impl EnvAppData { ); Err(126.into()) } - _ => { + Err(_) => { uucore::show_error!( "{}", translate!( "env-error-unknown", - "error" => err + "error" => "execvp failed" ) ); Err(126.into()) } + Ok(_) => { + unreachable!("execvp should never return on success") + } } } @@ -1091,7 +1120,7 @@ fn list_signal_handling(log: &SignalActionLog) { SignalActionKind::Block => "BLOCK", }; let signal_name = signal_name_by_value(sig_value).unwrap_or("?"); - eprintln!("{:<10} ({}): {}", signal_name, sig_value as i32, action); + eprintln!("{signal_name:<10} ({}): {action}", sig_value as i32); } } diff --git a/src/uu/expand/Cargo.toml b/src/uu/expand/Cargo.toml index 8776557d3..694c47e10 100644 --- a/src/uu/expand/Cargo.toml +++ b/src/uu/expand/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/expr/Cargo.toml b/src/uu/expr/Cargo.toml index bdea66698..037286d52 100644 --- a/src/uu/expr/Cargo.toml +++ b/src/uu/expr/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/factor/Cargo.toml b/src/uu/factor/Cargo.toml index 15d09f7a0..988e809d0 100644 --- a/src/uu/factor/Cargo.toml +++ b/src/uu/factor/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/false/Cargo.toml b/src/uu/false/Cargo.toml index 55a46e98e..46e119b28 100644 --- a/src/uu/false/Cargo.toml +++ b/src/uu/false/Cargo.toml @@ -10,6 +10,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/false/src/false.rs b/src/uu/false/src/false.rs index d4e6b6738..b0ade6957 100644 --- a/src/uu/false/src/false.rs +++ b/src/uu/false/src/false.rs @@ -33,10 +33,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; if let Err(print_fail) = error { - // Try to display this error. let _ = writeln!(std::io::stderr(), "{}: {print_fail}", uucore::util_name()); - // Completely ignore any error here, no more failover and we will fail in any case. - set_exit_code(1); } Ok(()) diff --git a/src/uu/fmt/Cargo.toml b/src/uu/fmt/Cargo.toml index 78fa1cdea..e882966af 100644 --- a/src/uu/fmt/Cargo.toml +++ b/src/uu/fmt/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/fold/Cargo.toml b/src/uu/fold/Cargo.toml index 845ce2c96..e4e36b205 100644 --- a/src/uu/fold/Cargo.toml +++ b/src/uu/fold/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/groups/Cargo.toml b/src/uu/groups/Cargo.toml index f86b5652f..a94827160 100644 --- a/src/uu/groups/Cargo.toml +++ b/src/uu/groups/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/head/Cargo.toml b/src/uu/head/Cargo.toml index 2c0e18c1a..353404a77 100644 --- a/src/uu/head/Cargo.toml +++ b/src/uu/head/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/head/locales/en-US.ftl b/src/uu/head/locales/en-US.ftl index 9b2143206..25cabeaf2 100644 --- a/src/uu/head/locales/en-US.ftl +++ b/src/uu/head/locales/en-US.ftl @@ -7,10 +7,10 @@ head-usage = head [FLAG]... [FILE]... # Help messages head-help-bytes = print the first NUM bytes of each file; - with the leading '-', print all but the last + with a leading '-', print all but the last NUM bytes of each file head-help-lines = print the first NUM lines instead of the first 10; - with the leading '-', print all but the last + with a leading '-', print all but the last NUM lines of each file head-help-quiet = never print headers giving file names head-help-verbose = always print headers giving file names diff --git a/src/uu/hostid/Cargo.toml b/src/uu/hostid/Cargo.toml index 2a0bdc449..f4a6414f5 100644 --- a/src/uu/hostid/Cargo.toml +++ b/src/uu/hostid/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/hostname/Cargo.toml b/src/uu/hostname/Cargo.toml index 4077143bc..2a5b89529 100644 --- a/src/uu/hostname/Cargo.toml +++ b/src/uu/hostname/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/id/Cargo.toml b/src/uu/id/Cargo.toml index de1752df1..ce31a65f5 100644 --- a/src/uu/id/Cargo.toml +++ b/src/uu/id/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 880768333..5bd27bc3e 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -223,9 +223,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else { show_error!( "{}", - translate!("id-error-no-such-user", - "user" => users[i].quote() - ) + translate!("id-error-no-such-user", "user" => users[i].quote()) ); set_exit_code(1); if i + 1 >= users.len() { diff --git a/src/uu/install/Cargo.toml b/src/uu/install/Cargo.toml index 9eb7679a4..41d1af3d1 100644 --- a/src/uu/install/Cargo.toml +++ b/src/uu/install/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index a3cd77931..84eefdcc8 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -813,6 +813,7 @@ fn perform_backup(to: &Path, b: &Behavior) -> UResult> { /// Returns an empty Result or an error in case of failure. /// fn copy_file(from: &Path, to: &Path) -> UResult<()> { + use std::os::unix::fs::OpenOptionsExt; if let Ok(to_abs) = to.canonicalize() { if from.canonicalize()? == to_abs { return Err(InstallError::SameFile(from.to_path_buf(), to.to_path_buf()).into()); @@ -841,7 +842,11 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> { let mut handle = File::open(from)?; // create_new provides TOCTOU protection - let mut dest = OpenOptions::new().write(true).create_new(true).open(to)?; + let mut dest = OpenOptions::new() + .write(true) + .create_new(true) + .mode(0o600) + .open(to)?; copy_stream(&mut handle, &mut dest).map_err(|err| { InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err.to_string()) diff --git a/src/uu/join/Cargo.toml b/src/uu/join/Cargo.toml index 8599c5c6a..0f5cd8a2c 100644 --- a/src/uu/join/Cargo.toml +++ b/src/uu/join/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/join/benches/join_bench.rs b/src/uu/join/benches/join_bench.rs index 800bfa96d..4a18fb51e 100644 --- a/src/uu/join/benches/join_bench.rs +++ b/src/uu/join/benches/join_bench.rs @@ -50,7 +50,7 @@ fn create_partial_overlap_files( // File 2: keys (num_lines - overlap_count) to (2*num_lines - overlap_count - 1) let start = num_lines - overlap_count; for i in 0..num_lines { - writeln!(file2, "{:08} f2_data_{}", start + i, i).unwrap(); + writeln!(file2, "{:08} f2_data_{i}", start + i).unwrap(); } ( diff --git a/src/uu/kill/Cargo.toml b/src/uu/kill/Cargo.toml index 10de8379d..0b20e594f 100644 --- a/src/uu/kill/Cargo.toml +++ b/src/uu/kill/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/link/Cargo.toml b/src/uu/link/Cargo.toml index 6c23e965b..c1da6b5e0 100644 --- a/src/uu/link/Cargo.toml +++ b/src/uu/link/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/ln/Cargo.toml b/src/uu/ln/Cargo.toml index 6cae47f78..6527ee89a 100644 --- a/src/uu/ln/Cargo.toml +++ b/src/uu/ln/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/logname/Cargo.toml b/src/uu/logname/Cargo.toml index 67e0b5be9..8d1e17962 100644 --- a/src/uu/logname/Cargo.toml +++ b/src/uu/logname/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index 0b53e8749..a82ad13c3 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -11,6 +11,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 0ec9beb40..7365cb4dc 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -3406,13 +3406,30 @@ fn display_item_name( config, false, ); - name.push(color_name( - escaped_target, - &target_data, - style_manager, - None, - is_wrap(name.len()), - )); + + // Check if the target actually needs coloring + let md_option: Option = target_data + .metadata() + .cloned() + .or_else(|| target_data.p_buf.symlink_metadata().ok()); + let style = style_manager.colors.style_for_path_with_metadata( + &target_data.p_buf, + md_option.as_ref(), + ); + + if style.is_some() { + // Only apply coloring if there's actually a style + name.push(color_name( + escaped_target, + &target_data, + style_manager, + None, + is_wrap(name.len()), + )); + } else { + // For regular files with no coloring, just use plain text + name.push(escaped_target); + } } Err(_) => { name.push( @@ -3463,29 +3480,34 @@ fn create_hyperlink(name: &OsStr, path: &PathData) -> OsString { let hostname = hostname.to_string_lossy(); let absolute_path = fs::canonicalize(path.path()).unwrap_or_default(); - let absolute_path = absolute_path.to_string_lossy(); + // Get bytes for URL encoding in a cross-platform way + let absolute_path_bytes = os_str_as_bytes_lossy(absolute_path.as_os_str()); + + // Create a set of safe ASCII bytes that don't need encoding #[cfg(not(target_os = "windows"))] - let unencoded_chars = "_-.:~/"; + let unencoded_bytes: std::collections::HashSet = "_-.~/".bytes().collect(); #[cfg(target_os = "windows")] - let unencoded_chars = "_-.:~/\\"; + let unencoded_bytes: std::collections::HashSet = "_-.~/\\:".bytes().collect(); - // percentage encoding of path - let absolute_path: String = absolute_path - .chars() - .map(|c| { - if c.is_alphanumeric() || unencoded_chars.contains(c) { - c.to_string() + // Encode at byte level to properly handle UTF-8 sequences and preserve invalid UTF-8 + let full_encoded_path: String = absolute_path_bytes + .iter() + .map(|&b: &u8| { + if b.is_ascii_alphanumeric() || unencoded_bytes.contains(&b) { + (b as char).to_string() } else { - format!("%{:02x}", c as u8) + format!("%{b:02x}") } }) .collect(); - // \x1b = ESC, \x07 = BEL - let mut ret: OsString = format!("\x1b]8;;file://{hostname}{absolute_path}\x07").into(); + // OSC 8 hyperlink format: \x1b]8;;URL\x1b\\TEXT\x1b]8;;\x1b\\ + // \x1b = ESC, \x1b\\ = ESC backslash + let mut ret: OsString = format!("\x1b]8;;file://{hostname}{full_encoded_path}\x1b\\").into(); ret.push(name); - ret.push("\x1b]8;;\x07"); + ret.push("\x1b]8;;\x1b\\"); + ret } diff --git a/src/uu/md5sum/Cargo.toml b/src/uu/md5sum/Cargo.toml index 23ee431af..de269a099 100644 --- a/src/uu/md5sum/Cargo.toml +++ b/src/uu/md5sum/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/mkdir/Cargo.toml b/src/uu/mkdir/Cargo.toml index b2723e1cf..6e2e082d0 100644 --- a/src/uu/mkdir/Cargo.toml +++ b/src/uu/mkdir/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/mkfifo/Cargo.toml b/src/uu/mkfifo/Cargo.toml index ece483810..594b91ae7 100644 --- a/src/uu/mkfifo/Cargo.toml +++ b/src/uu/mkfifo/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/mknod/Cargo.toml b/src/uu/mknod/Cargo.toml index a32aa3e9a..2ea3daddc 100644 --- a/src/uu/mknod/Cargo.toml +++ b/src/uu/mknod/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index 9ab0af057..50aa70755 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -95,7 +95,7 @@ fn mknod(file_name: &str, config: Config) -> i32 { ) { // if it fails, delete the file let _ = std::fs::remove_file(file_name); - eprintln!("{}: {}", uucore::util_name(), e); + eprintln!("{}: {e}", uucore::util_name()); return 1; } } @@ -108,7 +108,7 @@ fn mknod(file_name: &str, config: Config) -> i32 { std::fs::remove_file(p) }) { - eprintln!("{}: {}", uucore::util_name(), e); + eprintln!("{}: {e}", uucore::util_name()); return 1; } } diff --git a/src/uu/mktemp/Cargo.toml b/src/uu/mktemp/Cargo.toml index 651db9e21..5c42d14b6 100644 --- a/src/uu/mktemp/Cargo.toml +++ b/src/uu/mktemp/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index 3efd06901..d22f7500a 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -101,7 +101,7 @@ pub struct Options { pub tmpdir: Option, /// The suffix to append to the temporary file, if any. - pub suffix: Option, + pub suffix: Option, /// Whether to treat the template argument as a single file path component. pub treat_as_template: bool, @@ -150,7 +150,7 @@ impl Options { dry_run: matches.get_flag(OPT_DRY_RUN), quiet: matches.get_flag(OPT_QUIET), tmpdir, - suffix: matches.get_one::(OPT_SUFFIX).map(String::from), + suffix: matches.get_one::(OPT_SUFFIX).cloned(), treat_as_template: matches.get_flag(OPT_T), template, } @@ -214,27 +214,39 @@ fn find_last_contiguous_block_of_xs(s: &str) -> Option<(usize, usize)> { impl Params { fn from(options: Options) -> Result { // Convert OsString template to string for processing - let Some(template_str) = options.template.to_str() else { - // For non-UTF-8 templates, return an error - return Err(MkTempError::InvalidTemplate(options.template)); + // When using -t flag, be permissive with invalid UTF-8 like GNU mktemp + // Otherwise, maintain strict UTF-8 validation (existing behavior) + let template_str = if options.treat_as_template { + // For -t templates, use lossy conversion for GNU compatibility + options.template.to_string_lossy().into_owned() + } else { + // For regular templates, maintain strict validation + match options.template.to_str() { + Some(s) => s.to_string(), + None => { + return Err(MkTempError::InvalidTemplate( + "template contains invalid UTF-8".into(), + )); + } + } }; // The template argument must end in 'X' if a suffix option is given. if options.suffix.is_some() && !template_str.ends_with('X') { - return Err(MkTempError::MustEndInX(template_str.to_string())); + return Err(MkTempError::MustEndInX(template_str.clone())); } // Get the start and end indices of the randomized part of the template. // // For example, if the template is "abcXXXXyz", then `i` is 3 and `j` is 7. - let Some((i, j)) = find_last_contiguous_block_of_xs(template_str) else { + let Some((i, j)) = find_last_contiguous_block_of_xs(&template_str) else { let s = match options.suffix { // If a suffix is specified, the error message includes the template without the suffix. Some(_) => template_str .chars() .take(template_str.len()) .collect::(), - None => template_str.to_string(), + None => template_str.clone(), }; return Err(MkTempError::TooFewXs(s)); }; @@ -249,11 +261,11 @@ impl Params { let prefix_path = Path::new(&prefix_from_option).join(prefix_from_template); if options.treat_as_template && prefix_from_template.contains(MAIN_SEPARATOR) { return Err(MkTempError::PrefixContainsDirSeparator( - template_str.to_string(), + template_str.clone(), )); } if tmpdir.is_some() && Path::new(prefix_from_template).is_absolute() { - return Err(MkTempError::InvalidTemplate(template_str.into())); + return Err(MkTempError::InvalidTemplate(template_str.clone().into())); } // Split the parent directory from the file part of the prefix. @@ -271,7 +283,7 @@ impl Params { }; let prefix = match prefix_path.file_name() { None => String::new(), - Some(f) => f.to_str().unwrap().to_string(), + Some(f) => f.to_string_lossy().to_string(), }; (directory, prefix) } @@ -281,7 +293,10 @@ impl Params { // // For example, if the suffix command-line argument is ".txt" and // the template is "XXXabc", then `suffix` is "abc.txt". - let suffix_from_option = options.suffix.unwrap_or_default(); + let suffix_from_option = options + .suffix + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default(); let suffix_from_template = &template_str[j..]; let suffix = format!("{suffix_from_template}{suffix_from_option}"); if suffix.contains(MAIN_SEPARATOR) { @@ -445,7 +460,8 @@ pub fn uu_app() -> Command { Arg::new(OPT_SUFFIX) .long(OPT_SUFFIX) .help(translate!("mktemp-help-suffix")) - .value_name("SUFFIX"), + .value_name("SUFFIX") + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(OPT_P) diff --git a/src/uu/more/Cargo.toml b/src/uu/more/Cargo.toml index bee3ff755..5057cee8a 100644 --- a/src/uu/more/Cargo.toml +++ b/src/uu/more/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/mv/Cargo.toml b/src/uu/mv/Cargo.toml index 0ed038fe6..5cba2d472 100644 --- a/src/uu/mv/Cargo.toml +++ b/src/uu/mv/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/mv/src/hardlink.rs b/src/uu/mv/src/hardlink.rs index 8c4ea7b33..61ad74d47 100644 --- a/src/uu/mv/src/hardlink.rs +++ b/src/uu/mv/src/hardlink.rs @@ -65,7 +65,7 @@ impl std::fmt::Display for HardlinkError { ) } Self::Metadata { path, error } => { - write!(f, "Metadata access error for {}: {}", path.quote(), error) + write!(f, "Metadata access error for {}: {error}", path.quote()) } } } @@ -99,9 +99,8 @@ impl From for io::Error { )), HardlinkError::Metadata { path, error } => Self::other(format!( - "Metadata access error for {}: {}", + "Metadata access error for {}: {error}", path.quote(), - error )), } } @@ -127,7 +126,7 @@ impl HardlinkTracker { Err(e) => { // Gracefully handle metadata errors by logging and continuing without hardlink tracking if options.verbose { - eprintln!("warning: cannot get metadata for {}: {}", source.quote(), e); + eprintln!("warning: cannot get metadata for {}: {e}", source.quote()); } return None; } @@ -180,7 +179,7 @@ impl HardlinkGroupScanner { if let Err(e) = self.scan_single_path(file) { if options.verbose { // Only show warnings for verbose mode - eprintln!("warning: failed to scan {}: {}", file.quote(), e); + eprintln!("warning: failed to scan {}: {e}", file.quote()); } // For non-verbose mode, silently continue for missing files // This provides graceful degradation - we'll lose hardlink info for this file diff --git a/src/uu/nice/Cargo.toml b/src/uu/nice/Cargo.toml index 00f96718e..58dfb69f6 100644 --- a/src/uu/nice/Cargo.toml +++ b/src/uu/nice/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/nl/Cargo.toml b/src/uu/nl/Cargo.toml index 82d06cc7f..5818dfa91 100644 --- a/src/uu/nl/Cargo.toml +++ b/src/uu/nl/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs index 5288aaefb..616f8d410 100644 --- a/src/uu/nl/src/nl.rs +++ b/src/uu/nl/src/nl.rs @@ -172,7 +172,7 @@ impl SectionDelimiter { fn parse(bytes: &[u8], pattern: &OsStr) -> Option { let pattern = pattern.as_encoded_bytes(); - if bytes.is_empty() || pattern.is_empty() || bytes.len() % pattern.len() != 0 { + if bytes.is_empty() || pattern.is_empty() || !bytes.len().is_multiple_of(pattern.len()) { return None; } @@ -426,7 +426,9 @@ fn nl(reader: &mut BufReader, stats: &mut Stats, settings: &Settings NumberingStyle::All if line.is_empty() && settings.join_blank_lines > 0 - && stats.consecutive_empty_lines % settings.join_blank_lines != 0 => + && !stats + .consecutive_empty_lines + .is_multiple_of(settings.join_blank_lines) => { false } diff --git a/src/uu/nohup/Cargo.toml b/src/uu/nohup/Cargo.toml index 50eb258a9..4b9d0213b 100644 --- a/src/uu/nohup/Cargo.toml +++ b/src/uu/nohup/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/nproc/Cargo.toml b/src/uu/nproc/Cargo.toml index 641fc413e..496a8409f 100644 --- a/src/uu/nproc/Cargo.toml +++ b/src/uu/nproc/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/numfmt/Cargo.toml b/src/uu/numfmt/Cargo.toml index fed39ad68..765d4dcce 100644 --- a/src/uu/numfmt/Cargo.toml +++ b/src/uu/numfmt/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/od/Cargo.toml b/src/uu/od/Cargo.toml index 13d12a413..31cdc8c78 100644 --- a/src/uu/od/Cargo.toml +++ b/src/uu/od/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/od/locales/en-US.ftl b/src/uu/od/locales/en-US.ftl index bcafe1fe2..2c0d8baed 100644 --- a/src/uu/od/locales/en-US.ftl +++ b/src/uu/od/locales/en-US.ftl @@ -90,5 +90,5 @@ od-help-s = decimal 2-byte units od-help-capital-x = hexadecimal 4-byte units od-help-capital-h = hexadecimal 4-byte units od-help-e = floating point double precision (64-bit) units -od-help-f = floating point double precision (32-bit) units +od-help-f = floating point single precision (32-bit) units od-help-capital-f = floating point double precision (64-bit) units diff --git a/src/uu/od/locales/fr-FR.ftl b/src/uu/od/locales/fr-FR.ftl index df07eebe6..ada0c2d79 100644 --- a/src/uu/od/locales/fr-FR.ftl +++ b/src/uu/od/locales/fr-FR.ftl @@ -90,5 +90,5 @@ od-help-s = unités décimales 2-octets od-help-capital-x = unités hexadécimales 4-octets od-help-capital-h = unités hexadécimales 4-octets od-help-e = unités virgule flottante double précision (64-bits) -od-help-f = unités virgule flottante double précision (32-bits) +od-help-f = unités virgule flottante simple précision (32-bits) od-help-capital-f = unités virgule flottante double précision (64-bits) diff --git a/src/uu/od/src/multifile_reader.rs b/src/uu/od/src/multifile_reader.rs index 48e1f1225..4213fde90 100644 --- a/src/uu/od/src/multifile_reader.rs +++ b/src/uu/od/src/multifile_reader.rs @@ -93,7 +93,7 @@ impl MultifileReader<'_> { io::ErrorKind::PermissionDenied => "Permission denied", _ => "I/O error", }; - show_error!("{}: {}", fname.maybe_quote().external(true), error_msg); + show_error!("{}: {error_msg}", fname.maybe_quote().external(true)); self.any_err = true; } } diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 4da705aae..67992d769 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -658,7 +658,7 @@ fn extract_strings_from_input( // Note: GNU od does not output unterminated strings at EOF // Strings must be null-terminated to be output if mf.has_error() { - show_error!("{}", e); + show_error!("{e}"); return Err(1.into()); } break; diff --git a/src/uu/od/src/output_info.rs b/src/uu/od/src/output_info.rs index ef63c1602..86c481b0d 100644 --- a/src/uu/od/src/output_info.rs +++ b/src/uu/od/src/output_info.rs @@ -222,10 +222,9 @@ fn assert_alignment( assert_eq!( expected, &spacing[..byte_size_block], - "unexpected spacing for byte_size={} print_width={} block_width={}", + "unexpected spacing for byte_size={} print_width={} block_width={print_width_block}", type_info.byte_size, type_info.print_width, - print_width_block ); assert!( spacing[byte_size_block..].iter().all(|&s| s == 0), diff --git a/src/uu/od/src/parse_inputs.rs b/src/uu/od/src/parse_inputs.rs index b6f763b2e..3d6263e56 100644 --- a/src/uu/od/src/parse_inputs.rs +++ b/src/uu/od/src/parse_inputs.rs @@ -91,7 +91,7 @@ pub fn parse_inputs(matches: &dyn CommandLineOpts) -> Result Result Err(format!("{}: {}", input_strings[1], e)), + (_, Err(e)) => Err(format!("{}: {e}", input_strings[1])), } } 3 => { @@ -148,8 +148,8 @@ pub fn parse_inputs_traditional(input_strings: &[&str]) -> Result Err(format!("{}: {}", input_strings[1], e)), - (_, Err(e)) => Err(format!("{}: {}", input_strings[2], e)), + (Err(e), _) => Err(format!("{}: {e}", input_strings[1])), + (_, Err(e)) => Err(format!("{}: {e}", input_strings[2])), } } _ => Err(translate!("od-error-too-many-inputs", "input" => input_strings[3])), diff --git a/src/uu/paste/Cargo.toml b/src/uu/paste/Cargo.toml index ca0efa67c..eee948150 100644 --- a/src/uu/paste/Cargo.toml +++ b/src/uu/paste/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] @@ -19,7 +20,7 @@ path = "src/paste.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true } +uucore = { workspace = true, features = ["i18n-charmap"] } fluent = { workspace = true } [[bin]] diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 5d6ae5ed9..b0f51e8ce 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -14,6 +14,7 @@ use std::rc::Rc; use std::slice::Iter; use uucore::error::{UResult, USimpleError}; use uucore::format_usage; +use uucore::i18n::charmap::mb_char_len; use uucore::line_ending::LineEnding; use uucore::translate; @@ -29,7 +30,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; let serial = matches.get_flag(options::SERIAL); - let delimiters = matches.get_one::(options::DELIMITER).unwrap(); + let delimiters = matches.get_one::(options::DELIMITER).unwrap(); let files = matches .get_many::(options::FILE) .unwrap() @@ -61,7 +62,8 @@ pub fn uu_app() -> Command { .help(translate!("paste-help-delimiter")) .value_name("LIST") .default_value("\t") - .hide_default_value(true), + .hide_default_value(true) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::FILE) @@ -84,7 +86,7 @@ pub fn uu_app() -> Command { fn paste( filenames: Vec, serial: bool, - delimiters: &str, + delimiters: &OsString, line_ending: LineEnding, ) -> UResult<()> { let unescaped_and_encoded_delimiters = parse_delimiters(delimiters)?; @@ -185,65 +187,44 @@ fn paste( Ok(()) } -fn parse_delimiters(delimiters: &str) -> UResult]>> { - /// A single backslash char - const BACKSLASH: char = '\\'; +fn parse_delimiters(delimiters: &OsString) -> UResult]>> { + let bytes = uucore::os_str_as_bytes(delimiters)?; + let mut vec = Vec::>::with_capacity(bytes.len()); + let mut i = 0; - fn add_one_byte_single_char_delimiter(vec: &mut Vec>, byte: u8) { - vec.push(Box::new([byte])); - } - - // a buffer of length four is large enough to encode any char - let mut buffer = [0; 4]; - - let mut add_single_char_delimiter = |vec: &mut Vec>, ch: char| { - let delimiter_encoded = ch.encode_utf8(&mut buffer); - - vec.push(Box::<[u8]>::from(delimiter_encoded.as_bytes())); - }; - - let mut vec = Vec::>::with_capacity(delimiters.len()); - - let mut chars = delimiters.chars(); - - // Unescape all special characters - while let Some(char) = chars.next() { - match char { - BACKSLASH => match chars.next() { - // "Empty string (not a null character)" - // https://pubs.opengroup.org/onlinepubs/9799919799/utilities/paste.html - Some('0') => { - vec.push(Box::<[u8; 0]>::new([])); - } - // "\\" to "\" (U+005C) - Some(BACKSLASH) => { - add_one_byte_single_char_delimiter(&mut vec, b'\\'); - } - // "\n" to U+000A - Some('n') => { - add_one_byte_single_char_delimiter(&mut vec, b'\n'); - } - // "\t" to U+0009 - Some('t') => { - add_one_byte_single_char_delimiter(&mut vec, b'\t'); - } - Some(other_char) => { - // "If any other characters follow the , the results are unspecified." - // https://pubs.opengroup.org/onlinepubs/9799919799/utilities/paste.html - // However, other implementations remove the backslash - // See "test_posix_unspecified_delimiter" - add_single_char_delimiter(&mut vec, other_char); - } - None => { - return Err(USimpleError::new( - 1, - translate!("paste-error-delimiter-unescaped-backslash", "delimiters" => delimiters), - )); - } - }, - non_backslash_char => { - add_single_char_delimiter(&mut vec, non_backslash_char); + while i < bytes.len() { + if bytes[i] == b'\\' { + i += 1; + if i >= bytes.len() { + return Err(USimpleError::new( + 1, + translate!("paste-error-delimiter-unescaped-backslash", "delimiters" => delimiters.to_string_lossy()), + )); } + match bytes[i] { + b'0' => vec.push(Box::new([])), + b'\\' => vec.push(Box::new([b'\\'])), + b'n' => vec.push(Box::new([b'\n'])), + b't' => vec.push(Box::new([b'\t'])), + b'b' => vec.push(Box::new([b'\x08'])), + b'f' => vec.push(Box::new([b'\x0C'])), + b'r' => vec.push(Box::new([b'\r'])), + b'v' => vec.push(Box::new([b'\x0B'])), + _ => { + // Unknown escape: strip backslash, use the following character(s) + let remaining = &bytes[i..]; + let len = mb_char_len(remaining).min(remaining.len()); + vec.push(Box::from(&bytes[i..i + len])); + i += len; + continue; + } + } + i += 1; + } else { + let remaining = &bytes[i..]; + let len = mb_char_len(remaining).min(remaining.len()); + vec.push(Box::from(&bytes[i..i + len])); + i += len; } } diff --git a/src/uu/pathchk/Cargo.toml b/src/uu/pathchk/Cargo.toml index b75bfab51..dcf6ced60 100644 --- a/src/uu/pathchk/Cargo.toml +++ b/src/uu/pathchk/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/pinky/Cargo.toml b/src/uu/pinky/Cargo.toml index acb0eb207..5fc84f3bd 100644 --- a/src/uu/pinky/Cargo.toml +++ b/src/uu/pinky/Cargo.toml @@ -11,6 +11,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/pr/Cargo.toml b/src/uu/pr/Cargo.toml index 4eb7539dd..e3953c62b 100644 --- a/src/uu/pr/Cargo.toml +++ b/src/uu/pr/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index 25a1e2e3e..f000abbd7 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -1264,11 +1264,8 @@ fn header_content(options: &OutputOptions, page: usize) -> Vec { let padding_after_filename = space_for_filename - filename_len - padding_before_filename; format!( - "{date_part}{:width1$}{filename}{:width2$}{page_part}", - "", - "", - width1 = padding_before_filename, - width2 = padding_after_filename + "{date_part}{:padding_before_filename$}{filename}{:padding_after_filename$}{page_part}", + "", "" ) } else { // If content is too long, just use single spaces diff --git a/src/uu/printenv/Cargo.toml b/src/uu/printenv/Cargo.toml index 09f073ffc..b73975636 100644 --- a/src/uu/printenv/Cargo.toml +++ b/src/uu/printenv/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/printf/Cargo.toml b/src/uu/printf/Cargo.toml index 05d033fc6..dfeacb30e 100644 --- a/src/uu/printf/Cargo.toml +++ b/src/uu/printf/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/ptx/Cargo.toml b/src/uu/ptx/Cargo.toml index 44dbf9a5e..0e2d7ac8e 100644 --- a/src/uu/ptx/Cargo.toml +++ b/src/uu/ptx/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/pwd/Cargo.toml b/src/uu/pwd/Cargo.toml index 1a1fabcfa..5d34c0c23 100644 --- a/src/uu/pwd/Cargo.toml +++ b/src/uu/pwd/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/readlink/Cargo.toml b/src/uu/readlink/Cargo.toml index 31d3d851c..a4b92b6f0 100644 --- a/src/uu/readlink/Cargo.toml +++ b/src/uu/readlink/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/realpath/Cargo.toml b/src/uu/realpath/Cargo.toml index d0a37a8b4..af675def1 100644 --- a/src/uu/realpath/Cargo.toml +++ b/src/uu/realpath/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/rm/Cargo.toml b/src/uu/rm/Cargo.toml index cf53e0323..e04b0e52d 100644 --- a/src/uu/rm/Cargo.toml +++ b/src/uu/rm/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/rmdir/Cargo.toml b/src/uu/rmdir/Cargo.toml index 6001af169..2841e3412 100644 --- a/src/uu/rmdir/Cargo.toml +++ b/src/uu/rmdir/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/runcon/Cargo.toml b/src/uu/runcon/Cargo.toml index fdb2f5174..6ed1c2e3d 100644 --- a/src/uu/runcon/Cargo.toml +++ b/src/uu/runcon/Cargo.toml @@ -6,6 +6,7 @@ keywords = ["coreutils", "uutils", "cli", "utility"] authors.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true homepage.workspace = true license.workspace = true version.workspace = true diff --git a/src/uu/seq/Cargo.toml b/src/uu/seq/Cargo.toml index cdc1c29af..87b6a52e1 100644 --- a/src/uu/seq/Cargo.toml +++ b/src/uu/seq/Cargo.toml @@ -10,6 +10,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/sha1sum/Cargo.toml b/src/uu/sha1sum/Cargo.toml index 7cb6aceab..0704d6ba6 100644 --- a/src/uu/sha1sum/Cargo.toml +++ b/src/uu/sha1sum/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/sha224sum/Cargo.toml b/src/uu/sha224sum/Cargo.toml index 3436c9d2b..6120aad80 100644 --- a/src/uu/sha224sum/Cargo.toml +++ b/src/uu/sha224sum/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/sha256sum/Cargo.toml b/src/uu/sha256sum/Cargo.toml index 43a5c8577..6a93bf916 100644 --- a/src/uu/sha256sum/Cargo.toml +++ b/src/uu/sha256sum/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/sha384sum/Cargo.toml b/src/uu/sha384sum/Cargo.toml index ff9d57871..00e9f5472 100644 --- a/src/uu/sha384sum/Cargo.toml +++ b/src/uu/sha384sum/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/sha512sum/Cargo.toml b/src/uu/sha512sum/Cargo.toml index 281544adb..cf204df69 100644 --- a/src/uu/sha512sum/Cargo.toml +++ b/src/uu/sha512sum/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/shred/Cargo.toml b/src/uu/shred/Cargo.toml index 59f0fb6c2..6ebecb01a 100644 --- a/src/uu/shred/Cargo.toml +++ b/src/uu/shred/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/shuf/Cargo.toml b/src/uu/shuf/Cargo.toml index bfe41d46e..d59f022e1 100644 --- a/src/uu/shuf/Cargo.toml +++ b/src/uu/shuf/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index f0caf445d..c7e5689ab 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -378,7 +378,6 @@ impl Writable for &OsStr { } impl Writable for u64 { - #[inline] fn write_all_to(&self, output: &mut impl OsWrite) -> Result<(), io::Error> { // The itoa crate is surprisingly much more efficient than a formatted write. // It speeds up `shuf -r -n1000000 -i1-1024` by 1.8×. @@ -395,7 +394,6 @@ fn handle_write_error(e: io::Error) -> Box { e.map_err_context(move || ctx) } -#[inline(never)] fn shuf_exec( input: &mut impl Shufable, opts: &Options, diff --git a/src/uu/sleep/Cargo.toml b/src/uu/sleep/Cargo.toml index bf97f856f..ccd878bd4 100644 --- a/src/uu/sleep/Cargo.toml +++ b/src/uu/sleep/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index e487a1bfe..92bca5884 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -1,4 +1,4 @@ -# spell-checker:ignore bigdecimal +# spell-checker:ignore ahash bigdecimal [package] name = "uu_sort" @@ -11,6 +11,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] @@ -28,7 +29,6 @@ bigdecimal = { workspace = true } binary-heap-plus = { workspace = true } clap = { workspace = true } compare = { workspace = true } -fnv = { workspace = true } itertools = { workspace = true } memchr = { workspace = true } rand = { workspace = true } @@ -44,6 +44,7 @@ uucore = { workspace = true, features = [ "i18n-collator", ] } fluent = { workspace = true } +ahash = { workspace = true } [target.'cfg(not(target_os = "redox"))'.dependencies] ctrlc = { workspace = true } diff --git a/src/uu/sort/src/ext_sort.rs b/src/uu/sort/src/ext_sort.rs index d61f7d200..6e4e98a58 100644 --- a/src/uu/sort/src/ext_sort.rs +++ b/src/uu/sort/src/ext_sort.rs @@ -70,8 +70,7 @@ pub fn ext_sort( Err(err) => { // Print the error and disable compression eprintln!( - "sort: could not run compress program '{}': {}", - prog, + "sort: could not run compress program '{prog}': {}", strip_errno(&err) ); effective_settings.compress_prog = None; diff --git a/src/uu/sort/src/merge.rs b/src/uu/sort/src/merge.rs index 39465827e..d2bb3056b 100644 --- a/src/uu/sort/src/merge.rs +++ b/src/uu/sort/src/merge.rs @@ -548,7 +548,9 @@ impl ClosedTmpFile for ClosedCompressedTmpFile { fn reopen(self) -> UResult { let mut command = Command::new(&self.compress_prog); - let file = File::open(&self.path).unwrap(); + // mirroring what is done for ClosedPlainTmpFile + let file = + File::open(&self.path).map_err(|error| SortError::OpenTmpFileFailed { error })?; command.stdin(file).stdout(Stdio::piped()).arg("-d"); let mut child = command .spawn() diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index ceaf51256..52247c4fe 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -18,14 +18,13 @@ mod merge; mod numeric_str_cmp; mod tmp_dir; +use ahash::AHashMap; use bigdecimal::BigDecimal; use chunks::LineData; use clap::builder::ValueParser; use clap::{Arg, ArgAction, ArgMatches, Command}; use custom_str_cmp::custom_str_cmp; - use ext_sort::ext_sort; -use fnv::FnvHasher; use numeric_str_cmp::{NumInfo, NumInfoParseSettings, human_numeric_str_cmp, numeric_str_cmp}; use rand::{Rng, rng}; use rayon::prelude::*; @@ -33,7 +32,7 @@ use std::cmp::Ordering; use std::env; use std::ffi::{OsStr, OsString}; use std::fs::{File, OpenOptions}; -use std::hash::{Hash, Hasher}; +use std::hash::{BuildHasher, Hash, Hasher}; use std::io::{BufRead, BufReader, BufWriter, Read, Write, stdin, stdout}; use std::num::{IntErrorKind, NonZero}; use std::ops::Range; @@ -1467,7 +1466,7 @@ struct LegacyKeyWarning { impl LegacyKeyWarning { fn legacy_key_display(&self) -> String { match self.to_field { - Some(to) => format!("+{} -{}", self.from_field, to), + Some(to) => format!("+{} -{to}", self.from_field), None => format!("+{}", self.from_field), } } @@ -1569,14 +1568,13 @@ fn legacy_key_to_k(from: &LegacyKeyPart, to: Option<&LegacyKeyPart>) -> String { let start_char = from.char_pos.saturating_add(1); let mut keydef = format!( - "{}{}{}", - start_field, + "{start_field}{}{}", if from.char_pos == 0 { String::new() } else { format!(".{start_char}") }, - from.opts + from.opts, ); if let Some(to) = to { @@ -1682,7 +1680,7 @@ fn index_legacy_warnings(processed_args: &[OsString], legacy_warnings: &mut [Leg return; } - let mut index_by_arg = std::collections::HashMap::new(); + let mut index_by_arg = AHashMap::default(); for (warning_idx, warning) in legacy_warnings.iter().enumerate() { index_by_arg.insert(warning.arg_index, warning_idx); } @@ -2150,7 +2148,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { #[cfg(target_os = "linux")] { - show_error!("{}", batch_too_large); + show_error!("{batch_too_large}"); translate!( "sort-maximum-batch-size-rlimit", @@ -2910,7 +2908,8 @@ fn salt_from_random_source(path: &Path) -> UResult<[u8; SALT_LEN]> { let mut reader = open_with_open_failed_error(path)?; let mut buf = [0u8; BUF_LEN]; let mut total = 0usize; - let mut hasher = FnvHasher::default(); + // freeze seed for --random-source + let mut hasher = ahash::RandomState::with_seeds(1, 1, 1, 1).build_hasher(); loop { let n = reader @@ -2935,7 +2934,8 @@ fn salt_from_random_source(path: &Path) -> UResult<[u8; SALT_LEN]> { } let first = hasher.finish(); - let mut second_hasher = FnvHasher::default(); + // freeze seed for --random-source + let mut second_hasher = ahash::RandomState::with_seeds(2, 2, 2, 2).build_hasher(); second_hasher.write(RANDOM_SOURCE_TAG); second_hasher.write_u64(first); let second = second_hasher.finish(); @@ -2947,9 +2947,8 @@ fn salt_from_random_source(path: &Path) -> UResult<[u8; SALT_LEN]> { } fn get_hash(t: &T) -> u64 { - let mut s = FnvHasher::default(); - t.hash(&mut s); - s.finish() + // Is reproducibility of get_hash itself needed for --random-source ? + ahash::RandomState::with_seeds(0, 0, 0, 0).hash_one(t) } fn random_shuffle(a: &[u8], b: &[u8], salt: &[u8]) -> Ordering { @@ -3087,13 +3086,6 @@ mod tests { buffer } - #[test] - fn test_get_hash() { - let a = "Ted".to_string(); - - assert_eq!(2_646_829_031_758_483_623, get_hash(&a)); - } - #[test] fn test_random_shuffle() { let a = b"Ted"; diff --git a/src/uu/split/Cargo.toml b/src/uu/split/Cargo.toml index 2c51bb780..d9878b47b 100644 --- a/src/uu/split/Cargo.toml +++ b/src/uu/split/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/split/src/number.rs b/src/uu/split/src/number.rs index fd0256a15..58c08d12b 100644 --- a/src/uu/split/src/number.rs +++ b/src/uu/split/src/number.rs @@ -15,7 +15,7 @@ //! [radix]: https://en.wikipedia.org/wiki/Radix //! [positional notation]: https://en.wikipedia.org/wiki/Positional_notation use std::error::Error; -use std::fmt::{self, Display, Formatter}; +use std::fmt::{self, Display, Formatter, Write}; use uucore::translate; /// An overflow due to incrementing a number beyond its representable limit. @@ -244,12 +244,10 @@ impl FixedWidthNumber { impl Display for FixedWidthNumber { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let digits: String = self - .digits - .iter() - .map(|d| map_digit(self.radix, *d)) - .collect(); - write!(f, "{digits}") + for d in &self.digits { + f.write_char(map_digit(self.radix, *d))?; + } + Ok(()) } } diff --git a/src/uu/stat/Cargo.toml b/src/uu/stat/Cargo.toml index 21adecd90..d01a713ec 100644 --- a/src/uu/stat/Cargo.toml +++ b/src/uu/stat/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index 8e044ed77..65b6e0540 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -1303,13 +1303,12 @@ impl Stater { }; format!( - " {}: %N\n {}: %-10s\t{}: %-10b {} {}: %-6o %F\n{}{}: (%04a/%10.10A) {}: (%5u/%8U) {}: (%5g/%8G)\n{}: %x\n{}: %y\n{}: %z\n {}: %w\n", + " {}: %N\n {}: %-10s\t{}: %-10b {} {}: %-6o %F\n{device_line}{}: (%04a/%10.10A) {}: (%5u/%8U) {}: (%5g/%8G)\n{}: %x\n{}: %y\n{}: %z\n {}: %w\n", translate!("stat-word-file"), translate!("stat-word-size"), translate!("stat-word-blocks"), translate!("stat-word-io"), translate!("stat-word-block"), - device_line, translate!("stat-word-access"), translate!("stat-word-uid"), translate!("stat-word-gid"), diff --git a/src/uu/stdbuf/Cargo.toml b/src/uu/stdbuf/Cargo.toml index 802796199..b0742e89d 100644 --- a/src/uu/stdbuf/Cargo.toml +++ b/src/uu/stdbuf/Cargo.toml @@ -10,6 +10,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/stdbuf/build.rs b/src/uu/stdbuf/build.rs index d844f3790..43eea3f38 100644 --- a/src/uu/stdbuf/build.rs +++ b/src/uu/stdbuf/build.rs @@ -145,4 +145,40 @@ fn main() { found, "Could not find built libstdbuf library. Searched in: {possible_paths:?}." ); + + // Create a symlink to libstdbuf in the main target directory for development convenience + // This allows running stdbuf directly from the build directory (e.g., target/debug/coreutils) + // without needing to install the library to LIBSTDBUF_DIR. This is particularly useful for + // running tests and manual testing during development. + #[cfg(all(unix, feature = "feat_external_libstdbuf"))] + { + use std::path::PathBuf; + + // Get the main target directory (e.g., target/debug or target/release) + // OUT_DIR is something like target/debug/build/uu_stdbuf-/out + let out_dir_path = PathBuf::from(&out_dir); + if let Some(target_dir) = out_dir_path + .parent() + .and_then(|p| p.parent()) + .and_then(|p| p.parent()) + { + let lib_filename = format!("libstdbuf{}", platform::DYLIB_EXT); + let source = target_dir.join("deps").join(&lib_filename); + let dest = target_dir.join(&lib_filename); + + // Remove old symlink if it exists (in case it points to the wrong place) + let _ = fs::remove_file(&dest); + + // Create symlink if the source exists + if source.exists() { + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + if let Err(e) = symlink(&source, &dest) { + eprintln!("Warning: Failed to create symlink for libstdbuf: {e}"); + } + } + } + } + } } diff --git a/src/uu/stdbuf/src/libstdbuf/Cargo.toml b/src/uu/stdbuf/src/libstdbuf/Cargo.toml index 8a92fcbb5..2e76b2ee6 100644 --- a/src/uu/stdbuf/src/libstdbuf/Cargo.toml +++ b/src/uu/stdbuf/src/libstdbuf/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true [lints] workspace = true diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index bffe1f7b1..b5a8965cc 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -179,13 +179,34 @@ fn get_preload_env(_tmp_dir: &TempDir) -> UResult<(String, PathBuf)> { // Use the directory provided at compile time via LIBSTDBUF_DIR environment variable // This will fail to compile if LIBSTDBUF_DIR is not set, which is the desired behavior const LIBSTDBUF_DIR: &str = env!("LIBSTDBUF_DIR"); + + // Search paths in order: + // 1. Directory where stdbuf is located (program_path) + // 2. Compile-time directory from LIBSTDBUF_DIR + let mut search_paths: Vec = Vec::new(); + + // First, try to get the directory where stdbuf is running from + if let Ok(exe_path) = std::env::current_exe() { + if let Some(exe_dir) = exe_path.parent() { + search_paths.push(exe_dir.to_path_buf()); + } + } + + // Add the compile-time directory as fallback + search_paths.push(PathBuf::from(LIBSTDBUF_DIR)); + + // Search for libstdbuf in each path + for base_path in search_paths { + let path_buf = base_path.join("libstdbuf").with_extension(extension); + if path_buf.exists() { + return Ok((preload.to_owned(), path_buf)); + } + } + + // If not found in any path, report error let path_buf = PathBuf::from(LIBSTDBUF_DIR) .join("libstdbuf") .with_extension(extension); - if path_buf.exists() { - return Ok((preload.to_owned(), path_buf)); - } - Err(USimpleError::new( 1, translate!("stdbuf-error-external-libstdbuf-not-found", "path" => path_buf.display()), diff --git a/src/uu/stty/Cargo.toml b/src/uu/stty/Cargo.toml index 94812b2ac..f6c472a89 100644 --- a/src/uu/stty/Cargo.toml +++ b/src/uu/stty/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index af54db3e9..e6336aab1 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -595,7 +595,7 @@ impl WrappedPrinter { self.first_in_line = true; } - print!("{}{}", self.prefix(), token); + print!("{}{token}", self.prefix()); self.current += token_len; self.first_in_line = false; } diff --git a/src/uu/sum/Cargo.toml b/src/uu/sum/Cargo.toml index 983a7208c..b3b923ec2 100644 --- a/src/uu/sum/Cargo.toml +++ b/src/uu/sum/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/sync/Cargo.toml b/src/uu/sync/Cargo.toml index 6d00339ab..21df8266d 100644 --- a/src/uu/sync/Cargo.toml +++ b/src/uu/sync/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/sync/locales/en-US.ftl b/src/uu/sync/locales/en-US.ftl index d0b520bcb..272e5a822 100644 --- a/src/uu/sync/locales/en-US.ftl +++ b/src/uu/sync/locales/en-US.ftl @@ -7,8 +7,9 @@ sync-help-data = sync only file data, no unneeded metadata (Linux only) # Error messages sync-error-data-needs-argument = --data needs at least one argument -sync-error-opening-file = error opening { $file } +sync-error-opening-file = error opening { $file }: { $err } sync-error-no-such-file = error opening { $file }: No such file or directory +sync-error-syncing-file = error syncing { $file } # Warning messages sync-warning-fcntl-failed = warning: failed to reset O_NONBLOCK flag for { $file }: { $error } diff --git a/src/uu/sync/locales/fr-FR.ftl b/src/uu/sync/locales/fr-FR.ftl index f0d00db03..31c51aa4d 100644 --- a/src/uu/sync/locales/fr-FR.ftl +++ b/src/uu/sync/locales/fr-FR.ftl @@ -7,8 +7,9 @@ sync-help-data = synchroniser seulement les données des fichiers, pas les méta # Messages d'erreur sync-error-data-needs-argument = --data nécessite au moins un argument -sync-error-opening-file = erreur lors de l'ouverture de { $file } +sync-error-opening-file = erreur lors de l'ouverture de { $file } : { $err } sync-error-no-such-file = erreur lors de l'ouverture de { $file } : Aucun fichier ou répertoire de ce type +sync-error-syncing-file = erreur lors de la synchronisation de { $file } # Messages d'avertissement sync-warning-fcntl-failed = avertissement : échec de la réinitialisation du drapeau O_NONBLOCK pour { $file } : { $error } diff --git a/src/uu/sync/src/sync.rs b/src/uu/sync/src/sync.rs index 0e5b864b9..b91f216aa 100644 --- a/src/uu/sync/src/sync.rs +++ b/src/uu/sync/src/sync.rs @@ -14,10 +14,9 @@ use nix::fcntl::{OFlag, open}; use nix::sys::stat::Mode; use std::path::Path; use uucore::display::Quotable; -#[cfg(any(target_os = "linux", target_os = "android"))] -use uucore::error::FromIo; -use uucore::error::{UResult, USimpleError}; +use uucore::error::{UResult, USimpleError, get_exit_code, set_exit_code}; use uucore::format_usage; +use uucore::show_error; use uucore::translate; pub mod options { @@ -35,7 +34,11 @@ mod platform { #[cfg(any(target_os = "linux", target_os = "android"))] use nix::unistd::{fdatasync, syncfs}; #[cfg(any(target_os = "linux", target_os = "android"))] - use std::fs::File; + use std::fs::{File, OpenOptions}; + #[cfg(any(target_os = "linux", target_os = "android"))] + use std::os::unix::fs::OpenOptionsExt; + #[cfg(any(target_os = "linux", target_os = "android"))] + use uucore::display::Quotable; #[cfg(any(target_os = "linux", target_os = "android"))] use uucore::error::FromIo; #[cfg(any(target_os = "linux", target_os = "android"))] @@ -57,7 +60,11 @@ mod platform { /// Logs a warning if fcntl fails but doesn't abort the operation. #[cfg(any(target_os = "linux", target_os = "android"))] fn open_and_reset_nonblock(path: &str) -> UResult { - let f = File::open(path).map_err_context(|| path.to_string())?; + let f = OpenOptions::new() + .read(true) + .custom_flags(OFlag::O_NONBLOCK.bits()) + .open(path) + .map_err_context(|| path.to_string())?; // Reset O_NONBLOCK flag if it was set (matches GNU behavior) // This is non-critical, so we log errors but don't fail if let Err(e) = fcntl(&f, FcntlArg::F_SETFL(OFlag::empty())) { @@ -73,7 +80,9 @@ mod platform { pub fn do_syncfs(files: Vec) -> UResult<()> { for path in files { let f = open_and_reset_nonblock(&path)?; - syncfs(f)?; + syncfs(f).map_err_context( + || translate!("sync-error-syncing-file", "file" => path.quote()), + )?; } Ok(()) } @@ -82,7 +91,9 @@ mod platform { pub fn do_fdatasync(files: Vec) -> UResult<()> { for path in files { let f = open_and_reset_nonblock(&path)?; - fdatasync(f)?; + fdatasync(f).map_err_context( + || translate!("sync-error-syncing-file", "file" => path.quote()), + )?; } Ok(()) } @@ -223,23 +234,30 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let path = Path::new(&f); if let Err(e) = open(path, OFlag::O_NONBLOCK, Mode::empty()) { if e != Errno::EACCES || (e == Errno::EACCES && path.is_dir()) { - e.map_err_context( - || translate!("sync-error-opening-file", "file" => f.quote()), - )?; + show_error!( + "{}", + translate!("sync-error-opening-file", "file" => f.quote(), "err" => e.desc()) + ); + set_exit_code(1); } } } #[cfg(not(any(target_os = "linux", target_os = "android")))] { if !Path::new(&f).exists() { - return Err(USimpleError::new( - 1, - translate!("sync-error-no-such-file", "file" => f.quote()), - )); + show_error!( + "{}", + translate!("sync-error-no-such-file", "file" => f.quote()) + ); + set_exit_code(1); } } } + if get_exit_code() != 0 { + return Err(USimpleError::new(1, "")); + } + #[allow(clippy::if_same_then_else)] if matches.get_flag(options::FILE_SYSTEM) { #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] diff --git a/src/uu/tac/Cargo.toml b/src/uu/tac/Cargo.toml index 82ec5c5bc..e65ff6c88 100644 --- a/src/uu/tac/Cargo.toml +++ b/src/uu/tac/Cargo.toml @@ -11,6 +11,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index ec8ae4503..164cd58aa 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -411,7 +411,11 @@ fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UR fn try_mmap_stdin() -> Option { // SAFETY: If the file is truncated while we map it, SIGBUS will be raised // and our process will be terminated, thus preventing access of invalid memory. - unsafe { Mmap::map(&stdin()).ok() } + let mmap = unsafe { Mmap::map(&stdin()).ok()? }; + // On Windows, mmap on a pipe handle can "succeed" but return 0 bytes + // (the file size of a pipe is reported as 0). When that happens, return + // None so we fall through to buffer_stdin() which reads the pipe properly. + if mmap.is_empty() { None } else { Some(mmap) } } enum StdinData { diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index f01b4f603..5e11a5411 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -10,6 +10,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/tee/Cargo.toml b/src/uu/tee/Cargo.toml index 38a946edf..9be41804a 100644 --- a/src/uu/tee/Cargo.toml +++ b/src/uu/tee/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/test/Cargo.toml b/src/uu/test/Cargo.toml index 3348efd7b..002e42fd0 100644 --- a/src/uu/test/Cargo.toml +++ b/src/uu/test/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/timeout/Cargo.toml b/src/uu/timeout/Cargo.toml index e0f3db171..90a0b378c 100644 --- a/src/uu/timeout/Cargo.toml +++ b/src/uu/timeout/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index 1bde504fb..9f66c3795 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -10,6 +10,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/tr/Cargo.toml b/src/uu/tr/Cargo.toml index 0ab2ca9dd..e470db1ce 100644 --- a/src/uu/tr/Cargo.toml +++ b/src/uu/tr/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/tr/locales/en-US.ftl b/src/uu/tr/locales/en-US.ftl index 0f1fc38a9..087436847 100644 --- a/src/uu/tr/locales/en-US.ftl +++ b/src/uu/tr/locales/en-US.ftl @@ -23,6 +23,7 @@ tr-error-write-error = write error # Warning messages tr-warning-unescaped-backslash = warning: an unescaped backslash at end of string is not portable tr-warning-ambiguous-octal-escape = the ambiguous octal escape \{ $origin_octal } is being interpreted as the 2-byte sequence \0{ $actual_octal_tail }, { $outstand_char } +tr-warning-invalid-utf8 = invalid utf8 sequence # Sequence parsing error messages tr-error-missing-char-class-name = missing character class name '[::]' diff --git a/src/uu/tr/locales/fr-FR.ftl b/src/uu/tr/locales/fr-FR.ftl index e8af47a0d..075b4a447 100644 --- a/src/uu/tr/locales/fr-FR.ftl +++ b/src/uu/tr/locales/fr-FR.ftl @@ -24,6 +24,7 @@ tr-error-write-error = erreur d'écriture tr-warning-unescaped-backslash = avertissement : une barre oblique inverse non échappée à la fin de la chaîne n'est pas portable tr-warning-ambiguous-octal-escape = l'échappement octal ambigu \{ $origin_octal } est en cours d'interprétation comme la séquence de 2 octets \0{ $actual_octal_tail }, { $outstand_char } +tr-warning-invalid-utf8 = séquence UTF-8 non valide # Messages d'erreur d'analyse de séquence tr-error-missing-char-class-name = nom de classe de caractères manquant '[::]' diff --git a/src/uu/tr/src/operation.rs b/src/uu/tr/src/operation.rs index 0c62840e0..a9d1f5287 100644 --- a/src/uu/tr/src/operation.rs +++ b/src/uu/tr/src/operation.rs @@ -379,13 +379,16 @@ impl Sequence { let str_to_parse = std::str::from_utf8(out).unwrap(); let result = u8::from_str_radix(str_to_parse, 8).ok(); if result.is_none() { - let origin_octal: &str = std::str::from_utf8(input).unwrap(); - let actual_octal_tail: &str = std::str::from_utf8(&input[0..2]).unwrap(); - let outstand_char: char = char::from_u32(input[2] as u32).unwrap(); - show_warning!( - "{}", - translate!("tr-warning-ambiguous-octal-escape", "origin_octal" => origin_octal, "actual_octal_tail" => actual_octal_tail, "outstand_char" => outstand_char) - ); + if let Ok(origin_octal) = std::str::from_utf8(input) { + let actual_octal_tail: &str = std::str::from_utf8(&input[0..2]).unwrap(); + let outstand_char: char = char::from_u32(input[2] as u32).unwrap(); + show_warning!( + "{}", + translate!("tr-warning-ambiguous-octal-escape", "origin_octal" => origin_octal, "actual_octal_tail" => actual_octal_tail, "outstand_char" => outstand_char) + ); + } else { + show_warning!("{}", translate!("tr-warning-invalid-utf8")); + } } result }, @@ -547,8 +550,8 @@ impl Sequence { (Ok(c), Ok(())) => Ok(Self::Char(c)), (Ok(c), Err(v)) => Err(BadSequence::MultipleCharInEquivalence(format!( "{}{}", - String::from_utf8_lossy(&[c]).into_owned(), - String::from_utf8_lossy(v).into_owned() + String::from_utf8_lossy(&[c]), + String::from_utf8_lossy(v), ))), }, ) diff --git a/src/uu/true/Cargo.toml b/src/uu/true/Cargo.toml index 468edddc2..c77ad1ba7 100644 --- a/src/uu/true/Cargo.toml +++ b/src/uu/true/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/truncate/Cargo.toml b/src/uu/truncate/Cargo.toml index 07ab63e6d..c2bd37149 100644 --- a/src/uu/truncate/Cargo.toml +++ b/src/uu/truncate/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/tsort/Cargo.toml b/src/uu/tsort/Cargo.toml index 74209a62f..84a313496 100644 --- a/src/uu/tsort/Cargo.toml +++ b/src/uu/tsort/Cargo.toml @@ -10,6 +10,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/tsort/benches/tsort_bench.rs b/src/uu/tsort/benches/tsort_bench.rs index 18d121d66..28395cffd 100644 --- a/src/uu/tsort/benches/tsort_bench.rs +++ b/src/uu/tsort/benches/tsort_bench.rs @@ -12,7 +12,7 @@ fn generate_linear_chain(num_nodes: usize) -> Vec { let mut data = Vec::new(); for i in 0..num_nodes.saturating_sub(1) { - data.extend_from_slice(format!("node{} node{}\n", i, i + 1).as_bytes()); + data.extend_from_slice(format!("node{i} node{}\n", i + 1).as_bytes()); } data @@ -85,10 +85,8 @@ fn generate_wide_dag(num_nodes: usize) -> Vec { for i in chain_start..chain_end.saturating_sub(1) { data.extend_from_slice( format!( - "chain{}_{} chain{}_{}\n", - chain, + "chain{chain}_{} chain{chain}_{}\n", i - chain_start, - chain, i + 1 - chain_start ) .as_bytes(), @@ -102,10 +100,8 @@ fn generate_wide_dag(num_nodes: usize) -> Vec { let curr_mid = chain_start + chain_length / 4; data.extend_from_slice( format!( - "chain{}_{} chain{}_{}\n", - prev_chain, + "chain{prev_chain}_{} chain{chain}_{}\n", prev_end - prev_chain * chain_length, - chain, curr_mid - chain_start ) .as_bytes(), diff --git a/src/uu/tty/Cargo.toml b/src/uu/tty/Cargo.toml index 77165c605..fe5a97b07 100644 --- a/src/uu/tty/Cargo.toml +++ b/src/uu/tty/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/uname/Cargo.toml b/src/uu/uname/Cargo.toml index f640412e2..fe704f96e 100644 --- a/src/uu/uname/Cargo.toml +++ b/src/uu/uname/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/unexpand/Cargo.toml b/src/uu/unexpand/Cargo.toml index d7ea1533b..81e44d920 100644 --- a/src/uu/unexpand/Cargo.toml +++ b/src/uu/unexpand/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index 14c2b8b0b..cfedbe367 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -8,7 +8,7 @@ use clap::{Arg, ArgAction, Command}; use std::ffi::OsString; use std::fs::File; -use std::io::{BufRead, BufReader, BufWriter, Read, Stdout, Write, stdin, stdout}; +use std::io::{BufReader, BufWriter, Read, Stdout, Write, stdin, stdout}; use std::num::IntErrorKind; use std::path::Path; use std::str::from_utf8; @@ -347,7 +347,7 @@ fn next_tabstop(tab_config: &TabConfig, col: usize) -> Option { fn write_tabs( output: &mut BufWriter, tab_config: &TabConfig, - mut scol: usize, + scol: &mut usize, col: usize, prevtab: bool, init: bool, @@ -357,20 +357,20 @@ fn write_tabs( // We never turn a single space before a non-blank into // a tab, unless it's at the start of the line. let ai = init || amode; - if (ai && !prevtab && col > scol + 1) || (col > scol && (init || ai && prevtab)) { - while let Some(nts) = next_tabstop(tab_config, scol) { - if col < scol + nts { + if (ai && !prevtab && col > *scol + 1) || (col > *scol && (init || ai && prevtab)) { + while let Some(nts) = next_tabstop(tab_config, *scol) { + if col < *scol + nts { break; } output.write_all(b"\t")?; - scol += nts; + *scol += nts; } } - while col > scol { + while col > *scol { output.write_all(b" ")?; - scol += 1; + *scol += 1; } Ok(()) } @@ -424,81 +424,98 @@ fn next_char_info(uflag: bool, buf: &[u8], byte: usize) -> (CharType, usize, usi } #[allow(clippy::cognitive_complexity)] -fn unexpand_line( - buf: &mut Vec, +#[allow(clippy::too_many_arguments)] +fn unexpand_buf( + buf: &[u8], output: &mut BufWriter, options: &Options, lastcol: usize, tab_config: &TabConfig, + col: &mut usize, + scol: &mut usize, + leading: &mut bool, + pctype: &mut CharType, ) -> UResult<()> { - // Fast path: if we're not converting all spaces (-a flag not set) - // and the line doesn't start with spaces, just write it directly - if !options.aflag && !buf.is_empty() && buf[0] != b' ' && buf[0] != b'\t' { - output.write_all(buf)?; - buf.truncate(0); - return Ok(()); - } - - 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 - let mut init = true; // are we at the start of the line? - let mut pctype = CharType::Other; - - // Fast path for leading spaces in non-UTF8 mode: count consecutive spaces/tabs at start - if !options.uflag && !options.aflag { - // In default mode (not -a), we only convert leading spaces - // So we can batch process them and then copy the rest - while byte < buf.len() { - match buf[byte] { - b' ' => { - col += 1; - byte += 1; - } - b'\t' => { - col += next_tabstop(tab_config, col).unwrap_or(1); - byte += 1; - pctype = CharType::Tab; - } - _ => break, - } - } - - // If we found spaces/tabs, write them as tabs - if byte > 0 { - write_tabs( - output, - tab_config, - 0, - col, - pctype == CharType::Tab, - true, - true, - )?; - } - - // Write the rest of the line directly (no more tab conversion needed) - if byte < buf.len() { - output.write_all(&buf[byte..])?; - } - buf.truncate(0); - return Ok(()); - } - - while byte < buf.len() { - // when we have a finite number of columns, never convert past the last column - if lastcol > 0 && col >= lastcol { + // We can only fast forward if we don't need to calculate col/scol + if let Some(b'\n') = buf.last() { + // Fast path: if we're not converting all spaces (-a flag not set) + // and the line doesn't start with spaces, just write it directly + if !options.aflag && !buf.is_empty() && ((buf[0] != b' ' && buf[0] != b'\t') || !*leading) { write_tabs( output, tab_config, scol, - col, - pctype == CharType::Tab, - init, + *col, + *pctype == CharType::Tab, + *leading, + options.aflag, + )?; + *scol = *col; + *col += buf.len(); + output.write_all(buf)?; + return Ok(()); + } + } + + let mut byte = 0; // offset into the buffer + + // We can only fast forward if we don't need to calculate col/scol + if let Some(b'\n') = buf.last() { + // Fast path for leading spaces in non-UTF8 mode: count consecutive spaces/tabs at start + if !options.uflag && !options.aflag && *leading { + // In default mode (not -a), we only convert leading spaces + // So we can batch process them and then copy the rest + while byte < buf.len() { + match buf[byte] { + b' ' => { + *col += 1; + byte += 1; + } + b'\t' => { + *col += next_tabstop(tab_config, *col).unwrap_or(1); + byte += 1; + *pctype = CharType::Tab; + } + _ => break, + } + } + + // If we found spaces/tabs, write them as tabs + if byte > 0 { + write_tabs( + output, + tab_config, + scol, + *col, + *pctype == CharType::Tab, + true, + options.aflag, + )?; + } + + // Write the rest of the line directly (no more tab conversion needed) + if byte < buf.len() { + *leading = false; + output.write_all(&buf[byte..])?; + } + return Ok(()); + } + } + + 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, + tab_config, + scol, + *col, + *pctype == CharType::Tab, + *leading, true, )?; output.write_all(&buf[byte..])?; - scol = col; + *scol = *col; break; } @@ -506,19 +523,19 @@ fn unexpand_line( let (ctype, cwidth, nbytes) = next_char_info(options.uflag, buf, byte); // now figure out how many columns this char takes up, and maybe print it - let tabs_buffered = init || options.aflag; + let tabs_buffered = *leading || options.aflag; match ctype { CharType::Space | CharType::Tab => { // compute next col, but only write space or tab chars if not buffering - col += if ctype == CharType::Space { + *col += if ctype == CharType::Space { 1 } else { - next_tabstop(tab_config, col).unwrap_or(1) + next_tabstop(tab_config, *col).unwrap_or(1) }; if !tabs_buffered { output.write_all(&buf[byte..byte + nbytes])?; - scol = col; // now printed up to this column + *scol = *col; // now printed up to this column } } CharType::Other | CharType::Backspace => { @@ -527,42 +544,30 @@ fn unexpand_line( output, tab_config, scol, - col, - pctype == CharType::Tab, - init, + *col, + *pctype == CharType::Tab, + *leading, options.aflag, )?; - init = false; // no longer at the start of a line - col = if ctype == CharType::Other { + *leading = false; // no longer at the start of a line + *col = if ctype == CharType::Other { // use computed width - col + cwidth - } else if col > 0 { + *col + cwidth + } else if *col > 0 { // Backspace case, but only if col > 0 - col - 1 + *col - 1 } else { 0 }; output.write_all(&buf[byte..byte + nbytes])?; - scol = col; // we've now printed up to this column + *scol = *col; // we've now printed up to this column } } byte += nbytes; // move on to next char - pctype = ctype; // save the previous type + *pctype = ctype; // save the previous type } - // write out anything remaining - write_tabs( - output, - tab_config, - scol, - col, - pctype == CharType::Tab, - init, - true, - )?; - buf.truncate(0); // clear out the buffer - Ok(()) } @@ -573,15 +578,49 @@ fn unexpand_file( lastcol: usize, tab_config: &TabConfig, ) -> UResult<()> { - let mut buf = Vec::new(); + let mut buf = [0u8; 4096]; let mut input = open(file)?; + let mut col = 0; + let mut scol = 0; + let mut leading = true; + let mut pctype = CharType::Other; loop { - match input.read_until(b'\n', &mut buf) { + match input.read(&mut buf) { Ok(0) => break, - Ok(_) => unexpand_line(&mut buf, output, options, lastcol, tab_config)?, + Ok(n) => { + for line in buf[..n].split_inclusive(|b| *b == b'\n') { + unexpand_buf( + line, + output, + options, + lastcol, + tab_config, + &mut col, + &mut scol, + &mut leading, + &mut pctype, + )?; + if let Some(b'\n') = line.last() { + col = 0; + scol = 0; + leading = true; + pctype = CharType::Other; + } + } + } Err(e) => return Err(e.map_err_context(|| file.maybe_quote().to_string())), } } + // write out anything remaining + write_tabs( + output, + tab_config, + &mut scol, + col, + pctype == CharType::Tab, + leading, + options.aflag, + )?; Ok(()) } diff --git a/src/uu/uniq/Cargo.toml b/src/uu/uniq/Cargo.toml index 0bd197827..a484c2a14 100644 --- a/src/uu/uniq/Cargo.toml +++ b/src/uu/uniq/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/unlink/Cargo.toml b/src/uu/unlink/Cargo.toml index 85d668751..8f9adc52d 100644 --- a/src/uu/unlink/Cargo.toml +++ b/src/uu/unlink/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/uptime/Cargo.toml b/src/uu/uptime/Cargo.toml index 026039026..3d078fabb 100644 --- a/src/uu/uptime/Cargo.toml +++ b/src/uu/uptime/Cargo.toml @@ -11,6 +11,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/users/Cargo.toml b/src/uu/users/Cargo.toml index fa311e74a..3a68307e3 100644 --- a/src/uu/users/Cargo.toml +++ b/src/uu/users/Cargo.toml @@ -11,6 +11,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/vdir/Cargo.toml b/src/uu/vdir/Cargo.toml index 63097cdec..a9b6c5191 100644 --- a/src/uu/vdir/Cargo.toml +++ b/src/uu/vdir/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/wc/Cargo.toml b/src/uu/wc/Cargo.toml index ae9bb6e89..b4c2005af 100644 --- a/src/uu/wc/Cargo.toml +++ b/src/uu/wc/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/wc/src/count_fast.rs b/src/uu/wc/src/count_fast.rs index d20c53d4f..5e2ecd080 100644 --- a/src/uu/wc/src/count_fast.rs +++ b/src/uu/wc/src/count_fast.rs @@ -136,7 +136,7 @@ pub(crate) fn count_bytes_fast(handle: &mut T) -> (usize, Opti && stat.st_size > 0 { let sys_page_size = unsafe { sysconf(_SC_PAGESIZE) as usize }; - if stat.st_size as usize % sys_page_size > 0 { + if !(stat.st_size as usize).is_multiple_of(sys_page_size) { // regular file or file from /proc, /sys and similar pseudo-filesystems // with size that is NOT a multiple of system page size return (stat.st_size as usize, None); diff --git a/src/uu/who/Cargo.toml b/src/uu/who/Cargo.toml index fdb5dd41d..c4cf5145b 100644 --- a/src/uu/who/Cargo.toml +++ b/src/uu/who/Cargo.toml @@ -12,6 +12,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/whoami/Cargo.toml b/src/uu/whoami/Cargo.toml index 80e135827..fe88d6407 100644 --- a/src/uu/whoami/Cargo.toml +++ b/src/uu/whoami/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uu/yes/Cargo.toml b/src/uu/yes/Cargo.toml index 33623c7c9..b8703bb55 100644 --- a/src/uu/yes/Cargo.toml +++ b/src/uu/yes/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true keywords.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true readme.workspace = true [lints] diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index d18d0630e..29a7189d4 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -7,6 +7,7 @@ repository = "https://github.com/uutils/coreutils/tree/main/src/uucore" authors.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true homepage.workspace = true keywords.workspace = true license.workspace = true @@ -150,7 +151,8 @@ format = [ "quoting-style", "unit-prefix", ] -i18n-all = ["i18n-collator", "i18n-decimal", "i18n-datetime"] +i18n-all = ["i18n-charmap", "i18n-collator", "i18n-decimal", "i18n-datetime"] +i18n-charmap = ["i18n-common"] i18n-common = ["icu_locale"] i18n-collator = ["i18n-common", "icu_collator"] i18n-decimal = ["i18n-common", "icu_decimal", "icu_provider"] diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index a2fba209c..13b0680df 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -114,6 +114,9 @@ impl AlgoKind { ALGORITHM_OPTIONS_SHA256 => Sha256, ALGORITHM_OPTIONS_SHA384 => Sha384, ALGORITHM_OPTIONS_SHA512 => Sha512, + + ALGORITHM_OPTIONS_SHAKE128 => Shake128, + ALGORITHM_OPTIONS_SHAKE256 => Shake256, _ => return Err(ChecksumError::UnknownAlgorithm(algo.as_ref().to_string()).into()), }) } @@ -247,8 +250,8 @@ pub enum SizedAlgoKind { Sha3(ShaLength), // Note: we store Blake2b's length as BYTES. Blake2b(Option), - Shake128(usize), - Shake256(usize), + Shake128(Option), + Shake256(Option), } impl SizedAlgoKind { @@ -280,8 +283,8 @@ impl SizedAlgoKind { (ak::Sha1, _) => Ok(Self::Sha1), (ak::Blake3, _) => Ok(Self::Blake3), - (ak::Shake128, Some(l)) => Ok(Self::Shake128(l)), - (ak::Shake256, Some(l)) => Ok(Self::Shake256(l)), + (ak::Shake128, l) => Ok(Self::Shake128(l)), + (ak::Shake256, l) => Ok(Self::Shake256(l)), (ak::Sha2, Some(l)) => Ok(Self::Sha2(ShaLength::try_from(l)?)), (ak::Sha3, Some(l)) => Ok(Self::Sha3(ShaLength::try_from(l)?)), (algo @ (ak::Sha2 | ak::Sha3), None) => { @@ -298,7 +301,6 @@ impl SizedAlgoKind { (ak::Sha256, None) => Ok(Self::Sha2(ShaLength::Len256)), (ak::Sha384, None) => Ok(Self::Sha2(ShaLength::Len384)), (ak::Sha512, None) => Ok(Self::Sha2(ShaLength::Len512)), - (_, None) => Err(ChecksumError::LengthRequired(kind.to_uppercase().into()).into()), } } @@ -322,45 +324,49 @@ impl SizedAlgoKind { pub fn create_digest(&self) -> Box { use ShaLength::*; match self { - Self::Sysv => Box::new(SysV::new()), - Self::Bsd => Box::new(Bsd::new()), - Self::Crc => Box::new(Crc::new()), - Self::Crc32b => Box::new(CRC32B::new()), - Self::Md5 => Box::new(Md5::new()), - Self::Sm3 => Box::new(Sm3::new()), - Self::Sha1 => Box::new(Sha1::new()), - Self::Blake3 => Box::new(Blake3::new()), - Self::Sha2(Len224) => Box::new(Sha224::new()), - Self::Sha2(Len256) => Box::new(Sha256::new()), - Self::Sha2(Len384) => Box::new(Sha384::new()), - Self::Sha2(Len512) => Box::new(Sha512::new()), - Self::Sha3(Len224) => Box::new(Sha3_224::new()), - Self::Sha3(Len256) => Box::new(Sha3_256::new()), - Self::Sha3(Len384) => Box::new(Sha3_384::new()), - Self::Sha3(Len512) => Box::new(Sha3_512::new()), - Self::Blake2b(Some(byte_len)) => Box::new(Blake2b::with_output_bytes(*byte_len)), - Self::Blake2b(None) => Box::new(Blake2b::new()), - Self::Shake128(_) => Box::new(Shake128::new()), - Self::Shake256(_) => Box::new(Shake256::new()), + Self::Sysv => Box::new(SysV::default()), + Self::Bsd => Box::new(Bsd::default()), + Self::Crc => Box::new(Crc::default()), + Self::Crc32b => Box::new(CRC32B::default()), + Self::Md5 => Box::new(Md5::default()), + Self::Sm3 => Box::new(Sm3::default()), + Self::Sha1 => Box::new(Sha1::default()), + Self::Blake3 => Box::new(Blake3::default()), + Self::Sha2(Len224) => Box::new(Sha224::default()), + Self::Sha2(Len256) => Box::new(Sha256::default()), + Self::Sha2(Len384) => Box::new(Sha384::default()), + Self::Sha2(Len512) => Box::new(Sha512::default()), + Self::Sha3(Len224) => Box::new(Sha3_224::default()), + Self::Sha3(Len256) => Box::new(Sha3_256::default()), + Self::Sha3(Len384) => Box::new(Sha3_384::default()), + Self::Sha3(Len512) => Box::new(Sha3_512::default()), + Self::Blake2b(len_opt) => { + Box::new(len_opt.map(Blake2b::with_output_bytes).unwrap_or_default()) + } + Self::Shake128(len_opt) => { + Box::new(len_opt.map(Shake128::with_output_bits).unwrap_or_default()) + } + Self::Shake256(len_opt) => { + Box::new(len_opt.map(Shake256::with_output_bits).unwrap_or_default()) + } } } pub fn bitlen(&self) -> usize { - use SizedAlgoKind::*; match self { - Sysv => 512, - Bsd => 1024, - Crc => 256, - Crc32b => 32, - Md5 => 128, - Sm3 => 512, - Sha1 => 160, - Blake3 => 256, - Sha2(len) => len.as_usize(), - Sha3(len) => len.as_usize(), - Blake2b(len) => len.unwrap_or(512), - Shake128(len) => *len, - Shake256(len) => *len, + Self::Sysv => 512, + Self::Bsd => 1024, + Self::Crc => 256, + Self::Crc32b => 32, + Self::Md5 => 128, + Self::Sm3 => 512, + Self::Sha1 => 160, + Self::Blake3 => 256, + Self::Sha2(len) => len.as_usize(), + Self::Sha3(len) => len.as_usize(), + Self::Blake2b(len) => len.unwrap_or(Blake2b::DEFAULT_BYTE_SIZE * 8), + Self::Shake128(len) => len.unwrap_or(Shake128::DEFAULT_BIT_SIZE), + Self::Shake256(len) => len.unwrap_or(Shake256::DEFAULT_BIT_SIZE), } } pub fn is_legacy(&self) -> bool { diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index 7fede1c49..64dc47631 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -417,7 +417,7 @@ impl LineFormat { // checksums that are fully alphanumeric. Another check happens later // when we are provided with a length hint to detect ambiguous // base64-encoded checksums. - if is_base64 && checksum.len() % 4 != 0 { + if is_base64 && !checksum.len().is_multiple_of(4) { return None; } @@ -493,7 +493,7 @@ fn get_raw_expected_digest(checksum: &str, byte_len_hint: Option) -> Opti // If the length of the digest is not a multiple of 2, then it must be // improperly formatted (1 byte is 2 hex digits, and base64 strings should // always be a multiple of 4). - if checksum.len() % 2 != 0 { + if !checksum.len().is_multiple_of(2) { return None; } @@ -516,7 +516,7 @@ fn get_raw_expected_digest(checksum: &str, byte_len_hint: Option) -> Opti // It is important to check it before trying to decode, because the // forgiving mode of decoding will ignore if padding characters '=' are // MISSING, but to match GNU's behavior, we must reject it. - if checksum.len() % 4 != 0 { + if !checksum.len().is_multiple_of(4) { return None; } diff --git a/src/uucore/src/lib/features/encoding.rs b/src/uucore/src/lib/features/encoding.rs index 2f7caae2b..635f595e8 100644 --- a/src/uucore/src/lib/features/encoding.rs +++ b/src/uucore/src/lib/features/encoding.rs @@ -100,7 +100,7 @@ impl SupportsFastDecodeAndEncode for Base64SimdWrapper { // If there are no more '=' bytes the tail might still be padded // (len % 4 == 0) or purposely unpadded (GNU --ignore-garbage or // concatenated streams), so select the matching alphabet. - let decoder = if remaining.len() % 4 == 0 { + let decoder = if remaining.len().is_multiple_of(4) { Self::decode_with_standard } else { Self::decode_with_no_pad @@ -448,7 +448,7 @@ impl SupportsFastDecodeAndEncode for Z85Wrapper { fn encode_to_vec_deque(&self, input: &[u8], output: &mut VecDeque) -> UResult<()> { // According to the spec we should not accept inputs whose len is not a multiple of 4. // However, the z85 crate implements a padded encoding and accepts such inputs. We have to manually check for them. - if input.len() % 4 != 0 { + if !input.len().is_multiple_of(4) { return Err(USimpleError::new( 1, "error: invalid input (length must be multiple of 4 characters)".to_owned(), diff --git a/src/uucore/src/lib/features/format/num_format.rs b/src/uucore/src/lib/features/format/num_format.rs index 9a09b3f7d..9d44491b4 100644 --- a/src/uucore/src/lib/features/format/num_format.rs +++ b/src/uucore/src/lib/features/format/num_format.rs @@ -425,7 +425,7 @@ fn format_float_scientific( return if force_decimal == ForceDecimal::Yes && precision == 0 { format!("0.{exp_char}+00") } else { - format!("{:.*}{exp_char}+00", precision, 0.0) + format!("{:.precision$}{exp_char}+00", 0.0) }; } diff --git a/src/uucore/src/lib/features/i18n/charmap.rs b/src/uucore/src/lib/features/i18n/charmap.rs new file mode 100644 index 000000000..2ec99229b --- /dev/null +++ b/src/uucore/src/lib/features/i18n/charmap.rs @@ -0,0 +1,140 @@ +// 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 langinfo charmap eucjp euckr euctw CTYPE HKSCS hkscs localedata + +//! Locale-aware multi-byte character length detection via `LC_CTYPE`. + +use std::sync::OnceLock; + +enum MbEncoding { + Utf8, + Gb18030, + EucJp, + EucKr, + Big5, +} + +fn encoding_from_name(enc: &str) -> MbEncoding { + match enc { + "gb18030" | "gbk" | "gb2312" => MbEncoding::Gb18030, + "euc-jp" | "eucjp" => MbEncoding::EucJp, + "euc-kr" | "euckr" => MbEncoding::EucKr, + "big5" | "big5-hkscs" | "big5hkscs" | "euc-tw" | "euctw" => MbEncoding::Big5, + _ => MbEncoding::Utf8, + } +} + +fn get_encoding() -> &'static MbEncoding { + static ENCODING: OnceLock = OnceLock::new(); + ENCODING.get_or_init(|| { + let val = ["LC_ALL", "LC_CTYPE", "LANG"] + .iter() + .find_map(|&k| std::env::var(k).ok().filter(|v| !v.is_empty())); + let s = match val.as_deref() { + Some(s) if s != "C" && s != "POSIX" => s, + _ => return MbEncoding::Utf8, + }; + if let Some(enc) = s.split('.').nth(1) { + let enc = enc.split('@').next().unwrap_or(enc); + encoding_from_name(&enc.to_ascii_lowercase()) + } else { + // Bare locale defaults from glibc localedata/SUPPORTED + match s.split('@').next().unwrap_or(s) { + "zh_CN" | "zh_SG" => MbEncoding::Gb18030, + "zh_TW" | "zh_HK" => MbEncoding::Big5, + _ => MbEncoding::Utf8, + } + } + }) +} + +/// Byte length of the first character in `bytes` under the current locale encoding. +pub fn mb_char_len(bytes: &[u8]) -> usize { + debug_assert!(!bytes.is_empty()); + let b0 = bytes[0]; + if b0 <= 0x7F { + return 1; + } + match get_encoding() { + MbEncoding::Utf8 => utf8_len(bytes, b0), + MbEncoding::Gb18030 => gb18030_len(bytes, b0), + MbEncoding::EucJp => eucjp_len(bytes, b0), + MbEncoding::EucKr => euckr_len(bytes, b0), + MbEncoding::Big5 => big5_len(bytes, b0), + } +} + +// All helpers below assume b0 > 0x7F (ASCII already handled by caller). + +fn utf8_len(b: &[u8], b0: u8) -> usize { + let n = match b0 { + 0xC2..=0xDF => 2, + 0xE0..=0xEF => 3, + 0xF0..=0xF4 => 4, + _ => return 1, + }; + if b.len() >= n && b[1..n].iter().all(|&c| c & 0xC0 == 0x80) { + n + } else { + 1 + } +} + +// 2-byte: [81-FE][40-7E,80-FE] 4-byte: [81-FE][30-39][81-FE][30-39] +fn gb18030_len(b: &[u8], b0: u8) -> usize { + if !(0x81..=0xFE).contains(&b0) { + return 1; + } + if b.len() >= 4 + && (0x30..=0x39).contains(&b[1]) + && (0x81..=0xFE).contains(&b[2]) + && (0x30..=0x39).contains(&b[3]) + { + return 4; + } + if b.len() >= 2 && ((0x40..=0x7E).contains(&b[1]) || (0x80..=0xFE).contains(&b[1])) { + return 2; + } + 1 +} + +// 3-byte: [8F][A1-FE][A1-FE] 2-byte: [8E][A1-DF] or [A1-FE][A1-FE] +fn eucjp_len(b: &[u8], b0: u8) -> usize { + if b0 == 0x8F && b.len() >= 3 && (0xA1..=0xFE).contains(&b[1]) && (0xA1..=0xFE).contains(&b[2]) + { + return 3; + } + if b.len() >= 2 { + if b0 == 0x8E && (0xA1..=0xDF).contains(&b[1]) { + return 2; + } + if (0xA1..=0xFE).contains(&b0) && (0xA1..=0xFE).contains(&b[1]) { + return 2; + } + } + 1 +} + +// 2-byte: [A1-FE][A1-FE] +fn euckr_len(b: &[u8], b0: u8) -> usize { + if (0xA1..=0xFE).contains(&b0) && b.len() >= 2 && (0xA1..=0xFE).contains(&b[1]) { + 2 + } else { + 1 + } +} + +// 2-byte: [81-FE][40-7E,A1-FE] +fn big5_len(b: &[u8], b0: u8) -> usize { + if (0x81..=0xFE).contains(&b0) + && b.len() >= 2 + && ((0x40..=0x7E).contains(&b[1]) || (0xA1..=0xFE).contains(&b[1])) + { + 2 + } else { + 1 + } +} diff --git a/src/uucore/src/lib/features/i18n/mod.rs b/src/uucore/src/lib/features/i18n/mod.rs index e8e0f3f3c..13629710c 100644 --- a/src/uucore/src/lib/features/i18n/mod.rs +++ b/src/uucore/src/lib/features/i18n/mod.rs @@ -7,6 +7,8 @@ use std::sync::OnceLock; use icu_locale::{Locale, locale}; +#[cfg(feature = "i18n-charmap")] +pub mod charmap; #[cfg(feature = "i18n-collator")] pub mod collator; #[cfg(feature = "i18n-datetime")] diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index e9aaf875d..467406b45 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -388,14 +388,13 @@ impl ChownExecutor { // Use fchown (safe) to change the directory's ownership if let Err(e) = dir_fd.fchown(self.dest_uid, self.dest_gid) { let mut error_msg = format!( - "changing {} of {}: {}", + "changing {} of {}: {e}", if self.verbosity.groups_only { "group" } else { "ownership" }, path.quote(), - e ); if self.verbosity.level == VerbosityLevel::Verbose { @@ -521,7 +520,7 @@ impl ChownExecutor { entry_path.quote(), strip_errno(&e) ); - show_error!("{}", msg); + show_error!("{msg}"); } } else { // Report the successful ownership change using the shared helper diff --git a/src/uucore/src/lib/features/quoting_style/shell_quoter.rs b/src/uucore/src/lib/features/quoting_style/shell_quoter.rs index 58d3b3e8d..bb531b50d 100644 --- a/src/uucore/src/lib/features/quoting_style/shell_quoter.rs +++ b/src/uucore/src/lib/features/quoting_style/shell_quoter.rs @@ -40,7 +40,7 @@ impl<'a> NonEscapedShellQuoter<'a> { dirname: bool, size_hint: usize, ) -> Self { - let (quotes, must_quote) = initial_quoting(reference, dirname, always_quote); + let (quotes, must_quote) = initial_quoting(reference, dirname, always_quote, false); Self { reference, quotes, @@ -108,7 +108,7 @@ pub(super) struct EscapedShellQuoter<'a> { impl<'a> EscapedShellQuoter<'a> { pub fn new(reference: &'a [u8], always_quote: bool, dirname: bool, size_hint: usize) -> Self { - let (quotes, must_quote) = initial_quoting(reference, dirname, always_quote); + let (quotes, must_quote) = initial_quoting(reference, dirname, always_quote, true); Self { reference, quotes, @@ -185,11 +185,17 @@ impl Quoter for EscapedShellQuoter<'_> { } /// Deduce the initial quoting status from the provided information -fn initial_quoting(input: &[u8], dirname: bool, always_quote: bool) -> (Quotes, bool) { - if input - .iter() - .any(|c| shell_escaped_char_set(dirname).contains(c)) - { +fn initial_quoting( + input: &[u8], + dirname: bool, + always_quote: bool, + check_control_chars: bool, +) -> (Quotes, bool) { + let has_special_chars = input.iter().any(|c| { + shell_escaped_char_set(dirname).contains(c) || (check_control_chars && c.is_ascii_control()) + }); + + if has_special_chars { (Quotes::Single, true) } else if input.contains(&b'\'') { (Quotes::Double, true) @@ -239,3 +245,73 @@ fn finalize_shell_quoter( buffer } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_initial_quoting() { + // Control chars (0-31 and 0x7F) force single quotes in escape mode + assert_eq!( + initial_quoting(b"\x01", false, false, true), + (Quotes::Single, true) + ); + + // Control + quote uses single quotes (segmented) in escape mode + assert_eq!( + initial_quoting(b"\x01'\x01", false, false, true), + (Quotes::Single, true) + ); + + // Simple quote uses double quotes in escape mode + assert_eq!( + initial_quoting(b"a'b", false, false, true), + (Quotes::Double, true) + ); + + // Shell special chars force single quotes in escape mode + assert_eq!( + initial_quoting(b"test$var", false, false, true), + (Quotes::Single, true) + ); + assert_eq!( + initial_quoting(b"test\nline", false, false, true), + (Quotes::Single, true) + ); + + // Empty string forces quotes in escape mode + assert_eq!( + initial_quoting(b"", false, false, true), + (Quotes::Single, true) + ); + + // Always quote flag works in escape mode + assert_eq!( + initial_quoting(b"normal", false, true, true), + (Quotes::Single, true) + ); + + // Normal text doesn't need quoting in escape mode + assert_eq!( + initial_quoting(b"hello", false, false, true), + (Quotes::Single, false) + ); + + // Dirname affects colon handling in escape mode + assert_eq!( + initial_quoting(b"dir:name", true, false, true), + (Quotes::Single, true) + ); + assert_eq!( + initial_quoting(b"file:name", false, false, true), + (Quotes::Single, false) + ); + + // Control chars ignored in non-escape mode + assert_eq!( + initial_quoting(b"\x01", false, false, false), + (Quotes::Single, false) + ); + } +} diff --git a/src/uucore/src/lib/features/selinux.rs b/src/uucore/src/lib/features/selinux.rs index ed79be414..da0297d00 100644 --- a/src/uucore/src/lib/features/selinux.rs +++ b/src/uucore/src/lib/features/selinux.rs @@ -365,7 +365,7 @@ pub fn preserve_security_context(from_path: &Path, to_path: &Path) -> Result<(), /// use uucore::selinux::get_getfattr_output; /// /// let context = get_getfattr_output("/path/to/file"); -/// println!("SELinux context: {}", context); +/// println!("SELinux context: {context}"); /// ``` pub fn get_getfattr_output(f: &str) -> String { use std::process::Command; diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index 6d0956b39..319b36048 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -14,7 +14,8 @@ use nix::errno::Errno; #[cfg(unix)] use nix::sys::signal::{ - SigHandler::SigDfl, SigHandler::SigIgn, Signal::SIGINT, Signal::SIGPIPE, signal, + SaFlags, SigAction, SigHandler, SigHandler::SigDfl, SigHandler::SigIgn, SigSet, Signal, + Signal::SIGINT, Signal::SIGPIPE, sigaction, signal, }; /// The default signal value. @@ -435,6 +436,21 @@ pub fn ignore_interrupts() -> Result<(), Errno> { unsafe { signal(SIGINT, SigIgn) }.map(|_| ()) } +/// Installs a signal handler. The handler must be async-signal-safe. +#[cfg(unix)] +pub fn install_signal_handler( + sig: Signal, + handler: extern "C" fn(std::os::raw::c_int), +) -> Result<(), Errno> { + let action = SigAction::new( + SigHandler::Handler(handler), + SaFlags::SA_RESTART, + SigSet::empty(), + ); + unsafe { sigaction(sig, &action) }?; + Ok(()) +} + // Detect closed stdin/stdout before Rust reopens them as /dev/null (see issue #2873) #[cfg(unix)] use std::sync::atomic::{AtomicBool, Ordering}; diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index 5272ab3c1..279643a96 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -59,9 +59,6 @@ impl DigestOutput { } pub trait Digest { - fn new() -> Self - where - Self: Sized; fn hash_update(&mut self, input: &[u8]); fn hash_finalize(&mut self, out: &mut [u8]); fn reset(&mut self); @@ -79,31 +76,40 @@ pub trait Digest { /// first element of the tuple is the blake2b state /// second is the number of output bits -pub struct Blake2b(blake2b_simd::State, usize); +pub struct Blake2b { + digest: blake2b_simd::State, + bit_size: usize, +} impl Blake2b { + pub const DEFAULT_BYTE_SIZE: usize = 64; + /// Return a new Blake2b instance with a custom output bytes length pub fn with_output_bytes(output_bytes: usize) -> Self { let mut params = blake2b_simd::Params::new(); params.hash_length(output_bytes); let state = params.to_state(); - Self(state, output_bytes * 8) + Self { + digest: state, + bit_size: output_bytes * 8, + } + } +} + +impl Default for Blake2b { + fn default() -> Self { + Self::with_output_bytes(Self::DEFAULT_BYTE_SIZE) } } impl Digest for Blake2b { - fn new() -> Self { - // by default, Blake2b output is 512 bits long (= 64B) - Self::with_output_bytes(64) - } - fn hash_update(&mut self, input: &[u8]) { - self.0.update(input); + self.digest.update(input); } fn hash_finalize(&mut self, out: &mut [u8]) { - let hash_result = &self.0.finalize(); + let hash_result = &self.digest.finalize(); out.copy_from_slice(hash_result.as_bytes()); } @@ -112,16 +118,14 @@ impl Digest for Blake2b { } fn output_bits(&self) -> usize { - self.1 + self.bit_size } } +#[derive(Default)] pub struct Blake3(blake3::Hasher); -impl Digest for Blake3 { - fn new() -> Self { - Self(blake3::Hasher::new()) - } +impl Digest for Blake3 { fn hash_update(&mut self, input: &[u8]) { self.0.update(input); } @@ -132,7 +136,7 @@ impl Digest for Blake3 { } fn reset(&mut self) { - *self = Self::new(); + *self = Self::default(); } fn output_bits(&self) -> usize { @@ -140,12 +144,10 @@ impl Digest for Blake3 { } } +#[derive(Default)] pub struct Sm3(sm3::Sm3); -impl Digest for Sm3 { - fn new() -> Self { - Self(::new()) - } +impl Digest for Sm3 { fn hash_update(&mut self, input: &[u8]) { ::update(&mut self.0, input); } @@ -155,7 +157,7 @@ impl Digest for Sm3 { } fn reset(&mut self) { - *self = Self::new(); + *self = Self::default(); } fn output_bits(&self) -> usize { @@ -184,14 +186,16 @@ impl Crc { } } -impl Digest for Crc { - fn new() -> Self { +impl Default for Crc { + fn default() -> Self { Self { digest: crc_fast::Digest::new_with_params(Self::get_posix_cksum_params()), size: 0, } } +} +impl Digest for Crc { fn hash_update(&mut self, input: &[u8]) { self.digest.update(input); self.size += input.len(); @@ -230,13 +234,15 @@ pub struct CRC32B { digest: crc_fast::Digest, } -impl Digest for CRC32B { - fn new() -> Self { +impl Default for CRC32B { + fn default() -> Self { Self { digest: crc_fast::Digest::new(crc_fast::CrcAlgorithm::Crc32IsoHdlc), } } +} +impl Digest for CRC32B { fn hash_update(&mut self, input: &[u8]) { self.digest.update(input); } @@ -264,14 +270,12 @@ impl Digest for CRC32B { } } +#[derive(Default)] pub struct Bsd { state: u16, } -impl Digest for Bsd { - fn new() -> Self { - Self { state: 0 } - } +impl Digest for Bsd { fn hash_update(&mut self, input: &[u8]) { for &byte in input { self.state = (self.state >> 1) + ((self.state & 1) << 15); @@ -290,7 +294,7 @@ impl Digest for Bsd { } fn reset(&mut self) { - *self = Self::new(); + *self = Self::default(); } fn output_bits(&self) -> usize { @@ -298,14 +302,12 @@ impl Digest for Bsd { } } +#[derive(Default)] pub struct SysV { state: u32, } -impl Digest for SysV { - fn new() -> Self { - Self { state: 0 } - } +impl Digest for SysV { fn hash_update(&mut self, input: &[u8]) { for &byte in input { self.state = self.state.wrapping_add(u32::from(byte)); @@ -325,7 +327,7 @@ impl Digest for SysV { } fn reset(&mut self) { - *self = Self::new(); + *self = Self::default(); } fn output_bits(&self) -> usize { @@ -336,11 +338,12 @@ impl Digest for SysV { // Implements the Digest trait for sha2 / sha3 algorithms with fixed output macro_rules! impl_digest_common { ($algo_type: ty, $size: literal) => { - impl Digest for $algo_type { - fn new() -> Self { + impl Default for $algo_type { + fn default() -> Self { Self(Default::default()) } - + } + impl Digest for $algo_type { fn hash_update(&mut self, input: &[u8]) { digest::Digest::update(&mut self.0, input); } @@ -350,7 +353,7 @@ macro_rules! impl_digest_common { } fn reset(&mut self) { - *self = Self::new(); + *self = Self::default(); } fn output_bits(&self) -> usize { @@ -362,26 +365,43 @@ macro_rules! impl_digest_common { // Implements the Digest trait for sha2 / sha3 algorithms with variable output macro_rules! impl_digest_shake { - ($algo_type: ty, $output_bits: literal) => { - impl Digest for $algo_type { - fn new() -> Self { - Self(Default::default()) - } + ($algo_type: ty, $default_output_bits: literal) => { + impl $algo_type { + pub const DEFAULT_BIT_SIZE: usize = $default_output_bits; + pub fn with_output_bits(bits: usize) -> Self { + Self { + digest: Default::default(), + bit_size: bits, + } + } + } + impl Default for $algo_type { + fn default() -> Self { + Self::with_output_bits(Self::DEFAULT_BIT_SIZE) + } + } + impl Digest for $algo_type { fn hash_update(&mut self, input: &[u8]) { - digest::Update::update(&mut self.0, input); + digest::Update::update(&mut self.digest, input); } fn hash_finalize(&mut self, out: &mut [u8]) { - digest::ExtendableOutputReset::finalize_xof_reset_into(&mut self.0, out); + digest::ExtendableOutputReset::finalize_xof_reset_into(&mut self.digest, out); + + // Remove the last bits if the requested length is not a multiple of 8. + let extra = self.output_bits() % 8; + if extra != 0 { + out[out.len() - 1] &= (1 << extra) - 1; + } } fn reset(&mut self) { - *self = Self::new(); + *self = Self::with_output_bits(self.bit_size); } fn output_bits(&self) -> usize { - $output_bits + self.bit_size } fn result(&mut self) -> DigestOutput { @@ -415,8 +435,14 @@ impl_digest_common!(Sha3_256, 256); impl_digest_common!(Sha3_384, 384); impl_digest_common!(Sha3_512, 512); -pub struct Shake128(sha3::Shake128); -pub struct Shake256(sha3::Shake256); +pub struct Shake128 { + digest: sha3::Shake128, + bit_size: usize, +} +pub struct Shake256 { + digest: sha3::Shake256, + bit_size: usize, +} impl_digest_shake!(Shake128, 256); impl_digest_shake!(Shake256, 512); @@ -567,8 +593,8 @@ mod tests { #[test] fn test_crc_basic_functionality() { // Test that our CRC implementation works with basic functionality - let mut crc1 = Crc::new(); - let mut crc2 = Crc::new(); + let mut crc1 = Crc::default(); + let mut crc2 = Crc::default(); // Same input should give same output crc1.hash_update(b"test"); @@ -584,7 +610,7 @@ mod tests { #[test] fn test_crc_digest_basic() { - let mut crc = Crc::new(); + let mut crc = Crc::default(); // Test empty input let mut output = [0u8; 8]; @@ -592,7 +618,7 @@ mod tests { let empty_result = u64::from_ne_bytes(output); // Reset and test with "test" string - let mut crc = Crc::new(); + let mut crc = Crc::default(); crc.hash_update(b"test"); crc.hash_finalize(&mut output); let test_result = u64::from_ne_bytes(output); @@ -606,8 +632,8 @@ mod tests { #[test] fn test_crc_digest_incremental() { - let mut crc1 = Crc::new(); - let mut crc2 = Crc::new(); + let mut crc1 = Crc::default(); + let mut crc2 = Crc::default(); // Test that processing in chunks gives same result as all at once let data = b"Hello, World! This is a test string for CRC computation."; @@ -632,13 +658,13 @@ mod tests { // Test that our optimized slice-by-8 gives same results as byte-by-byte let test_data = b"This is a longer test string to verify slice-by-8 optimization works correctly with various data sizes including remainders."; - let mut crc_optimized = Crc::new(); + let mut crc_optimized = Crc::default(); crc_optimized.hash_update(test_data); let mut output_opt = [0u8; 8]; crc_optimized.hash_finalize(&mut output_opt); // Create a reference implementation using hash_update - let mut crc_reference = Crc::new(); + let mut crc_reference = Crc::default(); for &byte in test_data { crc_reference.hash_update(&[byte]); } @@ -659,7 +685,7 @@ mod tests { ]; for (input, expected) in test_cases { - let mut crc = Crc::new(); + let mut crc = Crc::default(); crc.hash_update(input.as_bytes()); let mut output = [0u8; 8]; crc.hash_finalize(&mut output); @@ -671,14 +697,14 @@ mod tests { #[test] fn test_crc_hash_update_edge_cases() { - let mut crc = Crc::new(); + let mut crc = Crc::default(); // Test with data that's not a multiple of 8 bytes let data7 = b"1234567"; // 7 bytes crc.hash_update(data7); let data9 = b"123456789"; // 9 bytes - let mut crc2 = Crc::new(); + let mut crc2 = Crc::default(); crc2.hash_update(data9); // Should not panic and should produce valid results diff --git a/src/uucore_procs/Cargo.toml b/src/uucore_procs/Cargo.toml index 956813592..a8c76441c 100644 --- a/src/uucore_procs/Cargo.toml +++ b/src/uucore_procs/Cargo.toml @@ -6,6 +6,7 @@ keywords = ["cross-platform", "proc-macros", "uucore", "uutils"] # categories = ["os"] authors.workspace = true edition.workspace = true +rust-version.workspace = true homepage.workspace = true license.workspace = true version.workspace = true diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index daa69047f..fac6ab679 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -4,6 +4,8 @@ // file that was distributed with this source code. // spell-checker:ignore (words) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb dont checkfile +use rstest::rstest; + use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::TestScenario; @@ -3098,3 +3100,129 @@ fn test_check_checkfile_with_io_error() { .stderr_contains("/proc/self/mem: read error") .no_stdout(); } + +#[rstest] +#[case::default_length( + &[], + "ac8549b2861a151896ab721bd29d7a20c1a3d1f75b31266f786f20d963fb0fdf" +)] +#[case::pass_default_length( + &["-l", "256"], + "ac8549b2861a151896ab721bd29d7a20c1a3d1f75b31266f786f20d963fb0fdf" +)] +#[case::smaller_length( + &["-l", "128"], + "ac8549b2861a151896ab721bd29d7a20" +)] +#[case::bigger_length( + &["-l", "264"], + "ac8549b2861a151896ab721bd29d7a20c1a3d1f75b31266f786f20d963fb0fdfc2" +)] +#[case::length_0( + &["-l", "0"], + "ac8549b2861a151896ab721bd29d7a20c1a3d1f75b31266f786f20d963fb0fdf" +)] +#[case::length_1( + &["-l", "1"], + "00" +)] +#[case::length_2( + &["-l", "2"], + "00" +)] +#[case::length_3( + &["-l", "3"], + "04" +)] +#[case::length_4( + &["-l", "4"], + "0c" +)] +#[case::length_5( + &["-l", "5"], + "0c" +)] +#[case::length_6( + &["-l", "6"], + "2c" +)] +#[case::length_7( + &["-l", "7"], + "2c" +)] +#[case::length_8( + &["-l", "8"], + "ac" +)] +fn test_shake128(#[case] args: &[&str], #[case] expected: &str) { + new_ucmd!() + .arg("-a") + .arg("shake128") + .args(args) + .pipe_in("xxx") + .succeeds() + .stdout_only(format!("SHAKE128 (-) = {expected}\n")); +} + +#[rstest] +#[case::default_length( + &[], + "2fa631503c3ea5fe85131dbfa24805185474740e6dcb5f2a64f69d932bcb55f7b24958f3e3c4cc0e71f1fe6f054cd3fb28b9efb62b4f8f3fbe6d50d90f5c6eba" +)] +#[case::pass_default_length( + &["-l", "512"], + "2fa631503c3ea5fe85131dbfa24805185474740e6dcb5f2a64f69d932bcb55f7b24958f3e3c4cc0e71f1fe6f054cd3fb28b9efb62b4f8f3fbe6d50d90f5c6eba" +)] +#[case::smaller_length( + &["-l", "128"], + "2fa631503c3ea5fe85131dbfa2480518" +)] +#[case::bigger_length( + &["-l", "1024"], + "2fa631503c3ea5fe85131dbfa24805185474740e6dcb5f2a64f69d932bcb55f7b24958f3e3c4cc0e71f1fe6f054cd3fb28b9efb62b4f8f3fbe6d50d90f5c6eba18783d25f8b36d92b8607f016352b5c405945a7859a8339201728f680647324d1b8ea93a01d2ef965dadf4a1bee3ff044ed2b4bd95e4311f5e3f2cd5bae0b7c6" +)] +#[case::length_0( + &["-l", "0"], + "2fa631503c3ea5fe85131dbfa24805185474740e6dcb5f2a64f69d932bcb55f7b24958f3e3c4cc0e71f1fe6f054cd3fb28b9efb62b4f8f3fbe6d50d90f5c6eba" +)] +#[case::length_1( + &["-l", "1"], + "01" +)] +#[case::length_2( + &["-l", "2"], + "03" +)] +#[case::length_3( + &["-l", "3"], + "07" +)] +#[case::length_4( + &["-l", "4"], + "0f" +)] +#[case::length_5( + &["-l", "5"], + "0f" +)] +#[case::length_6( + &["-l", "6"], + "2f" +)] +#[case::length_7( + &["-l", "7"], + "2f" +)] +#[case::length_8( + &["-l", "8"], + "2f" +)] +fn test_shake256(#[case] args: &[&str], #[case] expected: &str) { + new_ucmd!() + .arg("-a") + .arg("shake256") + .args(args) + .pipe_in("xxx") + .succeeds() + .stdout_only(format!("SHAKE256 (-) = {expected}\n")); +} diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index f01619af2..b49f4606c 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -6500,8 +6500,7 @@ fn test_cp_archive_preserves_directory_permissions() { assert_eq!( mode & 0o777, 0o755, - "Directory {} has incorrect permissions: {:o}", - path, + "Directory {path} has incorrect permissions: {:o}", mode & 0o777 ); }; @@ -7571,3 +7570,104 @@ fn test_cp_xattr_enotsup_handling() { std::fs::remove_file(f).ok(); } } + +#[test] +#[cfg(not(target_os = "windows"))] +fn test_cp_preserve_directory_permissions_by_default() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let dir = "a/b/c/d"; + let file = "foo.txt"; + + at.mkdir_all(dir); + + let file_path = format!("{dir}/{file}"); + + at.touch(file_path); + + scene.cmd("chmod").arg("-R").arg("555").arg("a").succeeds(); + scene.cmd("cp").arg("-r").arg("a").arg("b").succeeds(); + + scene.ucmd().arg("-r").arg("a").arg("c").succeeds(); + + // only verify owner bits on Android + if cfg!(target_os = "android") { + assert_eq!(at.metadata("b").mode() & 0o700, 0o500); + assert_eq!(at.metadata("b/b").mode() & 0o700, 0o500); + assert_eq!(at.metadata("b/b/c").mode() & 0o700, 0o500); + assert_eq!(at.metadata("b/b/c/d").mode() & 0o700, 0o500); + + assert_eq!(at.metadata("c").mode() & 0o700, 0o500); + assert_eq!(at.metadata("c/b").mode() & 0o700, 0o500); + assert_eq!(at.metadata("c/b/c").mode() & 0o700, 0o500); + assert_eq!(at.metadata("c/b/c/d").mode() & 0o700, 0o500); + } else { + assert_eq!(at.metadata("b").mode(), 0o40555); + assert_eq!(at.metadata("b/b").mode(), 0o40555); + assert_eq!(at.metadata("b/b/c").mode(), 0o40555); + assert_eq!(at.metadata("b/b/c/d").mode(), 0o40555); + + assert_eq!(at.metadata("c").mode(), 0o40555); + assert_eq!(at.metadata("c/b").mode(), 0o40555); + assert_eq!(at.metadata("c/b/c").mode(), 0o40555); + assert_eq!(at.metadata("c/b/c/d").mode(), 0o40555); + } +} + +#[test] +#[cfg(not(target_os = "windows"))] +fn test_cp_existing_perm_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + scene + .cmd("mkdir") + .arg("-p") + .arg("-m") + .arg("ug-s,u=rwx,g=rwx,o=rx") + .arg("src/dir") + .umask(0o022) + .succeeds(); + scene + .cmd("mkdir") + .arg("-p") + .arg("-m") + .arg("ug-s,u=rwx,g=,o=") + .arg("dst/dir") + .umask(0o022) + .succeeds(); + + scene.ucmd().arg("-r").arg("src/.").arg("dst/").succeeds(); + + let mode = at.metadata("dst/dir").mode(); + + assert_eq!(mode, 0o40700); +} + +#[test] +#[cfg(not(target_os = "windows"))] +fn test_cp_gnu_preserve_mode() { + use std::io; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + scene.cmd("mkdir").arg("d1").succeeds(); + scene.cmd("mkdir").arg("d2").succeeds(); + scene.cmd("chmod").arg("705").arg("d2").succeeds(); + + scene + .ucmd() + .arg("--no-preserve=mode") + .arg("-r") + .arg("d2") + .arg("d3") + .set_stdout(io::stdout()) + .succeeds(); + + let d1_mode = at.metadata("d1").mode(); + let d3_mode = at.metadata("d3").mode(); + + assert_eq!(d1_mode, d3_mode); +} diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 923922f2b..7a5625a01 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -419,8 +419,7 @@ fn test_date_set_hyphen_prefixed_values() { // permission error, not argument parsing error assert!( result.stderr_str().starts_with("date: cannot set date: "), - "Expected permission error for '{}', but got: {}", - date_str, + "Expected permission error for '{date_str}', but got: {}", result.stderr_str() ); } diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 2be6ef02c..2b3f54302 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -1782,7 +1782,7 @@ fn test_du_long_path_safe_traversal() { at.mkdir(&deep_path); for i in 0..15 { - let long_dir_name = format!("{}{}", "a".repeat(100), i); + let long_dir_name = format!("{}{i}", "a".repeat(100)); deep_path = format!("{deep_path}/{long_dir_name}"); at.mkdir_all(&deep_path); } @@ -1830,7 +1830,7 @@ fn test_du_safe_traversal_with_symlinks() { at.mkdir(&deep_path); for i in 0..8 { - let dir_name = format!("{}{}", "b".repeat(50), i); + let dir_name = format!("{}{i}", "b".repeat(50)); deep_path = format!("{deep_path}/{dir_name}"); at.mkdir_all(&deep_path); } @@ -2064,24 +2064,45 @@ fn test_block_size_args_override() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; let dir = "override_args_dir"; + let nested_dir = "override_args_dir/nested_dir"; + let nested_dir_2 = "override_args_dir/nested_dir_2"; - at.mkdir(dir); - let fpath = at.plus(format!("{dir}/file")); + at.mkdir_all(nested_dir); + at.mkdir_all(nested_dir_2); + let fpath = at.plus(format!("{nested_dir}/file")); std::fs::File::create(fpath) .expect("cannot create test file") .set_len(100_000_000) .expect("cannot set file size"); - let fpath2 = at.plus(format!("{dir}/file_2")); + let fpath2 = at.plus(format!("{nested_dir}/file_2")); std::fs::File::create(fpath2) .expect("cannot create test file") .set_len(100_000_000) .expect("cannot set file size"); + let fpath = at.plus(format!("{nested_dir_2}/file_3")); + std::fs::File::create(fpath) + .expect("cannot create test file") + .set_len(100_000) + .expect("cannot set file size"); + + let fpath2 = at.plus(format!("{nested_dir_2}/file_4")); + std::fs::File::create(fpath2) + .expect("cannot create test file") + .set_len(100) + .expect("cannot set file size"); + let test_cases = [ - (["-sk", "-m"], "-sm"), - (["-sk", "-b"], "-sb"), - (["-sm", "-k"], "-sk"), + (["-sk", "-m"], vec!["-sm"]), + (["-sk", "-b"], vec!["-sb"]), + (["-sm", "-k"], vec!["-sk"]), + (["-sk", "--si"], vec!["-s", "--si"]), + (["-sk", "-h"], vec!["-s", "-h"]), + (["-sm", "--block-size=128"], vec!["-s", "--block-size=128"]), + (["--block-size=128", "-b"], vec!["-b"]), + (["--si", "-b"], vec!["-b"]), + (["-h", "-b"], vec!["-b"]), ]; for (idx, (overwriting_args, expected)) in test_cases.into_iter().enumerate() { @@ -2095,7 +2116,7 @@ fn test_block_size_args_override() { let single_args = ts .ucmd() .arg(dir) - .arg(expected) + .args(&expected) .succeeds() .stdout_move_str(); @@ -2111,23 +2132,30 @@ fn test_block_override_b_still_has_apparent_size() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; let dir = "override_args_dir"; + let nested_dir = "override_args_dir/nested_dir"; - at.mkdir(dir); - let fpath = at.plus(format!("{dir}/file")); + at.mkdir_all(nested_dir); + let fpath = at.plus(format!("{nested_dir}/file")); std::fs::File::create(fpath) .expect("cannot create test file") .set_len(100_000_000) .expect("cannot set file size"); - let fpath2 = at.plus(format!("{dir}/file_2")); + let fpath2 = at.plus(format!("{nested_dir}/file_2")); std::fs::File::create(fpath2) .expect("cannot create test file") .set_len(100_000_000) .expect("cannot set file size"); let test_cases = [ - (["-sb", "-m"], ["-sm", "--apparent-size"]), - (["-sb", "-k"], ["-sk", "--apparent-size"]), + (["-b", "-m"], ["-m", "--apparent-size"]), + (["-b", "-k"], ["-k", "--apparent-size"]), + (["-b", "--si"], ["--si", "--apparent-size"]), + (["-b", "-h"], ["-h", "--apparent-size"]), + ( + ["-b", "--block-size=128"], + ["--block-size=128", "--apparent-size"], + ), ]; for (idx, (overwriting_args, expected)) in test_cases.into_iter().enumerate() { @@ -2151,3 +2179,27 @@ fn test_block_override_b_still_has_apparent_size() { ); } } + +#[test] +fn test_overriding_block_size_arg_with_invalid_value_still_errors() { + new_ucmd!() + .args(&["--block-size=abc", "-m"]) + .fails_with_code(1) + .stderr_contains("invalid --block-size argument 'abc'"); + new_ucmd!() + .args(&["--block-size=abc", "-k"]) + .fails_with_code(1) + .stderr_contains("invalid --block-size argument 'abc'"); + new_ucmd!() + .args(&["--block-size=abc", "-b"]) + .fails_with_code(1) + .stderr_contains("invalid --block-size argument 'abc'"); + new_ucmd!() + .args(&["--block-size=abc", "-h"]) + .fails_with_code(1) + .stderr_contains("invalid --block-size argument 'abc'"); + new_ucmd!() + .args(&["--block-size=abc", "--si"]) + .fails_with_code(1) + .stderr_contains("invalid --block-size argument 'abc'"); +} diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 8c488e9f3..074d29c29 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -1962,3 +1962,81 @@ fn test_non_utf8_env_vars() { .succeeds() .stdout_contains_bytes(b"NON_UTF8_VAR=hello\x80world"); } + +#[test] +#[cfg(unix)] +fn test_ignore_signal_pipe_broken_pipe_regression() { + // Test that --ignore-signal=PIPE properly ignores SIGPIPE in child processes. + // When SIGPIPE is ignored, processes should handle broken pipes gracefully + // instead of being terminated by the signal. + // + // Regression test for: https://github.com/uutils/coreutils/issues/9617 + + use std::io::{BufRead, BufReader}; + use std::process::{Command, Stdio}; + + let scene = TestScenario::new(util_name!()); + + // Helper function to simulate a broken pipe scenario (like "seq 1000000 | head -n1") + let test_sigpipe_behavior = |use_ignore_signal: bool| -> i32 { + let mut cmd = Command::new(&scene.bin_path); + cmd.arg("env"); + + if use_ignore_signal { + cmd.arg("--ignore-signal=PIPE"); + } + + // Use seq instead of yes - writes bounded output but enough to trigger SIGPIPE + cmd.arg("seq") + .arg("1") + .arg("1000000") + .stdout(Stdio::piped()) + .stderr(Stdio::null()); + + let mut child = cmd.spawn().expect("Failed to spawn env process"); + + // Read exactly one line then close the pipe to trigger SIGPIPE + if let Some(stdout) = child.stdout.take() { + let mut reader = BufReader::new(stdout); + let mut line = String::new(); + let _ = reader.read_line(&mut line); + // Pipe closes when reader is dropped, sending SIGPIPE to writing process + } + + // seq should exit quickly (either from SIGPIPE or after handling EPIPE) + match child.wait() { + Ok(status) => status.code().unwrap_or(141), // 128 + 13 + Err(_) => 141, + } + }; + + // Test without signal ignoring - should be killed by SIGPIPE + let normal_exit_code = test_sigpipe_behavior(false); + println!("Normal 'env seq' exit code: {normal_exit_code}"); + + // Test with --ignore-signal=PIPE - should handle broken pipe gracefully + let ignore_signal_exit_code = test_sigpipe_behavior(true); + println!("With --ignore-signal=PIPE exit code: {ignore_signal_exit_code}"); + + // Verify the --ignore-signal=PIPE flag changes the behavior + assert!( + ignore_signal_exit_code != 141, + "--ignore-signal=PIPE had no effect! Process was still killed by SIGPIPE (exit code 141). Normal: {normal_exit_code}, --ignore-signal: {ignore_signal_exit_code}" + ); + + // Expected behavior: + assert_eq!( + normal_exit_code, 141, + "Without --ignore-signal, process should be killed by SIGPIPE" + ); + assert_ne!( + ignore_signal_exit_code, 141, + "With --ignore-signal=PIPE, process should NOT be killed by SIGPIPE" + ); + + // Process should exit gracefully when SIGPIPE is ignored + assert!( + ignore_signal_exit_code == 0 || ignore_signal_exit_code == 1, + "With --ignore-signal=PIPE, process should exit gracefully (0 or 1), got: {ignore_signal_exit_code}" + ); +} diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 41b72af6b..37492d80a 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff dired subdired tmpfs mdir COLORTERM mexe bcdef mfoo timefile -// spell-checker:ignore (words) fakeroot setcap drwxr bcdlps mdangling mentry +// spell-checker:ignore (words) fakeroot setcap drwxr bcdlps mdangling mentry awith acolons #![allow( clippy::similar_names, clippy::too_many_lines, @@ -4785,8 +4785,8 @@ fn test_ls_selinux_context_indicator() { // The 11th character (0-indexed position 10) should be "." for SELinux context assert_eq!( chars[10], '.', - "Expected '.' indicator for SELinux context in position 11, got '{}' in line: {}", - chars[10], first_line + "Expected '.' indicator for SELinux context in position 11, got '{}' in line: {first_line}", + chars[10], ); } @@ -5701,19 +5701,15 @@ fn test_ls_hyperlink() { let result = scene.ucmd().arg("--hyperlink").succeeds(); assert!(result.stdout_str().contains("\x1b]8;;file://")); - assert!( - result - .stdout_str() - .contains(&format!("{path}{separator}{file}\x07{file}\x1b]8;;\x07")) - ); + assert!(result.stdout_str().contains(&format!( + "{path}{separator}{file}\x1b\\{file}\x1b]8;;\x1b\\" + ))); let result = scene.ucmd().arg("--hyperlink=always").succeeds(); assert!(result.stdout_str().contains("\x1b]8;;file://")); - assert!( - result - .stdout_str() - .contains(&format!("{path}{separator}{file}\x07{file}\x1b]8;;\x07")) - ); + assert!(result.stdout_str().contains(&format!( + "{path}{separator}{file}\x1b\\{file}\x1b]8;;\x1b\\" + ))); for argument in [ "--hyperlink=never", @@ -5748,23 +5744,23 @@ fn test_ls_hyperlink_encode_link() { assert!( result .stdout_str() - .contains("back%5cslash\x07back\\slash\x1b]8;;\x07") + .contains("back%5cslash\x1b\\back\\slash\x1b]8;;\x1b\\") ); assert!( result .stdout_str() - .contains("ques%3ftion\x07ques?tion\x1b]8;;\x07") + .contains("ques%3ftion\x1b\\ques?tion\x1b]8;;\x1b\\") ); } assert!( result .stdout_str() - .contains("encoded%253Fquestion\x07encoded%3Fquestion\x1b]8;;\x07") + .contains("encoded%253Fquestion\x1b\\encoded%3Fquestion\x1b]8;;\x1b\\") ); assert!( result .stdout_str() - .contains("sp%20ace\x07sp ace\x1b]8;;\x07") + .contains("sp%20ace\x1b\\sp ace\x1b]8;;\x1b\\") ); } // spell-checker: enable @@ -5796,7 +5792,9 @@ fn test_ls_hyperlink_dirs() { .lines() .next() .unwrap() - .contains(&format!("{path}{separator}{dir_a}\x07{dir_a}\x1b]8;;\x07:")) + .contains(&format!( + "{path}{separator}{dir_a}\x1b\\{dir_a}\x1b]8;;\x1b\\:" + )) ); assert_eq!(result.stdout_str().lines().nth(1).unwrap(), ""); assert!( @@ -5805,7 +5803,9 @@ fn test_ls_hyperlink_dirs() { .lines() .nth(2) .unwrap() - .contains(&format!("{path}{separator}{dir_b}\x07{dir_b}\x1b]8;;\x07:")) + .contains(&format!( + "{path}{separator}{dir_b}\x1b\\{dir_b}\x1b]8;;\x1b\\:" + )) ); } @@ -5837,21 +5837,95 @@ fn test_ls_hyperlink_recursive_dirs() { let mut lines = result.stdout_str().lines(); assert_hyperlink!( lines.next(), - &format!("{path}{separator}{dir_a}\x07{dir_a}\x1b]8;;\x07:") + &format!("{path}{separator}{dir_a}\x1b\\{dir_a}\x1b]8;;\x1b\\:") ); assert_hyperlink!( lines.next(), - &format!("{path}{separator}{dir_a}{separator}{dir_b}\x07{dir_b}\x1b]8;;\x07") + &format!("{path}{separator}{dir_a}{separator}{dir_b}\x1b\\{dir_b}\x1b]8;;\x1b\\") ); assert!(matches!(lines.next(), Some(l) if l.is_empty())); assert_hyperlink!( lines.next(), &format!( - "{path}{separator}{dir_a}{separator}{dir_b}\x07{dir_a}{separator}{dir_b}\x1b]8;;\x07:" + "{path}{separator}{dir_a}{separator}{dir_b}\x1b\\{dir_a}{separator}{dir_b}\x1b]8;;\x1b\\:" ) ); } +#[test] +fn test_ls_hyperlink_symlink_target_handling() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("target_dir"); + at.touch("target_file.txt"); + at.symlink_file("target_file.txt", "link_to_file"); + at.symlink_dir("target_dir", "link_to_dir"); + at.symlink_file("nonexistent", "link_to_missing"); + + let result = scene + .ucmd() + .args(&["-l", "--hyperlink", "--color"]) + .succeeds(); + let output = result.stdout_str(); + + assert!(output.contains("\x1b]8;;file://")); + assert!(!output.contains('\x07')); + assert!(output.contains("\x1b\\")); + + let file_link_line = output + .lines() + .find(|line| line.contains("link_to_file")) + .unwrap(); + assert!(file_link_line.contains(" -> ")); + let target_part = file_link_line.split(" -> ").nth(1).unwrap(); + assert!(!target_part.contains("\x1b[")); + assert!(!target_part.ends_with("\x1b[K")); + + let dir_link_line = output + .lines() + .find(|line| line.contains("link_to_dir")) + .unwrap(); + assert!(dir_link_line.contains(" -> ")); + let target_part = dir_link_line.split(" -> ").nth(1).unwrap(); + assert!(target_part.contains("\x1b[")); + + let missing_link_line = output + .lines() + .find(|line| line.contains("link_to_missing")) + .unwrap(); + assert!(missing_link_line.contains(" -> ")); + let missing_target_part = missing_link_line.split(" -> ").nth(1).unwrap(); + assert!(missing_target_part.contains("\x1b[")); +} + +#[test] +fn test_ls_hyperlink_utf8_encoding() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("café.txt"); + #[cfg(not(target_os = "windows"))] + at.touch("file:with:colons.txt"); + #[cfg(target_os = "windows")] + at.touch("file-with-colons.txt"); + at.touch("file with spaces.txt"); + + let result = scene.ucmd().args(&["--hyperlink"]).succeeds(); + let output = result.stdout_str(); + + assert!(output.contains("caf%c3%a9.txt")); + #[cfg(not(target_os = "windows"))] + assert!(output.contains("file%3awith%3acolons.txt")); + #[cfg(target_os = "windows")] + assert!(output.contains("file-with-colons.txt")); + assert!(output.contains("file%20with%20spaces.txt")); + + let hyperlink_count = output.matches("\x1b]8;;file://").count(); + let terminator_count = output.matches("\x1b\\").count(); + assert_eq!(terminator_count, hyperlink_count * 2); +} + #[test] fn test_ls_color_do_not_reset() { let scene: TestScenario = TestScenario::new(util_name!()); diff --git a/tests/by-util/test_mkdir.rs b/tests/by-util/test_mkdir.rs index 0756cb5d6..231fe3495 100644 --- a/tests/by-util/test_mkdir.rs +++ b/tests/by-util/test_mkdir.rs @@ -468,9 +468,7 @@ fn test_selinux() { let context_value = get_getfattr_output(&at.plus_as_string(dest)); assert!( context_value.contains("unconfined_u"), - "Expected '{}' not found in getfattr output:\n{}", - "unconfined_u", - context_value + "Expected 'unconfined_u' not found in getfattr output:\n{context_value}", ); at.rmdir(dest); } diff --git a/tests/by-util/test_mktemp.rs b/tests/by-util/test_mktemp.rs index 306f147d8..a74cb461a 100644 --- a/tests/by-util/test_mktemp.rs +++ b/tests/by-util/test_mktemp.rs @@ -1139,6 +1139,26 @@ fn test_non_utf8_tmpdir_long_option() { .succeeds(); } +#[test] +#[cfg(target_os = "linux")] +fn test_invalid_utf8_suffix() { + use std::os::unix::ffi::OsStrExt; + let (at, mut ucmd) = at_and_ucmd!(); + + // Create invalid UTF-8 bytes for suffix + // This mimics the GNU test which tests mktemp with bad unicode characters + let invalid_utf8 = std::ffi::OsStr::from_bytes(b"\xC3|\xED\xBA\xAD"); + + // Test that mktemp handles invalid UTF-8 in suffix gracefully + // It should succeed and create a file with the lossy conversion of the invalid UTF-8 + ucmd.arg("-p") + .arg(at.as_string()) + .arg("--suffix") + .arg(invalid_utf8) + .arg("tmpXXXXXX") + .succeeds(); +} + #[test] #[cfg(target_os = "linux")] fn test_non_utf8_tmpdir_directory_creation() { diff --git a/tests/by-util/test_paste.rs b/tests/by-util/test_paste.rs index a87f21598..11767e82a 100644 --- a/tests/by-util/test_paste.rs +++ b/tests/by-util/test_paste.rs @@ -135,6 +135,30 @@ const EXAMPLE_DATA: &[TestData] = &[ ins: &["1 \na \n", "2\t\nb\t\n"], out: "1 |2\t\na |b\t\n", }, + TestData { + name: "utf8-2byte-delim", + args: &["-d", "\u{00A2}"], + ins: &["1\n2\n", "a\nb\n"], + out: "1\u{00A2}a\n2\u{00A2}b\n", + }, + TestData { + name: "utf8-3byte-delim", + args: &["-d", "\u{20AC}"], + ins: &["1\n2\n", "a\nb\n"], + out: "1\u{20AC}a\n2\u{20AC}b\n", + }, + TestData { + name: "utf8-4byte-delim", + args: &["-d", "\u{1F600}", "-s"], + ins: &["1\n2\n3\n"], + out: "1\u{1F600}2\u{1F600}3\n", + }, + TestData { + name: "utf8-multi-delim-cycle", + args: &["-d", "\u{00A2}\u{20AC}"], + ins: &["a\nb\nc\n", "1\n2\n3\n", "x\ny\nz\n"], + out: "a\u{00A2}1\u{20AC}x\nb\u{00A2}2\u{20AC}y\nc\u{00A2}3\u{20AC}z\n", + }, ]; #[test] @@ -334,6 +358,19 @@ fn test_backslash_zero_delimiter() { } } +#[test] +fn test_gnu_escape_sequences() { + let cases: &[(&str, u8)] = &[(r"\b", 0x08), (r"\f", 0x0C), (r"\r", 0x0D), (r"\v", 0x0B)]; + for &(esc, byte) in cases { + let expected = [b'1', byte, b'2', byte, b'3', b'\n']; + new_ucmd!() + .args(&["-s", "-d", esc]) + .pipe_in("1\n2\n3\n") + .succeeds() + .stdout_only_bytes(expected); + } +} + // As of 2024-10-09, only bsdutils (https://github.com/dcantrell/bsdutils, derived from FreeBSD) and toybox handle // multibyte delimiter characters in the way a user would likely expect. BusyBox and GNU Core Utilities do not. #[test] @@ -378,6 +415,21 @@ fn test_data() { } } +#[test] +#[cfg(target_os = "linux")] +fn test_non_utf8_delimiter() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("f1", "1\n2\n"); + at.write("f2", "a\nb\n"); + let delim = std::ffi::OsString::from_vec(vec![0xA2, 0xE3]); + ucmd.env("LC_ALL", "zh_CN.gb18030") + .arg("-d") + .arg(&delim) + .args(&["f1", "f2"]) + .succeeds() + .stdout_only_bytes(b"1\xA2\xE3a\n2\xA2\xE3b\n"); +} + #[test] #[cfg(target_os = "linux")] fn test_paste_non_utf8_paths() { diff --git a/tests/by-util/test_printf.rs b/tests/by-util/test_printf.rs index 6afe0330c..81427fae5 100644 --- a/tests/by-util/test_printf.rs +++ b/tests/by-util/test_printf.rs @@ -1492,3 +1492,16 @@ fn test_extreme_field_width_overflow() { .fails_with_code(1) .stderr_contains("printf: write error"); //could contains additional message like "formatting width too large" not in GNU, thats fine. } + +#[test] +fn test_q_string_control_chars_with_quotes() { + // Test %q with control characters and single quotes combined. + // This tests the fix for the GNU compatibility issue where control + // characters combined with single quotes should use the segmented + // quoting approach rather than double quotes. + let input = "\x01'\x01"; + new_ucmd!() + .args(&["%q", input]) + .succeeds() + .stdout_only("''$'\\001'\\'''$'\\001'"); +} diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index c303a4bab..3d2f1df5e 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -1103,7 +1103,7 @@ fn test_rm_recursive_long_path_safe_traversal() { at.mkdir(&deep_path); for i in 0..12 { - let long_dir_name = format!("{}{}", "z".repeat(80), i); + let long_dir_name = format!("{}{i}", "z".repeat(80)); deep_path = format!("{deep_path}/{long_dir_name}"); at.mkdir_all(&deep_path); } diff --git a/tests/by-util/test_stdbuf.rs b/tests/by-util/test_stdbuf.rs index 00e117f47..0b60ef464 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -25,20 +25,100 @@ fn test_permission() { .stderr_contains("Permission denied"); } -// TODO: Tests below are brittle when feat_external_libstdbuf is enabled and libstdbuf is not installed. -// Align stdbuf with GNU search order to enable deterministic testing without installation: -// 1) search for libstdbuf next to the stdbuf binary, 2) then in LIBSTDBUF_DIR, 3) then system locations. -// After implementing this, rework tests to provide a temporary symlink rather than depending on system state. - -#[cfg(feature = "feat_external_libstdbuf")] +// LD_DEBUG is not available on macOS, OpenBSD, Android, or musl +#[cfg(all( + feature = "feat_external_libstdbuf", + not(target_os = "windows"), + not(target_os = "openbsd"), + not(target_os = "macos"), + not(target_os = "android"), + not(target_env = "musl") +))] #[test] -fn test_permission_external_missing_lib() { - // When built with external libstdbuf, running stdbuf fails early if lib is not installed - new_ucmd!() - .arg("-o1") - .arg(".") - .fails_with_code(1) - .stderr_contains("External libstdbuf not found"); +fn test_stdbuf_search_order_exe_dir_first() { + use std::fs; + use std::os::unix::fs::PermissionsExt; + + // Test that stdbuf searches for libstdbuf in its own directory first, + // before checking LIBSTDBUF_DIR. + let ts = TestScenario::new(util_name!()); + let temp_dir = tempfile::tempdir().unwrap(); + let temp_path = temp_dir.path(); + + // Determine the correct library extension for this platform + let lib_extension = if cfg!(target_vendor = "apple") { + "dylib" + } else { + "so" + }; + let lib_name = format!("libstdbuf.{lib_extension}"); + + // Look for libstdbuf in the build directory deps folder + // During build, libstdbuf.so is in target/debug/deps/ or target/release/deps/ + // This allows running tests without requiring installation to a root-owned path + // ts.bin_path is the path to the binary file, so we get its parent directory first + let source_lib = ts + .bin_path + .parent() + .expect("Binary should have a parent directory") + .join("deps") + .join(&lib_name); + + // Fail test if the library doesn't exist - it should have been built + assert!( + source_lib.exists(), + "libstdbuf not found at {}. It should have been built.", + source_lib.display() + ); + + // Copy stdbuf binary to temp directory + // ts.bin_path is the full path to the coreutils binary + let stdbuf_copy = temp_path.join("stdbuf"); + fs::copy(&ts.bin_path, &stdbuf_copy).unwrap(); + + // Make the copied binary executable + let mut perms = fs::metadata(&stdbuf_copy).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&stdbuf_copy, perms).unwrap(); + + // Copy libstdbuf to the same directory as stdbuf + let lib_copy = temp_path.join(&lib_name); + fs::copy(&source_lib, &lib_copy).unwrap(); + + // Run the copied stdbuf with LD_DEBUG to verify it loads the local libstdbuf + // This proves the exe-dir search happens first, before checking LIBSTDBUF_DIR + let output = std::process::Command::new(&stdbuf_copy) + .env("LD_DEBUG", "libs") + .args(["-o0", "echo", "test_output"]) + .output() + .unwrap(); + + let stderr = String::from_utf8_lossy(&output.stderr); + + // Verify the library was loaded from the temp directory (same dir as exe) + // LD_DEBUG output will show something like: + // " trying file=/tmp/.../libstdbuf.so" + let temp_dir_str = temp_path.to_string_lossy(); + let loaded_from_exe_dir = stderr + .lines() + .any(|line| line.contains(&*lib_name) && line.contains(&*temp_dir_str)); + + assert!( + loaded_from_exe_dir, + "libstdbuf should be loaded from exe directory ({}), not from LIBSTDBUF_DIR. LD_DEBUG output:\n{stderr}", + temp_path.display() + ); + + // The command should succeed and produce the expected output + assert!( + output.status.success(), + "stdbuf should succeed when libstdbuf is in the same directory. stderr: {stderr}" + ); + assert_eq!( + String::from_utf8_lossy(&output.stdout).trim(), + "test_output", + "stdbuf should execute echo successfully" + ); } #[cfg(not(feature = "feat_external_libstdbuf"))] @@ -51,17 +131,6 @@ fn test_no_such() { .stderr_contains("No such file or directory"); } -#[cfg(feature = "feat_external_libstdbuf")] -#[test] -fn test_no_such_external_missing_lib() { - // With external lib mode and missing installation, stdbuf fails before spawning the command - new_ucmd!() - .arg("-o1") - .arg("no_such") - .fails_with_code(1) - .stderr_contains("External libstdbuf not found"); -} - // Disabled on x86_64-unknown-linux-musl because the cross-rs Docker image for this target // does not provide musl-compiled system utilities (like head), leading to dynamic linker errors // when preloading musl-compiled libstdbuf.so into glibc-compiled binaries. Same thing for FreeBSD. diff --git a/tests/by-util/test_sync.rs b/tests/by-util/test_sync.rs index 9c3df3a1e..8c09d8637 100644 --- a/tests/by-util/test_sync.rs +++ b/tests/by-util/test_sync.rs @@ -167,3 +167,32 @@ fn test_sync_multiple_files() { // Sync both files new_ucmd!().arg("--data").arg(&file1).arg(&file2).succeeds(); } + +#[test] +fn test_sync_multiple_nonexistent_files() { + let result = new_ucmd!().arg("--data").arg("bad1").arg("bad2").fails(); + + result.stderr_contains("sync: error opening 'bad1': No such file or directory"); + result.stderr_contains("sync: error opening 'bad2': No such file or directory"); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_sync_data_fifo_fails_immediately() { + use std::time::Duration; + use uutests::util::TestScenario; + use uutests::util_name; + + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.mkfifo("test-fifo"); + + ts.ucmd() + .arg("--data") + .arg(at.plus_as_string("test-fifo")) + .timeout(Duration::from_secs(2)) + .fails() + .stderr_contains("error syncing") + .stderr_contains("test-fifo") + .stderr_contains("Invalid input"); +} diff --git a/tests/by-util/test_tac.rs b/tests/by-util/test_tac.rs index 1fc42d1c8..dee019677 100644 --- a/tests/by-util/test_tac.rs +++ b/tests/by-util/test_tac.rs @@ -29,8 +29,6 @@ fn test_invalid_arg() { } #[test] -// FIXME: See https://github.com/uutils/coreutils/issues/4204 -#[cfg(not(windows))] fn test_stdin_default() { new_ucmd!() .pipe_in("100\n200\n300\n400\n500") @@ -39,8 +37,6 @@ fn test_stdin_default() { } #[test] -// FIXME: See https://github.com/uutils/coreutils/issues/4204 -#[cfg(not(windows))] fn test_stdin_non_newline_separator() { new_ucmd!() .args(&["-s", ":"]) @@ -50,8 +46,6 @@ fn test_stdin_non_newline_separator() { } #[test] -// FIXME: See https://github.com/uutils/coreutils/issues/4204 -#[cfg(not(windows))] fn test_stdin_non_newline_separator_before() { new_ucmd!() .args(&["-b", "-s", ":"]) @@ -104,14 +98,11 @@ fn test_invalid_input() { } #[test] -#[cfg(not(windows))] // FIXME: https://github.com/uutils/coreutils/issues/4204 fn test_no_line_separators() { new_ucmd!().pipe_in("a").succeeds().stdout_is("a"); } #[test] -// FIXME: See https://github.com/uutils/coreutils/issues/4204 -#[cfg(not(windows))] fn test_before_trailing_separator_no_leading_separator() { new_ucmd!() .arg("-b") @@ -121,8 +112,6 @@ fn test_before_trailing_separator_no_leading_separator() { } #[test] -// FIXME: See https://github.com/uutils/coreutils/issues/4204 -#[cfg(not(windows))] fn test_before_trailing_separator_and_leading_separator() { new_ucmd!() .arg("-b") @@ -132,8 +121,6 @@ fn test_before_trailing_separator_and_leading_separator() { } #[test] -// FIXME: See https://github.com/uutils/coreutils/issues/4204 -#[cfg(not(windows))] fn test_before_leading_separator_no_trailing_separator() { new_ucmd!() .arg("-b") @@ -143,8 +130,6 @@ fn test_before_leading_separator_no_trailing_separator() { } #[test] -// FIXME: See https://github.com/uutils/coreutils/issues/4204 -#[cfg(not(windows))] fn test_before_no_separator() { new_ucmd!() .arg("-b") @@ -154,15 +139,11 @@ fn test_before_no_separator() { } #[test] -// FIXME: See https://github.com/uutils/coreutils/issues/4204 -#[cfg(not(windows))] fn test_before_empty_file() { new_ucmd!().arg("-b").pipe_in("").succeeds().stdout_is(""); } #[test] -// FIXME: See https://github.com/uutils/coreutils/issues/4204 -#[cfg(not(windows))] fn test_multi_char_separator() { new_ucmd!() .args(&["-s", "xx"]) @@ -204,8 +185,6 @@ fn test_multi_char_separator_overlap() { } #[test] -// FIXME: See https://github.com/uutils/coreutils/issues/4204 -#[cfg(not(windows))] fn test_multi_char_separator_overlap_before() { // With the "-b" option, the line separator is assumed to be at the // beginning of the line. In this case, That is, "axxx" is @@ -248,8 +227,6 @@ fn test_multi_char_separator_overlap_before() { } #[test] -// FIXME: See https://github.com/uutils/coreutils/issues/4204 -#[cfg(not(windows))] fn test_null_separator() { new_ucmd!() .args(&["-s", ""]) @@ -259,8 +236,6 @@ fn test_null_separator() { } #[test] -// FIXME: See https://github.com/uutils/coreutils/issues/4204 -#[cfg(not(windows))] fn test_regex() { new_ucmd!() .args(&["-r", "-s", "[xyz]+"]) @@ -289,8 +264,6 @@ fn test_regex() { } #[test] -// FIXME: See https://github.com/uutils/coreutils/issues/4204 -#[cfg(not(windows))] fn test_regex_before() { new_ucmd!() .args(&["-b", "-r", "-s", "[xyz]+"]) diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs index 491fd64ee..4a7c266b9 100644 --- a/tests/by-util/test_tr.rs +++ b/tests/by-util/test_tr.rs @@ -1544,6 +1544,30 @@ fn test_non_digit_repeat() { .stderr_only("tr: invalid repeat count 'c' in [c*n] construct\n"); } +#[test] +#[cfg(unix)] +fn test_octal_escape_ambiguous_followed_by_non_utf8() { + // This case does not trigger the panic + let set1 = OsStr::from_bytes(b"\\501a"); + new_ucmd!() + .arg("-d") + .arg(set1) + .pipe_in("(1a)") + .succeeds() + .stderr_contains("warning: the ambiguous octal escape") + .stdout_is(")"); + + // An user is not supposed to use this invalid utf8 set but + // we would need to make the command more error-proof still + let set1 = OsStr::from_bytes(b"\\501\xff"); + new_ucmd!() + .arg("-d") + .arg(set1) + .pipe_in([b'(', b'1', 0xff, b')']) + .succeeds() + .stderr_contains("warning: invalid utf8 sequence"); +} + #[cfg(target_os = "linux")] #[test] fn test_failed_write_is_reported() { diff --git a/tests/by-util/test_unexpand.rs b/tests/by-util/test_unexpand.rs index d29fecfd3..c6d481f53 100644 --- a/tests/by-util/test_unexpand.rs +++ b/tests/by-util/test_unexpand.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // -// spell-checker:ignore contenta +// spell-checker:ignore contenta edgecase behaviour use uutests::{at_and_ucmd, new_ucmd}; @@ -366,3 +366,90 @@ fn test_extended_tabstop_syntax() { .stdout_is(expected); } } + +#[test] +fn test_buffered_read_edgecase_behaviour() { + // reads are done in 4096 chunks. Tests edgecase spaces around chunk bounds + let test_cases = [ + { + // input has newlines in first chunk and has leading spaces after newline in chunk + let mut input = vec![b'0'; 180]; + input.push(b'\n'); + input.extend([b' '; 8]); + input.extend([b'0'; 3897]); + input.push(b'\n'); // 180 '0' -> 'n' -> 8 spaces -> 3897 '0' -> \n + + let mut expected = vec![b'0'; 180]; + expected.push(b'\n'); + expected.push(b'\t'); + expected.extend([b'0'; 3897]); + expected.push(b'\n'); // 180 '0' -> 'n' -> 1 tab -> 3897 '0' -> \n + (input, expected) + }, + { + // input has newline after first chunk with leading spaces + let mut input = vec![b'0'; 4096]; + input.extend("\n 0000\n".as_bytes()); // 4096 '0' -> \n -> 8 spaces -> 4 '0' -> \n + + let mut expected = vec![b'0'; 4096]; + expected.extend("\n\t0000\n".as_bytes()); // 4096 '0' -> \n -> 1 tab -> 4 '0' -> \n + (input, expected) + }, + { + // fixture has newlines in first chunk and has leading spaces after newline in chunk + let mut input = vec![b'0'; 4095]; + input.extend("\n 0000\n".as_bytes()); // 4095 '0' -> \n -> 8 spaces -> 4 '0' -> \n + + let mut expected = vec![b'0'; 4095]; + expected.extend("\n\t0000\n".as_bytes()); // 4095 '0' -> \n -> 1 tab -> 4 '0' -> \n + (input, expected) + }, + { + // input has trailing spaces in the first chunk (should not be unexpanded) into newline with leading + // spaces which should be unexpanded + let mut input = vec![b'0'; 4088]; + input.extend(" \n 0000\n".as_bytes()); // 4088 '0' -> 8 spaces -> \n -> 8 spaces -> 4 '0' -> \n + + let mut expected = vec![b'0'; 4088]; + expected.extend(" \n\t0000\n".as_bytes()); // 4088 '0' -> 8 spaces -> \n -> 8 spaces -> 4 '0' -> \n + (input, expected) + }, + { + // input has a trailing normal chars after new line in first chunk into leading spaces for new + // chunk (should not be unexpanded) + let mut input = vec![b'0'; 4087]; + input.extend("\n00000000 \n".as_bytes()); // 4087 '0' -> \n -> 8 '0' -> 8 spaces -> \n + + let mut expected = vec![b'0'; 4087]; + expected.extend("\n00000000 \n".as_bytes()); // 4087 '0' -> \n -> 8 '0' -> 8 spaces -> \n + (input, expected) + }, + { + // input has a trailing blanks after new line in first chunk (should be unexpanded) into leading spaces for new + // chunk (should be unexpanded) + let mut input = vec![b'0'; 4087]; + input.extend("\n \n".as_bytes()); // 4087 '0' -> 16 spaces -> \n + + let mut expected = vec![b'0'; 4087]; + expected.extend("\n\t\t\n".as_bytes()); // 4087 '0' -> 2 tabs -> \n + (input, expected) + }, + { + // input has a trailing blanks after new line in first chunk (should be unexpanded) into leading spaces for new + // chunk (should be unexpanded) (tests space counting is done over chunk bounds) + let mut input = vec![b'0'; 4091]; + input.extend("\n \n".as_bytes()); // 4091 '0' -> 8 spaces -> \n + + let mut expected = vec![b'0'; 4091]; + expected.extend("\n\t\n".as_bytes()); // 4091 '0' -> 1 tab -> \n + (input, expected) + }, + ]; + + for (input, expected) in test_cases { + new_ucmd!() + .pipe_in(input) + .succeeds() + .stdout_only(String::from_utf8(expected).unwrap()); + } +} diff --git a/tests/by-util/test_uptime.rs b/tests/by-util/test_uptime.rs index 42111834b..cf39eb8b2 100644 --- a/tests/by-util/test_uptime.rs +++ b/tests/by-util/test_uptime.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // -// spell-checker:ignore wincode serde utmp runlevel testusr testx boottime +// spell-checker:ignore utmp runlevel testusr testx boottime #![allow(clippy::cast_possible_wrap, clippy::unreadable_literal)] use uutests::{at_and_ucmd, new_ucmd}; @@ -97,8 +97,6 @@ fn test_uptime_with_non_existent_file() { fn test_uptime_with_file_containing_valid_boot_time_utmpx_record() { use std::fs::File; use std::{io::Write, path::PathBuf}; - use wincode::serialize; - use wincode_derive::SchemaWrite; // This test will pass for freebsd but we currently don't support changing the utmpx file for // freebsd. @@ -132,21 +130,18 @@ fn test_uptime_with_file_containing_valid_boot_time_utmpx_record() { const RUN_LVL: i32 = 1; const USER_PROCESS: i32 = 7; - #[derive(SchemaWrite)] #[repr(C)] pub struct TimeVal { pub tv_sec: i32, pub tv_usec: i32, } - #[derive(SchemaWrite)] #[repr(C)] pub struct ExitStatus { e_termination: i16, e_exit: i16, } - #[derive(SchemaWrite)] #[repr(C, align(4))] pub struct Utmp { pub ut_type: i32, @@ -222,9 +217,35 @@ fn test_uptime_with_file_containing_valid_boot_time_utmpx_record() { glibc_reserved: [0; 20], }; - let mut buf = serialize(&utmp).unwrap(); - buf.append(&mut serialize(&utmp1).unwrap()); - buf.append(&mut serialize(&utmp2).unwrap()); + fn serialize_i8_arr(buf: &mut Vec, arr: &[i8]) { + for b in arr { + buf.push(*b as u8); + } + } + + fn serialize(utmp: &Utmp) -> Vec { + let mut buf = Vec::new(); + buf.extend_from_slice(&utmp.ut_type.to_ne_bytes()); + buf.extend_from_slice(&utmp.ut_pid.to_ne_bytes()); + serialize_i8_arr(&mut buf, &utmp.ut_line); + serialize_i8_arr(&mut buf, &utmp.ut_id); + serialize_i8_arr(&mut buf, &utmp.ut_user); + serialize_i8_arr(&mut buf, &utmp.ut_host); + buf.extend_from_slice(&utmp.ut_exit.e_termination.to_ne_bytes()); + buf.extend_from_slice(&utmp.ut_exit.e_exit.to_ne_bytes()); + buf.extend_from_slice(&utmp.ut_session.to_ne_bytes()); + buf.extend_from_slice(&utmp.ut_tv.tv_sec.to_ne_bytes()); + buf.extend_from_slice(&utmp.ut_tv.tv_usec.to_ne_bytes()); + for v in &utmp.ut_addr_v6 { + buf.extend_from_slice(&v.to_ne_bytes()); + } + serialize_i8_arr(&mut buf, &utmp.glibc_reserved); + buf + } + + let mut buf = serialize(&utmp); + buf.append(&mut serialize(&utmp1)); + buf.append(&mut serialize(&utmp2)); let mut f = File::create(path).unwrap(); f.write_all(&buf).unwrap(); } diff --git a/tests/test_util_name.rs b/tests/test_util_name.rs index 745e6814b..17b9dc36e 100644 --- a/tests/test_util_name.rs +++ b/tests/test_util_name.rs @@ -235,3 +235,17 @@ fn test_musl_no_dynamic_deps() { stdout ); } + +#[test] +fn test_sorted_utils() { + let s = TestScenario::new("list_sorted"); + let out = String::from_utf8( + std::process::Command::new(&s.bin_path) + .arg("--list") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + assert!(out.lines().filter(|s| !s.is_empty()).is_sorted()); +} diff --git a/tests/test_uudoc.rs b/tests/test_uudoc.rs index b4f12c099..a1e32f484 100644 --- a/tests/test_uudoc.rs +++ b/tests/test_uudoc.rs @@ -62,10 +62,8 @@ fn get_doc_file_from_output(output: &str) -> (String, String) { Ok(content) => content, Err(e) => { panic!( - "Failed to read file {}: {} from {:?}", - correct_path_test, - e, - env::current_dir() + "Failed to read file {correct_path_test}: {e} from {:?}", + env::current_dir(), ); } }; diff --git a/tests/uutests/Cargo.toml b/tests/uutests/Cargo.toml index 457930b4e..56ccab13b 100644 --- a/tests/uutests/Cargo.toml +++ b/tests/uutests/Cargo.toml @@ -7,6 +7,7 @@ repository = "https://github.com/uutils/coreutils/tree/main/tests/uutests" authors.workspace = true categories.workspace = true edition.workspace = true +rust-version.workspace = true homepage.workspace = true keywords.workspace = true license.workspace = true diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 70886e5e9..9b1477f7b 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -124,7 +124,7 @@ else : > man/local.mk # Use CFLAGS for best build time since we discard GNU coreutils CFLAGS="${CFLAGS} -pipe -O0 -s" ./configure -C --quiet --disable-gcc-warnings --disable-nls --disable-dependency-tracking --disable-bold-man-page-references \ - --enable-single-binary=symlinks --enable-install-program="arch,kill,uptime,hostname" \ + --enable-single-binary=hardlinks --enable-install-program="arch,kill,uptime,hostname" \ "$([ "${SELINUX_ENABLED}" = 1 ] && echo --with-selinux || echo --without-selinux)" #Add timeout to to protect against hangs sed -i 's|^"\$@|'"${SYSTEM_TIMEOUT}"' 600 "\$@|' build-aux/test-driver @@ -162,6 +162,9 @@ fi grep -rl 'path_prepend_' tests/* | xargs -r "${SED}" -i 's| path_prepend_ ./src||' # path_prepend_ sets $abs_path_dir_: set it manually instead. grep -rl '\$abs_path_dir_' tests/*/*.sh | xargs -r "${SED}" -i "s|\$abs_path_dir_|${UU_BUILD_DIR//\//\\/}|g" +# Some tests use $abs_top_builddir/src for shebangs: point them to the uutils build dir. +grep -rl '\$abs_top_builddir/src' tests/*/*.sh tests/*/*.pl | xargs -r "${SED}" -i "s|\$abs_top_builddir/src|${UU_BUILD_DIR//\//\\/}|g" +grep -rl '\$ENV{abs_top_builddir}/src' tests/*/*.pl | xargs -r "${SED}" -i "s|\$ENV{abs_top_builddir}/src|${UU_BUILD_DIR//\//\\/}|g" # We can't build runcon and chcon without libselinux. But GNU no longer builds dummies of them. So consider they are SELinux specific. sed -i 's/^print_ver_.*/require_selinux_/' tests/runcon/runcon-compute.sh diff --git a/util/fetch-gnu.sh b/util/fetch-gnu.sh index a242b6606..afd9ebfa8 100755 --- a/util/fetch-gnu.sh +++ b/util/fetch-gnu.sh @@ -1,29 +1,10 @@ #!/bin/bash -e -ver="9.9" +ver="9.10" repo=https://github.com/coreutils/coreutils curl -L "${repo}/releases/download/v${ver}/coreutils-${ver}.tar.xz" | tar --strip-components=1 -xJf - -# TODO stop backporting tests from master at GNU coreutils > 9.9 -curl -L ${repo}/raw/refs/heads/master/tests/timeout/timeout.sh > tests/timeout/timeout.sh -curl -L ${repo}/raw/refs/heads/master/tests/timeout/timeout-group.sh > tests/timeout/timeout-group.sh -curl -L ${repo}/raw/refs/heads/master/tests/mv/hardlink-case.sh > tests/mv/hardlink-case.sh -curl -L ${repo}/raw/refs/heads/master/tests/mkdir/writable-under-readonly.sh > tests/mkdir/writable-under-readonly.sh -curl -L ${repo}/raw/refs/heads/master/tests/cp/cp-mv-enotsup-xattr.sh > tests/cp/cp-mv-enotsup-xattr.sh #spell-checker:disable-line -curl -L ${repo}/raw/refs/heads/master/tests/cp/nfs-removal-race.sh > tests/cp/nfs-removal-race.sh -curl -L ${repo}/raw/refs/heads/master/tests/csplit/csplit-io-err.sh > tests/csplit/csplit-io-err.sh -# Replace tests not compatible with our binaries -sed -i -e 's/no-mtab-status.sh/no-mtab-status-masked-proc.sh/' -e 's/nproc-quota.sh/nproc-quota-systemd.sh/' tests/local.mk -curl -L ${repo}/raw/refs/heads/master/tests/df/no-mtab-status-masked-proc.sh > tests/df/no-mtab-status-masked-proc.sh -curl -L ${repo}/raw/refs/heads/master/tests/nproc/nproc-quota-systemd.sh > tests/nproc/nproc-quota-systemd.sh -curl -L ${repo}/raw/refs/heads/master/tests/stty/bad-speed.sh > tests/stty/bad-speed.sh -# symlink to /bin/false should fail with --help. Freeze commit to avoid regression. -curl -L https://raw.githubusercontent.com/coreutils/coreutils/d5164f3d216917005003877faeb1abe7cae5d765/tests/misc/coreutils.sh > tests/misc/coreutils.sh -# Better support for single binary -curl -L ${repo}/raw/refs/heads/master/tests/env/env.sh > tests/env/env.sh -# Avoid incorrect PASS -curl -L ${repo}/raw/refs/heads/master/tests/runcon/runcon-compute.sh > tests/runcon/runcon-compute.sh -curl -L ${repo}/raw/refs/heads/master/tests/tac/tac-continue.sh > tests/tac/tac-continue.sh -curl -L ${repo}/raw/refs/heads/master/tests/tail/inotify-dir-recreate.sh > tests/tail/inotify-dir-recreate.sh -# Add tac-continue.sh to root tests (it requires root to mount tmpfs) -# Use sed -i.bak for macOS -sed -i.bak 's|tests/split/l-chunk-root.sh.*|tests/split/l-chunk-root.sh\t\t\t\\\n tests/tac/tac-continue.sh\t\t\t\\|' tests/local.mk +# TODO stop backporting tests from master at GNU coreutils > $ver +# backport = () +# for f in ${backport[@]} +# do curl -L ${repo}/raw/refs/heads/master/tests/$f > tests/$f +# done diff --git a/util/gnu-patches/series b/util/gnu-patches/series index 2d9b30b2c..f1a24d0f6 100644 --- a/util/gnu-patches/series +++ b/util/gnu-patches/series @@ -10,4 +10,3 @@ tests_sort_merge.pl.patch tests_du_move_dir_while_traversing.patch test_mkdir_restorecon.patch error_msg_uniq.diff -tests_tail_overlay_headers.patch diff --git a/util/gnu-patches/tests_tail_overlay_headers.patch b/util/gnu-patches/tests_tail_overlay_headers.patch index 205401294..c01b718ca 100644 --- a/util/gnu-patches/tests_tail_overlay_headers.patch +++ b/util/gnu-patches/tests_tail_overlay_headers.patch @@ -31,19 +31,3 @@ -retry_delay_ wait4lines_ .1 6 13 || fail=1 +# Wait for 8 more lines (should total 13) +retry_delay_ wait4lines_ .1 6 13 || { echo "Failed waiting for 13 total lines"; fail=1; } - - kill $sleep && wait || framework_failure_ - --test "$(countlines_)" = 13 || fail=1 -+final_count=$(countlines_) -+echo "=== Final line count: $final_count (expected 13) ===" -+ -+if test "$final_count" != 13; then -+ echo "=== FAILURE: Expected 13 lines, got $final_count ===" -+ echo "=== Full output content: ===" -+ cat -A out -+ echo "=== End output content ===" -+ fail=1 -+fi - - Exit $fail