mirror of
https://github.com/uutils/findutils.git
synced 2026-06-10 15:48:30 -07:00
Compare commits
42 Commits
latest-commit
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 795d29ab35 | |||
| c619fbeb82 | |||
| a70f5ccd7c | |||
| 5992901805 | |||
| 782744f35c | |||
| c100ac226f | |||
| 5bf236b891 | |||
| 001966f326 | |||
| 53980035af | |||
| e2d84e98d5 | |||
| 865ab0991e | |||
| 8add685cc6 | |||
| c4afeab1bc | |||
| 62deaf6e60 | |||
| 1be5541c74 | |||
| 5d0e7add34 | |||
| 42f8a9af36 | |||
| 664a9c23e6 | |||
| fd4bd6acfa | |||
| fa4f90befb | |||
| 7deeee6fdf | |||
| 51a50022c2 | |||
| 600f3af140 | |||
| a63482b8cf | |||
| 47796303ca | |||
| baa6ce5d92 | |||
| 9f33c14fc9 | |||
| a80620c88f | |||
| a2c75f94ca | |||
| 44fbc59263 | |||
| fb9f8df047 | |||
| 2044d1e31d | |||
| 0494ba1be4 | |||
| fc446aba0b | |||
| 33703b79d8 | |||
| c5ff1bbb6b | |||
| e7c34ab12f | |||
| 1a77a83432 | |||
| a65160f724 | |||
| 435f00d491 | |||
| db941e7fba | |||
| 5bef01f72f |
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+66
-11
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
@@ -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() {}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -31,7 +31,7 @@ impl Matcher for NameMatcher {
|
||||
self.pattern.matches(&name)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[cfg(not(unix))]
|
||||
self.pattern.matches(&name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user