42 Commits

Author SHA1 Message Date
Jesse Rosenstock 795d29ab35 xargs: clarify -x and -t help text
The previous text implied -x only interacts with -L/-n and used
imprecise phrasing. The new text mirrors the POSIX spec: terminate
when a constructed command line would exceed the size limit rather
than reducing the argument count.

Clarify that -t prints each command to stderr, rather than just
"be verbose".
2026-06-10 15:00:22 +02:00
xtqqczze c619fbeb82 chore(deps): update rust crates 2026-06-10 14:51:52 +02:00
weili a70f5ccd7c find: -ls: don't panic on an unmapped owner uid/gid
`find <file> -ls` panicked (exit 101) when the file's owning uid (or gid) had
no `/etc/passwd` (`/etc/group`) entry: it resolved the name with
`User::from_uid(...).unwrap().unwrap()`, and the inner unwrap aborts on the
`Ok(None)` that an unmapped id returns. Unmapped owners are routine (extracted
archives, NFS, deleted accounts, container images).

Resolve the uid/gid via `uucore::entries::uid2usr`/`gid2grp`, falling back to
the numeric id when there is no entry, matching GNU find (which prints the
number). These helpers already serialize the non-thread-safe getpwuid/getgrgid
behind a mutex, so reuse them rather than reimplementing the lookup.

Add a -ls integration test that chowns a file to an unmapped id and checks the
numeric fallback; it skips when the process lacks the privilege to do so.
2026-06-10 07:45:53 +02:00
weili 5992901805 find: -printf: test octal escape before a multibyte char
Regression test for the panic fixed in #720/#723: a `\NNN` octal escape
immediately followed by a multibyte character (`\0€`) used to slice the
format string mid-char and abort. Assert `-printf` emits the escape byte
and the following character intact.
2026-06-10 07:45:25 +02:00
Sylvestre Ledru 782744f35c ci: build and lint the wasm32-wasip1 target
Add a job that installs the wasm32-wasip1 target plus the WASI SDK (so the
bundled C in onig_sys can be cross-compiled) and runs cargo build and
clippy -D warnings, so non-Unix compilation can't regress.
2026-06-10 07:45:04 +02:00
Sylvestre Ledru c100ac226f build: support compiling for non-Unix targets such as wasm
The platform-specific code was split into cfg(unix)/cfg(windows) arms, so
wasm32 targets (which are neither) failed to compile. Widen the generic
Windows stubs to cfg(not(unix)) and make the -ls printer and xargs size
limiters fall back to portable APIs when no platform-specific one exists.
2026-06-10 07:45:04 +02:00
dependabot[bot] 5bf236b891 build(deps): bump regex from 1.12.3 to 1.12.4
Bumps [regex](https://github.com/rust-lang/regex) from 1.12.3 to 1.12.4.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.12.3...1.12.4)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 1.12.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-10 07:15:21 +02:00
weili 001966f326 find: -name: don't panic on a malformed POSIX bracket class
A `-name` glob with a `[.`/`[=`/`[:` introducer whose closing delimiter is
missing — only a lone `]` (or the delimiter) before a multibyte char, e.g.
`'[[:]é'` — panicked (exit 101). `extract_bracket_expr` searched for *either*
the delimiter or `]` and then added a blind `+ 2`, which overshot into the
following character and sliced off a UTF-8 boundary.

Search for the actual two-byte closing sequence `<delim>]` instead. A valid
class (`[:alpha:]`) is unchanged; a malformed one returns None, so the caller
treats `[` literally and accepts the pattern like GNU find (exit 0). Reachable
via -name/-iname/-path/-ipath/-lname/-ilname. Adds a regression test.
2026-06-09 22:41:13 +02:00
weili 53980035af find: -printf: don't panic on a multibyte char after an octal escape
`\NNN` octal escapes were parsed by slicing a fixed 3 bytes off the
format string, which panics when fewer than 3 octal digits are followed
by a multibyte character (e.g. `-printf '\0€'`): the 3-byte slice lands
inside the multibyte char and trips a char-boundary assertion.

Parse the octal escape from the leading octal digits only (1 to 3, all
ASCII) and advance by their byte length. This also fixes `\1`..`\7`,
which previously fell through to the single-character escape table and
errored instead of being treated as octal, matching GNU find.
2026-06-09 22:23:39 +02:00
weili e2d84e98d5 locate: don't panic on a too-short --database file
`locate -d <file> <pattern>` panicked (slice index underflow, exit 101) when
the database file is exactly one byte: `check_db` consumes that byte with
`read_exact`, then `read_until(b'\0', ...)` hits EOF and appends nothing,
leaving `buf` empty. `&buf[..buf.len() - 1]` then underflows `buf.len() - 1`
to `usize::MAX` and slices out of bounds.

Strip the trailing nul with `strip_suffix(b"\0")` (falling back to the whole
buffer when `read_until` stopped at EOF without one) instead of slicing
`buf.len() - 1`. An empty buffer now yields `None` (unrecognized format), so
the too-short database is skipped like any other invalid one and locate exits
1 instead of crashing. Adds a regression test.
2026-06-09 22:23:23 +02:00
dependabot[bot] 865ab0991e build(deps): bump moonrepo/setup-rust from 0 to 1
Bumps [moonrepo/setup-rust](https://github.com/moonrepo/setup-rust) from 0 to 1.
- [Release notes](https://github.com/moonrepo/setup-rust/releases)
- [Changelog](https://github.com/moonrepo/setup-rust/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moonrepo/setup-rust/compare/v0...v1)

---
updated-dependencies:
- dependency-name: moonrepo/setup-rust
  dependency-version: '1'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 09:35:56 +02:00
dependabot[bot] 8add685cc6 build(deps): bump codecov/codecov-action from 6 to 7
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 6 to 7.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 09:29:29 +02:00
dependabot[bot] c4afeab1bc build(deps): bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 09:29:07 +02:00
Sylvestre Ledru 62deaf6e60 updatedb: clear error when the output database can't be created
Opening the output file with `?` surfaced a bare "No such file or
directory" with no indication of which file failed (easy to hit because the
default output path /usr/local/var/locatedb often doesn't exist).

Report it like GNU does, naming the path and the reason, e.g.
`cannot create '/usr/local/var/locatedb': No such file or directory`.
strip_errno() drops the trailing "(os error N)" noise.
2026-06-08 23:38:59 +02:00
Sylvestre Ledru 1be5541c74 find: avoid redundant stat per -fstype evaluation
The -fstype matcher called path.symlink_metadata() (a statx/lstat syscall)
on every entry before consulting its cache, just to obtain the device id.
With several -fstype clauses on the same expression (as updatedb builds:
nfs/NFS/proc), every file was stat'd once per clause.

Derive the device id from WalkEntry::metadata() instead, which is cached on
the entry in a OnceCell and shared across all matchers, so each file is
stat'd at most once regardless of how many -fstype clauses run.

On 'updatedb --localpaths=/usr' (~506k files) this drops statx calls from
~1.52M to ~506k and wall-clock from ~1.82s to ~1.32s, with byte-identical
output.
2026-06-08 23:38:59 +02:00
Sylvestre Ledru 5d0e7add34 lints: enable clippy all/cargo/pedantic groups, mirroring uutils/coreutils
Enable the broader clippy lint set used by uutils/coreutils (the all,
cargo and pedantic groups plus the use_self nursery lint), allowing the
noisy/low-value lints. The preceding commits fix the warnings these
groups surface so the tree stays warning-free.
2026-06-08 22:48:45 +02:00
Sylvestre Ledru 42f8a9af36 find: fix clippy::needless_for_each 2026-06-08 22:48:45 +02:00
Sylvestre Ledru 664a9c23e6 xargs: fix clippy::needless_continue 2026-06-08 22:48:45 +02:00
Sylvestre Ledru fd4bd6acfa find,xargs: fix clippy::items_after_statements 2026-06-08 22:48:45 +02:00
Sylvestre Ledru fa4f90befb find: fix clippy::unnecessary_wraps 2026-06-08 22:48:45 +02:00
Sylvestre Ledru 7deeee6fdf find: fix clippy::unreadable_literal 2026-06-08 22:48:45 +02:00
Sylvestre Ledru 51a50022c2 find: fix clippy::assigning_clones 2026-06-08 22:48:45 +02:00
Sylvestre Ledru 600f3af140 find: fix clippy::implicit_clone 2026-06-08 22:48:45 +02:00
Sylvestre Ledru a63482b8cf find: fix clippy::match_bool 2026-06-08 22:48:45 +02:00
Sylvestre Ledru 47796303ca find: fix clippy::single_match_else 2026-06-08 22:48:45 +02:00
Sylvestre Ledru baa6ce5d92 find: fix clippy::unnecessary_semicolon 2026-06-08 22:48:45 +02:00
Sylvestre Ledru 9f33c14fc9 find: fix clippy::map_unwrap_or 2026-06-08 22:48:45 +02:00
Sylvestre Ledru a80620c88f find: fix clippy::redundant_closure_for_method_calls 2026-06-08 22:48:45 +02:00
Sylvestre Ledru a2c75f94ca prepare release 0.9.0 (#714) 2026-06-08 22:44:43 +02:00
dependabot[bot] 44fbc59263 build(deps): bump rstest from 0.25.0 to 0.26.1 (#710)
Bumps [rstest](https://github.com/la10736/rstest) from 0.25.0 to 0.26.1.
- [Release notes](https://github.com/la10736/rstest/releases)
- [Changelog](https://github.com/la10736/rstest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/la10736/rstest/compare/v0.25.0...v0.26.1)

---
updated-dependencies:
- dependency-name: rstest
  dependency-version: 0.26.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-08 08:40:05 +02:00
✿ Fleur de Blue fb9f8df047 fix: Handle write failures when printing --help/--version (#656) 2026-06-07 19:05:03 +02:00
Sylvestre Ledru 2044d1e31d make locate and updatedb work on Windows (#704)
locate and updatedb were Unix-only (gated behind cfg and stubbed out on
Windows). Make them cross-platform:

- locate: decode raw DB entries via a platform-specific bytes_to_path
  (verbatim bytes on Unix, lossy UTF-8 elsewhere), use Metadata::len()
  instead of the unix-only size(), and a cross-platform make_symlink in
  tests that skips gracefully when symlink creation is unprivileged.
- drop the #[cfg(unix)] gate on the locate module and the
  "unsupported on Windows" main stubs for locate/updatedb.
- db_tests: remove #[cfg(not(windows))] gates, write updatedb output to
  a tempdir instead of /dev/null, and add a full updatedb->locate
  roundtrip test that is platform-independent.
2026-06-07 11:52:08 +02:00
Jesse Rosenstock 0494ba1be4 find: Update --help text (#707)
Replace the "early alpha implementation" placeholder help text with a
proper organized reference grouped by category (global options,
operators, positional options, tests, and actions), with brief
descriptions for each entry. The old text was a flat list that hadn't
kept up with the implementation and still carried the "early alpha"
disclaimer.

Also fix a typo in a comment: "- newercm" -> "-newercm".
2026-06-07 11:51:45 +02:00
Sylvestre Ledru fc446aba0b ci: add CodSpeed benchmarks for updatedb and locate (#705)
Drive both utilities end-to-end through their real entry points.

- benches/updatedb_bench.rs: walks a generated tree (~8.5k paths) and
  writes a LOCATE02 database, measuring the walk + front-coding + write
  (full build and a -regex/-prune variant). Empty prune options keep the
  run deterministic regardless of temp_dir location.
- benches/locate_bench.rs: builds the database once via updatedb, then
  scans it in -c/count mode so database decoding and pattern matching
  dominate (no-match, substring, -b basename, -i ignore-case, -r regex).
  Unix-only, matching the locate module's cfg.

The existing CodSpeed workflow picks these up via cargo codspeed build/run.
2026-06-06 11:00:04 +02:00
Sylvestre Ledru 33703b79d8 ci: add CodSpeed benchmarks for find and xargs (#703)
Introduce criterion benchmarks (via codspeed-criterion-compat) that drive
find and xargs end-to-end through their real entry points, plus a CodSpeed
GitHub Actions workflow to track performance on pushes and PRs.

- benches/find_bench.rs: walks a generated directory tree exercising the
  matcher tree (-name/-iname/-regex/-size/-type, grouping, -prune, -printf).
  Output is sent to a sink so the directory walk and matching dominate.
- benches/xargs_bench.rs: feeds a corpus via -a and runs 'true' so xargs'
  own argument reading/splitting/batching dominates (whitespace, NUL, -n, -s).
- .github/workflows/codspeed.yml: mirrors the uutils/grep setup.
2026-06-06 10:37:56 +02:00
Jeremy Smart c5ff1bbb6b implement locate and updatedb (#536)
---------

Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
2026-06-06 10:10:44 +02:00
Sylvestre Ledru e7c34ab12f test: properly exercise {} in -exec utility name (#702)
The test from #647 used {} only in an argument, so it passed on
unpatched code. Rewrite it to make the matched entry the
testing-commandline binary and pass {} as the utility name, so the
placeholder must resolve to the entry's path and execute.
2026-06-06 09:35:34 +02:00
Matt Van Horn 1a77a83432 fix: replace {} placeholder in -exec utility name (#647)
* fix: replace {} placeholder in -exec utility name

The {} placeholder was only replaced in arguments to -exec, not in the
utility_name position itself. Running `find -exec {} \;` passed the
literal string "{}" to Command::new, causing "No such file or directory".

Extracted arg parsing into a shared `parse_arg` helper and changed the
`executable` field from String to the existing Arg enum so it undergoes
the same {} replacement as other arguments.

Fixes #614

* test: add test for {} placeholder in utility name

Verify that SingleExecMatcher resolves {} in arguments
when the executable is a known path. Covers the case
where -exec receives {} in argument positions.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-06-06 08:50:24 +02:00
Sylvestre Ledru a65160f724 find: match GNU prompt format for -ok/-okdir (#699)
The -ok/-okdir prompt rendered the full substituted command, but GNU
find prints a fixed, abbreviated form: "< executable ... pathname > ? "
(literal ..., a space before ?, and always the full entry path even for
-okdir). Match it exactly and update the prompt-format test.

Verified byte-for-byte against GNU findutils 4.10.0.
2026-06-05 13:18:13 +08:00
dependabot[bot] 435f00d491 build(deps): bump softprops/action-gh-release from 2 to 3
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-05 07:10:08 +02:00
dependabot[bot] db941e7fba build(deps): bump chrono from 0.4.44 to 0.4.45
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.44 to 0.4.45.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.44...v0.4.45)

---
updated-dependencies:
- dependency-name: chrono
  dependency-version: 0.4.45
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-05 07:08:07 +02:00
Sylvestre Ledru 5bef01f72f github action: publish the release as draft first (#530) 2026-06-04 22:51:02 +02:00
37 changed files with 3207 additions and 289 deletions
+24 -1
View File
@@ -65,6 +65,29 @@ jobs:
- run: |
cargo clippy --all-targets -- -D warnings
wasm:
name: cargo build (wasm32-wasip1)
runs-on: ubuntu-latest
env:
WASI_SDK_VERSION: "33"
WASI_SDK_PATH: ${{ github.workspace }}/wasi-sdk
# onig_sys compiles bundled C, so it needs a C compiler with a WASI
# sysroot to cross-compile to wasm.
CC_wasm32_wasip1: ${{ github.workspace }}/wasi-sdk/bin/clang
AR_wasm32_wasip1: ${{ github.workspace }}/wasi-sdk/bin/llvm-ar
steps:
- uses: actions/checkout@v6
- name: Install wasm target
run: rustup target add wasm32-wasip1
- name: Install WASI SDK
run: |
curl -sSfL "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz" | tar xz
mv "wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux" "${WASI_SDK_PATH}"
- name: Build
run: cargo build --target wasm32-wasip1
- name: Clippy
run: cargo clippy --target wasm32-wasip1 -- -D warnings
grcov:
name: Code coverage
runs-on: ${{ matrix.os }}
@@ -92,7 +115,7 @@ jobs:
path: lcov.info
- name: Upload coverage to codecov.io
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
with:
files: lcov.info
fail_ci_if_error: true
+37
View File
@@ -0,0 +1,37 @@
name: CodSpeed
on:
push:
branches:
- "main"
pull_request:
# `workflow_dispatch` allows CodSpeed to trigger backtest
# performance analysis in order to generate initial data.
workflow_dispatch:
permissions:
contents: read
id-token: write
jobs:
codspeed:
name: Run benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Rust toolchain, cache and cargo-codspeed binary
uses: moonrepo/setup-rust@v1
with:
channel: stable
cache-target: release
bins: cargo-codspeed
- name: Build the benchmark target(s)
run: cargo codspeed build
- name: Run the benchmarks
uses: CodSpeedHQ/action@v4
with:
mode: simulation
run: cargo codspeed run
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
install --strip -Dm755 target/release/{find,xargs} -t findutils-x86_64-unknown-linux-gnu
ZSTD_CLEVEL=19 tar --zstd -caf ../findutils-x86_64-unknown-linux-gnu.tar.zst findutils-x86_64-unknown-linux-gnu
- name: Publish latest commit
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
tag_name: latest-commit
+1 -1
View File
@@ -272,7 +272,7 @@ jobs:
# Write and read notes from a file to avoid quoting breaking things
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
gh release create "${{ needs.plan.outputs.tag }}" --draft --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
announce:
needs:
Generated
+722 -92
View File
File diff suppressed because it is too large Load Diff
+66 -11
View File
@@ -1,6 +1,6 @@
[package]
name = "findutils"
version = "0.8.0"
version = "0.9.0"
homepage = "https://github.com/uutils/findutils"
repository = "https://github.com/uutils/findutils"
edition = "2021"
@@ -11,26 +11,55 @@ authors = ["uutils developers"]
[dependencies]
argmax = "0.4.0"
chrono = "0.4.44"
clap = "4.6"
chrono = "0.4.45"
clap = { version = "4.6", features = ["env"] }
faccess = "0.2.4"
nix = { version = "0.31", features = ["fs", "user"] }
onig = { version = "6.5", default-features = false }
regex = "1.12"
uucore = { version = "0.9.0", features = ["entries", "fs", "fsext", "mode"] }
walkdir = "2.5"
itertools = "0.14.0"
thiserror = "2.0.12"
[dev-dependencies]
criterion = { version = "4.7.0", package = "codspeed-criterion-compat" }
assert_cmd = "2"
ctor = "1.0"
filetime = "0.2"
nix = { version = "0.31", features = ["fs"] }
rstest = "0.26.1"
tempfile = "3"
uutests = "0.9.0"
[[bench]]
name = "find_bench"
harness = false
[[bench]]
name = "xargs_bench"
harness = false
[[bench]]
name = "updatedb_bench"
harness = false
[[bench]]
name = "locate_bench"
harness = false
[[bin]]
name = "find"
path = "src/find/main.rs"
[[bin]]
name = "locate"
path = "src/locate/main.rs"
[[bin]]
name = "updatedb"
path = "src/updatedb/main.rs"
[[bin]]
name = "xargs"
path = "src/xargs/main.rs"
@@ -48,17 +77,43 @@ inherits = "release"
panic = "abort"
[lints.clippy]
all = { level = "warn", priority = -1 }
cargo = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
use_self = "warn" # nursery lint
# Group-level allows for lints that are too noisy or not worth enforcing for now.
multiple_crate_versions = "allow"
cargo_common_metadata = "allow"
uninlined_format_args = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
use_self = "warn"
needless_pass_by_value = "warn"
semicolon_if_nothing_returned = "warn"
single_char_pattern = "warn"
explicit_iter_loop = "warn"
if_not_else = "warn"
manual_let_else = "warn"
must_use_candidate = "allow"
match_same_arms = "allow"
cast_possible_truncation = "allow"
too_many_lines = "allow"
cast_possible_wrap = "allow"
cast_sign_loss = "allow"
struct_excessive_bools = "allow"
cast_precision_loss = "allow"
cast_lossless = "allow"
ignored_unit_patterns = "allow"
similar_names = "allow"
float_cmp = "allow"
return_self_not_must_use = "allow"
inline_always = "allow"
fn_params_excessive_bools = "allow"
used_underscore_items = "allow"
should_panic_without_expect = "allow"
doc_markdown = "allow"
unused_self = "allow"
enum_glob_use = "allow"
unnested_or_patterns = "allow"
implicit_hasher = "allow"
doc_link_with_quotes = "allow"
format_push_string = "allow"
flat_map_option = "allow"
from_iter_instead_of_collect = "allow"
large_types_passed_by_value = "allow"
# Disable for now, we have a few occurrences
# panic = "warn"
+166
View File
@@ -0,0 +1,166 @@
// Copyright 2024 the uutils developers
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
//! End-to-end benchmarks for `find`, run through the real `find_main` entry
//! point so they exercise argument parsing, the matcher tree and the directory
//! walk together. Output is sent to a sink so the benchmarks measure the work
//! `find` does rather than terminal I/O.
use std::cell::RefCell;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use findutils::find::{find_main, Dependencies};
/// `Dependencies` implementation that throws output away. A fixed `now` keeps
/// time-based matchers (`-newer`, `-mtime`, …) deterministic across runs.
struct SinkDependencies {
output: RefCell<io::Sink>,
now: SystemTime,
}
impl SinkDependencies {
fn new() -> Self {
Self {
output: RefCell::new(io::sink()),
now: SystemTime::now(),
}
}
}
impl Dependencies for SinkDependencies {
fn get_output(&self) -> &RefCell<dyn Write> {
&self.output
}
fn now(&self) -> SystemTime {
self.now
}
fn confirm(&self, _prompt: &str) -> bool {
false
}
}
/// Run `find` end-to-end. `args` are the arguments after the program name
/// (flags, paths, expression). The exit status is ignored — we only care about
/// the work performed.
fn run(args: &[&str]) {
let mut argv: Vec<&str> = Vec::with_capacity(args.len() + 1);
argv.push("find");
argv.extend_from_slice(args);
let deps = SinkDependencies::new();
let _ = find_main(&argv, &deps);
}
/// Build a moderately deep directory tree to walk. `depth` levels, each holding
/// `dirs` sub-directories and `files` files. File names vary so name/regex
/// matchers have something to discriminate on, and sizes vary so `-size` does
/// real work. Returns the root directory.
fn build_tree(depth: u32, dirs: u32, files: u32) -> PathBuf {
let root = std::env::temp_dir().join(format!("uu_find_bench_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
populate(&root, depth, dirs, files, 0);
root
}
fn populate(dir: &Path, depth: u32, dirs: u32, files: u32, seed: u32) {
for f in 0..files {
let n = seed.wrapping_add(f);
// A mix of extensions and a sprinkling of a rare marker name.
let name = match n % 5 {
0 => format!("module_{n}.rs"),
1 => format!("data_{n}.txt"),
2 => format!("image_{n}.png"),
3 => format!("README_{n}.md"),
_ if n % 500 == 0 => format!("RAREHIT_{n}.log"),
_ => format!("file_{n}.log"),
};
// Sizes from a few bytes up to ~8 KiB so -size buckets differ.
let size = (n as usize % 8192) + 1;
let path = dir.join(name);
std::fs::write(&path, vec![b'x'; size]).unwrap();
}
if depth == 0 {
return;
}
for d in 0..dirs {
let sub = dir.join(format!("dir_{d}"));
std::fs::create_dir_all(&sub).unwrap();
populate(
&sub,
depth - 1,
dirs,
files,
seed.wrapping_add((d + 1) * 31),
);
}
}
fn bench_e2e(c: &mut Criterion) {
// depth 4, 4 dirs/level, 25 files/dir → a few thousand entries.
let root = build_tree(4, 4, 25);
let dir = root.to_str().unwrap();
let mut group = c.benchmark_group("find");
// Plain full walk with the implicit -print.
group.bench_function("walk_all", |b| {
b.iter(|| run(black_box(&[dir])));
});
// Filter by file type only.
group.bench_function("type_f", |b| {
b.iter(|| run(black_box(&[dir, "-type", "f"])));
});
// Glob name match — a common invocation.
group.bench_function("name_glob", |b| {
b.iter(|| run(black_box(&[dir, "-name", "*.rs"])));
});
// Case-insensitive name match.
group.bench_function("iname_glob", |b| {
b.iter(|| run(black_box(&[dir, "-iname", "*.RS"])));
});
// Regex over the whole path.
group.bench_function("regex_path", |b| {
b.iter(|| run(black_box(&[dir, "-regex", r".*/module_[0-9]+\.rs"])));
});
// Size predicate forces a stat per entry.
group.bench_function("size_gt", |b| {
b.iter(|| run(black_box(&[dir, "-type", "f", "-size", "+4k"])));
});
// Combined predicate with AND/OR and grouping.
group.bench_function("combined_expr", |b| {
b.iter(|| {
run(black_box(&[
dir, "-type", "f", "(", "-name", "*.rs", "-o", "-name", "*.md", ")",
]));
});
});
// Prune whole subtrees, then print the rest.
group.bench_function("prune", |b| {
b.iter(|| {
run(black_box(&[
dir, "-name", "dir_0", "-prune", "-o", "-type", "f", "-print",
]));
});
});
// -printf with several directives exercises the formatter.
group.bench_function("printf", |b| {
b.iter(|| run(black_box(&[dir, "-type", "f", "-printf", "%p %s %y\\n"])));
});
group.finish();
let _ = std::fs::remove_dir_all(&root);
}
criterion_group!(benches, bench_e2e);
criterion_main!(benches);
+127
View File
@@ -0,0 +1,127 @@
// Copyright 2024 the uutils developers
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
//! End-to-end benchmarks for `locate`, run through the real `locate_main` entry
//! point. A LOCATE02 database is built once (via `updatedb`) from a generated
//! tree, then each case scans it in `-c`/count mode so the database decoding
//! and pattern matching dominate rather than terminal output.
//!
//! `locate` is Unix-only (it relies on `OsStr`/`CStr` byte APIs), so the
//! benchmark body is compiled only there.
#[cfg(unix)]
mod bench {
use std::path::{Path, PathBuf};
use criterion::{black_box, criterion_group, Criterion};
use findutils::locate::locate_main;
use findutils::updatedb::updatedb_main;
/// Run `locate` end-to-end. `args` are the arguments after the program name.
fn run(args: &[&str]) {
let mut argv: Vec<&str> = Vec::with_capacity(args.len() + 1);
argv.push("locate");
argv.extend_from_slice(args);
let _ = locate_main(&argv);
}
fn build_tree(depth: u32, dirs: u32, files: u32) -> PathBuf {
let root = std::env::temp_dir().join(format!("uu_locate_bench_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
populate(&root, depth, dirs, files, 0);
root
}
fn populate(dir: &Path, depth: u32, dirs: u32, files: u32, seed: u32) {
for f in 0..files {
let n = seed.wrapping_add(f);
let name = match n % 4 {
0 => format!("module_{n}.rs"),
1 => format!("data_{n}.txt"),
2 => format!("image_{n}.png"),
_ => format!("file_{n}.log"),
};
std::fs::write(dir.join(name), b"x").unwrap();
}
if depth == 0 {
return;
}
for d in 0..dirs {
let sub = dir.join(format!("dir_{d}"));
std::fs::create_dir_all(&sub).unwrap();
populate(
&sub,
depth - 1,
dirs,
files,
seed.wrapping_add((d + 1) * 31),
);
}
}
pub fn bench_e2e(c: &mut Criterion) {
// ~8.5k paths, then front-coded into a LOCATE02 database once.
let root = build_tree(4, 4, 25);
let dir = root.to_str().unwrap();
let db = std::env::temp_dir().join(format!("uu_locate_bench_{}.db", std::process::id()));
let db_path = db.to_str().unwrap();
let _ = updatedb_main(&[
"updatedb",
&format!("--localpaths={dir}"),
&format!("--output={db_path}"),
"--prunepaths=",
"--prunefs=",
]);
let mut group = c.benchmark_group("locate");
// Pattern matching nothing: forces a full decode + match of every entry.
group.bench_function("count_no_match", |b| {
b.iter(|| run(black_box(&["-d", db_path, "-c", "NONEXISTENT_TOKEN_XYZ"])));
});
// Substring matching many entries (count mode → no per-match output).
group.bench_function("count_many", |b| {
b.iter(|| run(black_box(&["-d", db_path, "-c", ".log"])));
});
// Basename matching (-b) restricts the comparison to the file name.
group.bench_function("basename", |b| {
b.iter(|| run(black_box(&["-d", db_path, "-c", "-b", "module"])));
});
// Case-insensitive matching (-i).
group.bench_function("ignore_case", |b| {
b.iter(|| run(black_box(&["-d", db_path, "-c", "-i", "MODULE"])));
});
// Regex matching (-r) over the whole path.
group.bench_function("regex", |b| {
b.iter(|| {
run(black_box(&[
"-d",
db_path,
"-c",
"-r",
r"module_[0-9]+\.rs",
]));
});
});
group.finish();
let _ = std::fs::remove_dir_all(&root);
let _ = std::fs::remove_file(&db);
}
criterion_group!(benches, bench_e2e);
}
#[cfg(unix)]
criterion::criterion_main!(bench::benches);
#[cfg(not(unix))]
fn main() {}
+110
View File
@@ -0,0 +1,110 @@
// Copyright 2024 the uutils developers
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
//! End-to-end benchmarks for `updatedb`, run through the real `updatedb_main`
//! entry point. Each case walks a generated directory tree (via the internal
//! `find`), encodes it into the LOCATE02 database format and writes the result
//! to a temp file, so the walk + front-coding + write dominate the measurement.
use std::path::{Path, PathBuf};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use findutils::updatedb::updatedb_main;
/// Run `updatedb` end-to-end. `args` are the arguments after the program name.
fn run(args: &[&str]) {
let mut argv: Vec<&str> = Vec::with_capacity(args.len() + 1);
argv.push("updatedb");
argv.extend_from_slice(args);
let _ = updatedb_main(&argv);
}
/// Build a directory tree to index. `depth` levels, each holding `dirs`
/// sub-directories and `files` files with varied names. Returns the root.
fn build_tree(depth: u32, dirs: u32, files: u32) -> PathBuf {
let root = std::env::temp_dir().join(format!("uu_updatedb_bench_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
populate(&root, depth, dirs, files, 0);
root
}
fn populate(dir: &Path, depth: u32, dirs: u32, files: u32, seed: u32) {
for f in 0..files {
let n = seed.wrapping_add(f);
let name = match n % 4 {
0 => format!("module_{n}.rs"),
1 => format!("data_{n}.txt"),
2 => format!("image_{n}.png"),
_ => format!("file_{n}.log"),
};
// Tiny files: updatedb only records names, so content size is irrelevant.
std::fs::write(dir.join(name), b"x").unwrap();
}
if depth == 0 {
return;
}
for d in 0..dirs {
let sub = dir.join(format!("dir_{d}"));
std::fs::create_dir_all(&sub).unwrap();
populate(
&sub,
depth - 1,
dirs,
files,
seed.wrapping_add((d + 1) * 31),
);
}
}
fn bench_e2e(c: &mut Criterion) {
// depth 4, 4 dirs/level, 25 files/dir → ~8.5k indexed paths.
let root = build_tree(4, 4, 25);
let dir = root.to_str().unwrap();
let db = std::env::temp_dir().join(format!("uu_updatedb_bench_{}.db", std::process::id()));
let db_path = db.to_str().unwrap();
let localpaths = format!("--localpaths={dir}");
let output = format!("--output={db_path}");
let mut group = c.benchmark_group("updatedb");
// Full build: walk, front-code and write the whole tree. Empty prune
// options keep the run deterministic regardless of where temp_dir lives.
group.bench_function("build_full", |b| {
b.iter(|| {
run(black_box(&[
&localpaths,
&output,
"--prunepaths=",
"--prunefs=",
]));
});
});
// Build with a prune clause that drops every `dir_0` subtree, exercising the
// -regex/-prune path updatedb hands to find.
group.bench_function("build_pruned", |b| {
b.iter(|| {
run(black_box(&[
&localpaths,
&output,
"--prunepaths=.*/dir_0",
"--prunefs=",
]));
});
});
group.finish();
let _ = std::fs::remove_dir_all(&root);
let _ = std::fs::remove_file(&db);
}
criterion_group!(benches, bench_e2e);
criterion_main!(benches);
+77
View File
@@ -0,0 +1,77 @@
// Copyright 2024 the uutils developers
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
//! End-to-end benchmarks for `xargs`, run through the real `xargs_main` entry
//! point. Input is supplied via `-a <file>` (rather than stdin) so the harness
//! can feed a fixed corpus without touching the process's stdin, and the
//! command is `true` so the measurement is dominated by `xargs`'s own work
//! (reading, splitting and batching arguments) rather than the child.
use std::path::PathBuf;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use findutils::xargs::xargs_main;
/// Run `xargs` end-to-end. `args` are the arguments after the program name.
/// The exit status is ignored — we only care about the work performed.
fn run(args: &[&str]) {
let mut argv: Vec<&str> = Vec::with_capacity(args.len() + 1);
argv.push("xargs");
argv.extend_from_slice(args);
let _ = xargs_main(&argv);
}
/// Write `count` short tokens to a temp file, joined by `sep`. Returns the path.
fn build_input(count: u32, sep: &str, tag: &str) -> PathBuf {
let mut content = String::new();
for i in 0..count {
if i > 0 {
content.push_str(sep);
}
content.push_str(&format!("item_{i}"));
}
let path = std::env::temp_dir().join(format!("uu_xargs_bench_{}_{tag}", std::process::id()));
std::fs::write(&path, content).unwrap();
path
}
fn bench_e2e(c: &mut Criterion) {
// Whitespace- and newline-separated corpora, plus a NUL-separated one for
// the `-0` path. A few thousand short tokens keeps parsing dominant while
// `true` batches stay to a handful of (untraced) spawns.
let ws = build_input(4000, " ", "ws");
let nul = build_input(4000, "\0", "nul");
let ws_path = ws.to_str().unwrap();
let nul_path = nul.to_str().unwrap();
let mut group = c.benchmark_group("xargs");
// Default whitespace splitting, single batch (fits one command line).
group.bench_function("split_whitespace", |b| {
b.iter(|| run(black_box(&["-a", ws_path, "true"])));
});
// NUL-delimited input (`find -print0 | xargs -0` shape).
group.bench_function("split_null", |b| {
b.iter(|| run(black_box(&["-0", "-a", nul_path, "true"])));
});
// Cap arguments per command with -n, exercising the batching path across
// several (untraced) invocations.
group.bench_function("batched_n", |b| {
b.iter(|| run(black_box(&["-a", ws_path, "-n", "1000", "true"])));
});
// Bound each command line by size with -s, another batching trigger.
group.bench_function("batched_size", |b| {
b.iter(|| run(black_box(&["-a", ws_path, "-s", "4096", "true"])));
});
group.finish();
let _ = std::fs::remove_file(&ws);
let _ = std::fs::remove_file(&nul);
}
criterion_group!(benches, bench_e2e);
criterion_main!(benches);
+6 -9
View File
@@ -174,9 +174,9 @@ impl From<walkdir::Error> for WalkError {
impl From<&walkdir::Error> for WalkError {
fn from(e: &walkdir::Error) -> Self {
Self {
path: e.path().map(|p| p.to_owned()),
path: e.path().map(std::borrow::ToOwned::to_owned),
depth: Some(e.depth()),
raw: e.io_error().and_then(|e| e.raw_os_error()),
raw: e.io_error().and_then(std::io::Error::raw_os_error),
}
}
}
@@ -190,8 +190,7 @@ impl From<WalkError> for io::Error {
impl From<&WalkError> for io::Error {
fn from(e: &WalkError) -> Self {
e.raw
.map(Self::from_raw_os_error)
.unwrap_or_else(|| ErrorKind::Other.into())
.map_or_else(|| ErrorKind::Other.into(), Self::from_raw_os_error)
}
}
@@ -279,8 +278,7 @@ impl WalkEntry {
// Path::file_name() only works if the last component is normal
path.components()
.next_back()
.map(|c| c.as_os_str())
.unwrap_or_else(|| path.as_os_str())
.map_or_else(|| path.as_os_str(), std::path::Component::as_os_str)
}
Entry::WalkDir(ent) => ent.file_name(),
}
@@ -311,7 +309,7 @@ impl WalkEntry {
Entry::Explicit(_, _) => Ok(self.get_metadata()?),
Entry::WalkDir(ent) => Ok(ent.metadata()?),
});
result.as_ref().map_err(|e| e.clone())
result.as_ref().map_err(std::clone::Clone::clone)
}
/// Get the file type of this entry.
@@ -319,8 +317,7 @@ impl WalkEntry {
match &self.inner {
Entry::Explicit(_, _) => self
.metadata()
.map(|m| m.file_type().into())
.unwrap_or(FileType::Unknown),
.map_or(FileType::Unknown, |m| m.file_type().into()),
Entry::WalkDir(ent) => ent.file_type().into(),
}
}
+41 -35
View File
@@ -18,8 +18,17 @@ enum Arg {
LiteralArg(OsString),
}
fn parse_arg(s: &str) -> Arg {
let parts = s.split("{}").collect::<Vec<_>>();
if parts.len() == 1 {
Arg::LiteralArg(OsString::from(s))
} else {
Arg::FileArg(parts.iter().map(OsString::from).collect())
}
}
pub struct SingleExecMatcher {
executable: String,
executable: Arg,
args: Vec<Arg>,
exec_in_parent_dir: bool,
interactive: bool,
@@ -31,7 +40,7 @@ impl SingleExecMatcher {
args: &[&str],
exec_in_parent_dir: bool,
) -> Result<Self, Box<dyn Error>> {
Self::new_impl(executable, args, exec_in_parent_dir, false)
Ok(Self::new_impl(executable, args, exec_in_parent_dir, false))
}
pub fn new_interactive(
@@ -39,7 +48,7 @@ impl SingleExecMatcher {
args: &[&str],
exec_in_parent_dir: bool,
) -> Result<Self, Box<dyn Error>> {
Self::new_impl(executable, args, exec_in_parent_dir, true)
Ok(Self::new_impl(executable, args, exec_in_parent_dir, true))
}
fn new_impl(
@@ -47,26 +56,15 @@ impl SingleExecMatcher {
args: &[&str],
exec_in_parent_dir: bool,
interactive: bool,
) -> Result<Self, Box<dyn Error>> {
let transformed_args = args
.iter()
.map(|&a| {
let parts = a.split("{}").collect::<Vec<_>>();
if parts.len() == 1 {
// No {} present
Arg::LiteralArg(OsString::from(a))
} else {
Arg::FileArg(parts.iter().map(OsString::from).collect())
}
})
.collect();
) -> Self {
let transformed_args = args.iter().map(|&a| parse_arg(a)).collect();
Ok(Self {
executable: executable.to_string(),
Self {
executable: parse_arg(executable),
args: transformed_args,
exec_in_parent_dir,
interactive,
})
}
}
}
@@ -82,28 +80,30 @@ impl Matcher for SingleExecMatcher {
file_info.path().to_path_buf()
};
let resolved_executable = match self.executable {
Arg::LiteralArg(ref a) => a.clone(),
Arg::FileArg(ref parts) => parts.join(path_to_file.as_os_str()),
};
if self.interactive {
let rendered_args: Vec<String> = self
.args
.iter()
.map(|arg| match arg {
Arg::LiteralArg(a) => a.to_string_lossy().into_owned(),
Arg::FileArg(parts) => parts
.join(path_to_file.as_os_str())
.to_string_lossy()
.into_owned(),
})
.collect();
let mut prompt_parts = vec![self.executable.clone()];
prompt_parts.extend(rendered_args);
let prompt = format!("< {} >? ", prompt_parts.join(" "));
// GNU find prints a fixed, abbreviated prompt of the form
// "< executable ... pathname > ? ". It does not render the
// substituted argument list, and always shows the full path of
// the entry being processed (even for -okdir, whose command runs
// with the "./basename" form).
let prompt = format!(
"< {} ... {} > ? ",
resolved_executable.to_string_lossy(),
file_info.path().to_string_lossy()
);
if !matcher_io.confirm(&prompt) {
return false;
}
}
let mut command = Command::new(&self.executable);
let mut command = Command::new(&resolved_executable);
for arg in &self.args {
match *arg {
Arg::LiteralArg(ref a) => command.arg(a.as_os_str()),
@@ -127,7 +127,13 @@ impl Matcher for SingleExecMatcher {
match command.status() {
Ok(status) => status.success(),
Err(e) => {
writeln!(&mut stderr(), "Failed to run {}: {}", self.executable, e).unwrap();
writeln!(
&mut stderr(),
"Failed to run {}: {}",
resolved_executable.to_string_lossy(),
e
)
.unwrap();
false
}
}
+38 -12
View File
@@ -42,8 +42,18 @@ pub fn get_file_system_type(path: &Path, cache: &RefCell<Option<Cache>>) -> URes
Ok(metadata) => metadata,
Err(err) => Err(err)?,
};
let dev_id = metadata.dev().to_string();
fs_type_for_dev(metadata.dev().to_string(), cache)
}
/// Resolve a device id to its filesystem type, consulting (and updating) `cache`.
///
/// Callers are expected to obtain `dev_id` from metadata they already hold (e.g. the cached
/// [`WalkEntry::metadata`]), so that no extra `stat`/`statx` syscall is issued per entry.
///
/// This is only supported on Unix.
#[cfg(unix)]
pub fn fs_type_for_dev(dev_id: String, cache: &RefCell<Option<Cache>>) -> UResult<String> {
if let Some(cache) = cache.borrow().as_ref() {
if cache.dev_id == dev_id {
return Ok(cache.fs_type.clone());
@@ -94,18 +104,34 @@ impl FileSystemMatcher {
impl Matcher for FileSystemMatcher {
#[cfg(unix)]
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
match get_file_system_type(file_info.path(), &self.cache) {
Ok(result) => result == self.fs_text,
Err(_) => {
writeln!(
&mut stderr(),
"Error getting filesystem type for {}",
file_info.path().to_string_lossy()
)
.unwrap();
use std::os::unix::fs::MetadataExt;
false
}
// Reuse the metadata already cached on the entry (a single shared `statx` per entry)
// rather than issuing a fresh `lstat`/`statx` here. With several `-fstype` clauses (as
// `updatedb` builds) this turns N stats per file into one.
let Ok(metadata) = file_info.metadata() else {
writeln!(
&mut stderr(),
"Error getting filesystem type for {}",
file_info.path().to_string_lossy()
)
.unwrap();
return false;
};
let dev_id = metadata.dev().to_string();
if let Ok(result) = fs_type_for_dev(dev_id, &self.cache) {
result == self.fs_text
} else {
writeln!(
&mut stderr(),
"Error getting filesystem type for {}",
file_info.path().to_string_lossy()
)
.unwrap();
false
}
}
+14 -1
View File
@@ -83,7 +83,10 @@ fn extract_bracket_expr(pattern: &str) -> Option<(String, &str)> {
if matches!(delim, '.' | '=' | ':') {
let rest = chars.as_str();
let end = rest.find([delim, ']'])? + 2;
// Search for the two-byte closer `<delim>]` (e.g. `:]`);
// matching either byte alone let `+ 2` overshoot a char boundary.
let closer = format!("{delim}]");
let end = rest.find(closer.as_str())? + 2;
expr.push_str(&rest[..end]);
chars = rest[end..].chars();
}
@@ -222,6 +225,16 @@ mod tests {
assert_glob_regex(r"foo[bar[!baz", r"foo\[bar\[!baz");
}
#[test]
fn malformed_posix_class_with_multibyte_char() {
for pat in ["[[:]é", "[[:a]é", "[[.]é", "[[:é]", "[[=]😀"] {
assert!(
glob_to_regex(pat).is_some(),
"panicked or rejected: {pat:?}"
);
}
}
#[test]
fn incomplete_escape() {
assert_eq!(glob_to_regex(r"foo\"), None);
+7 -5
View File
@@ -11,6 +11,8 @@ use nix::unistd::Group;
use std::os::unix::fs::MetadataExt;
pub struct GroupMatcher {
// Only read on Unix; the non-Unix `matches` implementation is a stub.
#[cfg_attr(not(unix), allow(dead_code))]
gid: ComparableValue,
}
@@ -31,7 +33,7 @@ impl GroupMatcher {
Self { gid }
}
#[cfg(windows)]
#[cfg(not(unix))]
pub fn from_group_name(_group: &str) -> Option<Self> {
None
}
@@ -46,10 +48,10 @@ impl Matcher for GroupMatcher {
}
}
#[cfg(windows)]
#[cfg(not(unix))]
fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
// The user group acquisition function for Windows systems is not implemented in MetadataExt,
// so it is somewhat difficult to implement it. :(
// The user group acquisition function for non-Unix systems is not implemented in
// MetadataExt, so it is somewhat difficult to implement it. :(
false
}
}
@@ -80,7 +82,7 @@ impl Matcher for NoGroupMatcher {
false
}
#[cfg(windows)]
#[cfg(not(unix))]
fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
false
}
+24 -22
View File
@@ -126,7 +126,6 @@ impl Ls {
mut out: impl Write,
print_error_message: bool,
) {
use nix::unistd::{Gid, Group, Uid, User};
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let metadata = file_info.metadata().unwrap();
@@ -150,14 +149,13 @@ impl Ls {
let permission =
{ format_permissions(metadata.permissions().mode() as uucore::libc::mode_t) };
let hard_links = metadata.nlink();
let user = {
let uid = metadata.uid();
User::from_uid(Uid::from_raw(uid)).unwrap().unwrap().name
};
let group = {
let gid = metadata.gid();
Group::from_gid(Gid::from_raw(gid)).unwrap().unwrap().name
};
// Fall back to the numeric id when the uid/gid has no passwd/group entry
// (unmapped owner) — matching GNU find, which never crashes. uucore::entries
// serializes the non-thread-safe getpwuid/getgrgid behind a mutex.
let user =
uucore::entries::uid2usr(metadata.uid()).unwrap_or_else(|_| metadata.uid().to_string());
let group =
uucore::entries::gid2grp(metadata.gid()).unwrap_or_else(|_| metadata.gid().to_string());
let size = metadata.size();
let last_modified = {
let system_time = metadata.modified().unwrap();
@@ -195,7 +193,7 @@ impl Ls {
}
}
#[cfg(windows)]
#[cfg(not(unix))]
fn print(
&self,
file_info: &WalkEntry,
@@ -203,16 +201,15 @@ impl Ls {
mut out: impl Write,
print_error_message: bool,
) {
use std::os::windows::fs::MetadataExt;
// Non-Unix targets (Windows, wasm, ...) don't expose inode, owner,
// group or Unix permission bits, so those columns are left blank.
let metadata = file_info.metadata().unwrap();
let inode_number = 0;
let size = metadata.len();
let number_of_blocks = {
let size = metadata.file_size();
let number_of_blocks = size / 1024;
let remainder = number_of_blocks % 4;
if remainder == 0 {
if number_of_blocks == 0 {
4
@@ -220,14 +217,19 @@ impl Ls {
number_of_blocks
}
} else {
number_of_blocks + (4 - (remainder))
number_of_blocks + (4 - remainder)
}
};
let permission = { format_permissions(metadata.file_attributes()) };
#[cfg(windows)]
let permission = {
use std::os::windows::fs::MetadataExt;
format_permissions(metadata.file_attributes())
};
#[cfg(not(windows))]
let permission = "?---------";
let hard_links = 0;
let user = 0;
let group = 0;
let size = metadata.file_size();
let last_modified = {
let system_time = metadata.modified().unwrap();
let now_utc: DateTime<chrono::Utc> = system_time.into();
@@ -235,9 +237,9 @@ impl Ls {
};
let path = file_info.path().to_string_lossy();
match write!(
match writeln!(
out,
" {:<4} {:>6} {:<10} {:>3} {:<8} {:<8} {:>8} {} {}\n",
" {:<4} {:>6} {:<10} {:>3} {:<8} {:<8} {:>8} {} {}",
inode_number,
number_of_blocks,
permission,
@@ -292,15 +294,15 @@ mod tests {
fn test_format_permissions() {
use super::format_permissions;
let mode: uucore::libc::mode_t = 0o100644;
let mode: uucore::libc::mode_t = 0o100_644;
let expected = "-rw-r--r--";
assert_eq!(format_permissions(mode), expected);
let mode: uucore::libc::mode_t = 0o040755;
let mode: uucore::libc::mode_t = 0o040_755;
let expected = "drwxr-xr-x";
assert_eq!(format_permissions(mode), expected);
let mode: uucore::libc::mode_t = 0o100777;
let mode: uucore::libc::mode_t = 0o100_777;
let expected = "-rwxrwxrwx";
assert_eq!(format_permissions(mode), expected);
}
+4 -3
View File
@@ -74,6 +74,7 @@ use std::{
use super::{Config, Dependencies};
pub use entry::{FileType, WalkEntry, WalkError};
pub use regex::RegexType;
/// Symlink following mode.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -399,7 +400,7 @@ fn parse_date_str_to_timestamps(date_str: &str) -> Option<i64> {
///
/// Additionally, there is support for the -anewer and -cnewer short arguments. as follows:
/// 1. -anewer is equivalent to -neweram
/// 2. -cnewer is equivalent to - newercm
/// 2. -cnewer is equivalent to -newercm
///
/// If -newer is used it will be resolved to -newermm.
fn parse_str_to_newer_args(input: &str) -> Option<(String, String)> {
@@ -1031,10 +1032,10 @@ fn parse_files0_args(config: &mut Config) -> Result<(), Box<dyn Error>> {
let mut string_segments: Vec<String> = buffer_split
.iter()
.filter_map(|s| std::str::from_utf8(s).ok())
.map(|s| s.to_string())
.map(std::string::ToString::to_string)
.collect();
// empty starting point checker
if string_segments.iter().any(|s| s.is_empty()) {
if string_segments.iter().any(std::string::String::is_empty) {
eprintln!("find: invalid zero-length file name");
// remove the empty ones so as to avoid file not found error
string_segments.retain(|s| !s.is_empty());
+1 -1
View File
@@ -31,7 +31,7 @@ impl Matcher for NameMatcher {
self.pattern.matches(&name)
}
#[cfg(windows)]
#[cfg(not(unix))]
self.pattern.matches(&name)
}
}
+2 -2
View File
@@ -91,7 +91,7 @@ impl PermMatcher {
}
#[cfg(not(unix))]
pub fn new(_dummy_pattern: &str) -> Result<PermMatcher, Box<dyn Error>> {
pub fn new(_dummy_pattern: &str) -> Result<Self, Box<dyn Error>> {
Err(From::from(
"Permission matching is not available on this platform",
))
@@ -132,7 +132,7 @@ impl Matcher for PermMatcher {
"Permission matching not available on this platform!"
)
.unwrap();
return false;
false
}
}
+42 -11
View File
@@ -17,6 +17,7 @@ use super::{FileType, Matcher, MatcherIO, WalkEntry, WalkError};
#[cfg(unix)]
use std::os::unix::prelude::MetadataExt;
#[cfg(unix)]
const STANDARD_BLOCK_SIZE: u64 = 512;
#[derive(Debug, PartialEq, Eq)]
@@ -167,16 +168,21 @@ impl FormatStringParser<'_> {
// Try parsing an octal sequence first.
let first = self.front()?;
if first.is_digit(OCTAL_RADIX) {
if let Ok(code) = self.peek(OCTAL_LEN).and_then(|octal| {
u32::from_str_radix(octal, OCTAL_RADIX).map_err(std::convert::Into::into)
}) {
// safe to unwrap: .peek() already succeeded above.
let octal = self.advance_by(OCTAL_LEN).unwrap();
return match char::from_u32(code) {
Some(c) => Ok(FormatComponent::Literal(c.to_string())),
None => Err(format!("Invalid character value: \\{octal}").into()),
};
}
// A GNU octal escape is 1 to 3 octal digits. Consume only the leading
// octal digits (which are ASCII), rather than slicing a fixed 3 bytes
// that can land inside a following multibyte char.
let octal: String = self
.string
.chars()
.take(OCTAL_LEN)
.take_while(|c| c.is_digit(OCTAL_RADIX))
.collect();
let code = u32::from_str_radix(&octal, OCTAL_RADIX)?;
self.advance_by(octal.len())?;
return match char::from_u32(code) {
Some(c) => Ok(FormatComponent::Literal(c.to_string())),
None => Err(format!("Invalid character value: \\{octal}").into()),
};
}
self.advance_one()?;
@@ -205,7 +211,7 @@ impl FormatStringParser<'_> {
let start = self.string;
let mut digits = 0;
while self.front().map(|c| c.is_ascii_digit()).unwrap_or(false) {
while self.front().is_ok_and(|c| c.is_ascii_digit()) {
digits += 1;
// safe to unwrap: the front() check already succeeded above.
self.advance_one().unwrap();
@@ -688,6 +694,31 @@ mod tests {
assert!(FormatString::parse("\\").is_err());
}
#[test]
fn test_parse_octal_escape_before_multibyte_char() {
assert_eq!(
FormatString::parse("\\0€").unwrap().components,
vec![
FormatComponent::Literal("\0".to_owned()),
FormatComponent::Literal("".to_owned()),
]
);
assert_eq!(
FormatString::parse("\\1😀").unwrap().components,
vec![
FormatComponent::Literal("\u{1}".to_owned()),
FormatComponent::Literal("😀".to_owned()),
]
);
assert_eq!(
FormatString::parse("\\00é").unwrap().components,
vec![
FormatComponent::Literal("\0".to_owned()),
FormatComponent::Literal("é".to_owned()),
]
);
}
#[test]
fn test_parse_formatting() {
fn unaligned_directive(directive: FormatDirective) -> FormatComponent {

Some files were not shown because too many files have changed in this diff Show More