findutils-0.4.2/.cargo_vcs_info.json0000644000000001360000000000100130400ustar { "git": { "sha1": "c6a8a799d34869f0fc6d1250661fd2e68f98b2de" }, "path_in_vcs": "" }findutils-0.4.2/.github/dependabot.yml000064400000000000000000000004131046102023000160160ustar 00000000000000version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "daily" open-pull-requests-limit: 5 - package-ecosystem: "github-actions" directory: "/" schedule: interval: daily open-pull-requests-limit: 5 findutils-0.4.2/.github/workflows/ci.yml000064400000000000000000000112771046102023000163530ustar 00000000000000on: [push, pull_request] name: Basic CI jobs: check: name: cargo check runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v3 - name: Install `rust` toolchain run: | ## Install `rust` toolchain rustup toolchain install stable --no-self-update -c rustfmt --profile minimal rustup default stable # For bindgen: https://github.com/rust-lang/rust-bindgen/issues/1797 - uses: KyleMayes/install-llvm-action@v1 if: matrix.os == 'windows-latest' with: version: "11.0" directory: ${{ runner.temp }}/llvm - run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV if: matrix.os == 'windows-latest' - name: Check run: | cargo check --all --all-features test: name: cargo test runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v3 - name: Install `rust` toolchain run: | ## Install `rust` toolchain rustup toolchain install stable --no-self-update -c rustfmt --profile minimal rustup default stable # For bindgen: https://github.com/rust-lang/rust-bindgen/issues/1797 - uses: KyleMayes/install-llvm-action@v1 if: matrix.os == 'windows-latest' with: version: "11.0" directory: ${{ runner.temp }}/llvm - run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV if: matrix.os == 'windows-latest' - name: Check run: | cargo check fmt: name: cargo fmt --all -- --check runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install `rust` toolchain run: | ## Install `rust` toolchain rustup toolchain install stable --no-self-update -c rustfmt --profile minimal rustup default stable - run: rustup component add rustfmt - name: cargo fmt run: | cargo fmt --all -- --check clippy: name: cargo clippy -- -D warnings runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install `rust` toolchain run: | ## Install `rust` toolchain rustup toolchain install stable --no-self-update -c rustfmt --profile minimal rustup default stable - run: rustup component add clippy - name: cargo clippy run: | cargo clippy -- -D warnings grcov: name: Code coverage runs-on: ${{ matrix.os }} strategy: matrix: os: - ubuntu-latest toolchain: - nightly cargo_flags: - "--all-features" steps: - name: Checkout source code uses: actions/checkout@v3 - name: Install `rust` toolchain run: | ## Install `rust` toolchain rustup toolchain install nightly --no-self-update -c rustfmt --profile minimal rustup default nightly - name: "`grcov` ~ install" run: cargo install grcov - name: cargo test run: | cargo test --all --no-fail-fast ${{ matrix.cargo_flags }} env: CARGO_INCREMENTAL: "0" RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort -Cdebug-assertions=off' RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort -Cdebug-assertions=off' - name: Generate coverage data id: grcov run: | grcov target/debug/ \ --branch \ --llvm \ --source-dir . \ --output-path lcov.info \ --ignore-not-existing \ --excl-line "#\\[derive\\(" \ --excl-br-line "#\\[derive\\(" \ --excl-start "#\\[cfg\\(test\\)\\]" \ --excl-br-start "#\\[cfg\\(test\\)\\]" \ --commit-sha ${{ github.sha }} \ --service-job-id ${{ github.job }} \ --service-name "GitHub Actions" \ --service-number ${{ github.run_id }} - name: Upload coverage as artifact uses: actions/upload-artifact@v3 with: name: lcov.info # path: ${{ steps.grcov.outputs.report }} path: lcov.info - name: Upload coverage to codecov.io uses: codecov/codecov-action@v3 with: # file: ${{ steps.grcov.outputs.report }} file: lcov.info fail_ci_if_error: true findutils-0.4.2/.github/workflows/compat.yml000064400000000000000000000102131046102023000172300ustar 00000000000000on: [push, pull_request] name: External testsuites jobs: gnu-tests: name: Run GNU findutils tests runs-on: ubuntu-latest steps: - name: Checkout findutils uses: actions/checkout@v3 with: path: findutils - name: Checkout GNU findutils uses: actions/checkout@v3 with: repository: gnu-mirror-unofficial/findutils path: findutils.gnu ref: 5768a03ddfb5e18b1682e339d6cdd24ff721c510 submodules: true - name: Install `rust` toolchain run: | ## Install `rust` toolchain rustup toolchain install stable --no-self-update -c rustfmt --profile minimal rustup default stable - name: Install dependencies shell: bash run: | # Enable sources & install dependencies sudo find /etc/apt/sources.list* -type f -exec sed -i 'p; s/^deb /deb-src /' '{}' + sudo apt-get update sudo apt-get build-dep findutils - name: Run GNU tests shell: bash run: | cd findutils bash util/build-gnu.sh ||: - name: Extract testing info shell: bash run: | - uses: actions/upload-artifact@v3 with: name: gnu-test-report path: | findutils.gnu/find/testsuite/*.log findutils.gnu/xargs/testsuite/*.log findutils.gnu/tests/**/*.log - uses: actions/upload-artifact@v3 with: name: gnu-result path: gnu-result.json - name: Download the result uses: dawidd6/action-download-artifact@v2 with: workflow: compat.yml workflow_conclusion: completed name: gnu-result repo: uutils/findutils branch: main path: dl - name: Download the log uses: dawidd6/action-download-artifact@v2 with: workflow: compat.yml workflow_conclusion: completed name: gnu-test-report repo: uutils/findutils branch: main path: dl - name: Compare failing tests against master shell: bash run: | ./findutils/util/diff-gnu.sh ./dl ./findutils.gnu - name: Compare against main results shell: bash run: | mv dl/gnu-result.json latest-gnu-result.json python findutils/util/compare_gnu_result.py bfs-tests: name: Run BFS tests runs-on: ubuntu-latest steps: - name: Checkout findutils uses: actions/checkout@v3 with: path: findutils - name: Checkout BFS uses: actions/checkout@v3 with: repository: tavianator/bfs path: bfs ref: '2.6.2' - name: Install `rust` toolchain run: | ## Install `rust` toolchain rustup toolchain install stable --no-self-update -c rustfmt --profile minimal rustup default stable - name: Install dependencies shell: bash run: | # Enable sources & install dependencies sudo find /etc/apt/sources.list* -type f -exec sed -i 'p; s/^deb /deb-src /' '{}' + sudo apt-get update sudo apt-get build-dep bfs - name: Run BFS tests shell: bash run: | cd findutils bash util/build-bfs.sh ||: - uses: actions/upload-artifact@v3 with: name: bfs-test-report path: bfs/tests.log - uses: actions/upload-artifact@v3 with: name: bfs-result path: bfs-result.json - name: Download the result uses: dawidd6/action-download-artifact@v2 with: workflow: compat.yml workflow_conclusion: completed name: bfs-result repo: uutils/findutils branch: main path: dl - name: Download the log uses: dawidd6/action-download-artifact@v2 with: workflow: compat.yml workflow_conclusion: completed name: bfs-test-report repo: uutils/findutils branch: main path: dl - name: Compare failing tests against main shell: bash run: | ./findutils/util/diff-bfs.sh dl/tests.log bfs/tests.log - name: Compare against main results shell: bash run: | mv dl/bfs-result.json latest-bfs-result.json python findutils/util/compare_bfs_result.py findutils-0.4.2/.gitignore000064400000000000000000000001031046102023000136120ustar 00000000000000target .gitignore .project .cargo .settings test_data/links/link-* findutils-0.4.2/.pre-commit-config.yaml000064400000000000000000000010561046102023000161130ustar 00000000000000repos: - repo: local hooks: - id: rust-linting name: Rust linting description: Run cargo fmt on files included in the commit. entry: cargo +nightly fmt -- pass_filenames: true types: [file, rust] language: system - id: rust-clippy name: Rust clippy description: Run cargo clippy on files included in the commit. entry: cargo +nightly clippy --workspace --all-targets --all-features -- pass_filenames: false types: [file, rust] language: system findutils-0.4.2/Cargo.lock0000644000001222750000000000100110240ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "ansi_term" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ "winapi", ] [[package]] name = "anstream" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is-terminal", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" [[package]] name = "anstyle-parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ "windows-sys 0.48.0", ] [[package]] name = "anstyle-wincon" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" dependencies = [ "anstyle", "windows-sys 0.48.0", ] [[package]] name = "arrayref" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "arrayvec" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "assert_cmd" version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86d6b683edf8d1119fe420a94f8a7e389239666aa72e65495d91c00462510151" dependencies = [ "anstyle", "bstr", "doc-comment", "predicates", "predicates-core", "predicates-tree", "wait-timeout", ] [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi 0.1.19", "libc", "winapi", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bindgen" version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" dependencies = [ "bitflags 1.3.2", "cexpr", "clang-sys", "clap 2.34.0", "env_logger", "lazy_static", "lazycell", "log", "peeking_take_while", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", "which", ] [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" [[package]] name = "blake2b_simd" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c2f0dc9a68c6317d884f97cc36cf5a3d20ba14ce404227df55e1af708ab04bc" dependencies = [ "arrayref", "arrayvec", "constant_time_eq 0.2.6", ] [[package]] name = "blake3" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "199c42ab6972d92c9f8995f086273d25c42fc0f7b2a1fcefba465c1352d25ba5" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq 0.3.0", "digest", ] [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bstr" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fca0852af221f458706eb0725c03e4ed6c46af9ac98e6a689d5e634215d594dd" dependencies = [ "memchr", "once_cell", "regex-automata", "serde", ] [[package]] name = "bumpalo" version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "byteorder" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" [[package]] name = "cc" version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" [[package]] name = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "time 0.1.43", "wasm-bindgen", "winapi", ] [[package]] name = "clang-sys" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa66045b9cb23c2e9c1520732030608b02ee07e5cfaa5a521ec15ded7fa24c90" dependencies = [ "glob", "libc", "libloading", ] [[package]] name = "clap" version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "ansi_term", "atty", "bitflags 1.3.2", "strsim 0.8.0", "textwrap", "unicode-width", "vec_map", ] [[package]] name = "clap" version = "4.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" dependencies = [ "anstream", "anstyle", "clap_lex", "once_cell", "strsim 0.10.0", "terminal_size", ] [[package]] name = "clap_lex" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "constant_time_eq" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" [[package]] name = "constant_time_eq" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" [[package]] name = "core-foundation-sys" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cpufeatures" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "dashmap" version = "5.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3495912c9c1ccf2e18976439f4443f3fee0fd61f424ff99fde6a66b15ecb448f" dependencies = [ "cfg-if", "hashbrown", "lock_api", "parking_lot_core", ] [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dunce" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] name = "either" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "env_logger" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" dependencies = [ "atty", "humantime", "log", "regex", "termcolor", ] [[package]] name = "errno" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", "windows-sys 0.48.0", ] [[package]] name = "errno-dragonfly" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" dependencies = [ "cc", "libc", ] [[package]] name = "faccess" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5" dependencies = [ "bitflags 1.3.2", "libc", "winapi", ] [[package]] name = "fastrand" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" dependencies = [ "instant", ] [[package]] name = "filetime" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" dependencies = [ "cfg-if", "libc", "redox_syscall 0.2.10", "windows-sys 0.48.0", ] [[package]] name = "findutils" version = "0.4.2" dependencies = [ "assert_cmd", "chrono", "clap 2.34.0", "faccess", "filetime", "nix", "once_cell", "onig", "predicates", "regex", "serial_test", "tempfile", "uucore", "walkdir", ] [[package]] name = "float-cmp" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" dependencies = [ "num-traits", ] [[package]] name = "futures" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-executor" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-sink" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-util" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "hermit-abi" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "iana-time-zone" version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c495f162af0bf17656d0014a0eded5f3cd2f365fdd204548c2869db89359dc7" dependencies = [ "android_system_properties", "core-foundation-sys", "js-sys", "once_cell", "wasm-bindgen", "winapi", ] [[package]] name = "instant" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" dependencies = [ "cfg-if", ] [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi 0.3.1", "libc", "windows-sys 0.48.0", ] [[package]] name = "is-terminal" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi 0.3.1", "rustix 0.38.4", "windows-sys 0.48.0", ] [[package]] name = "itertools" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" dependencies = [ "wasm-bindgen", ] [[package]] name = "keccak" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" dependencies = [ "cpufeatures", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lazycell" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libloading" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52" dependencies = [ "cfg-if", "winapi", ] [[package]] name = "linux-raw-sys" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" [[package]] name = "lock_api" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ "cfg-if", ] [[package]] name = "md-5" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" dependencies = [ "digest", ] [[package]] name = "memchr" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" [[package]] name = "memoffset" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "nix" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", "memoffset", "pin-utils", "static_assertions", ] [[package]] name = "nom" version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "normalize-line-endings" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "num-traits" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", ] [[package]] name = "num_threads" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" dependencies = [ "libc", ] [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "onig" version = "6.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" dependencies = [ "bitflags 1.3.2", "libc", "once_cell", "onig_sys", ] [[package]] name = "onig_sys" version = "69.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" dependencies = [ "bindgen", "cc", "pkg-config", ] [[package]] name = "os_display" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a6229bad892b46b0dcfaaeb18ad0d2e56400f5aaea05b768bde96e73676cf75" dependencies = [ "unicode-width", ] [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" dependencies = [ "cfg-if", "libc", "redox_syscall 0.2.10", "smallvec", "windows-sys 0.36.1", ] [[package]] name = "peeking_take_while" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "pin-project-lite" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" [[package]] name = "predicates" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09963355b9f467184c04017ced4a2ba2d75cbcb4e7462690d388233253d4b1a9" dependencies = [ "anstyle", "difflib", "float-cmp", "itertools", "normalize-line-endings", "predicates-core", "regex", ] [[package]] name = "predicates-core" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" [[package]] name = "predicates-tree" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aee95d988ee893cb35c06b148c80ed2cd52c8eea927f50ba7a0be1a786aeab73" dependencies = [ "predicates-core", "treeline", ] [[package]] name = "proc-macro2" version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_syscall" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "regex" version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" dependencies = [ "byteorder", ] [[package]] name = "regex-syntax" version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" version = "0.37.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", "linux-raw-sys 0.3.8", "windows-sys 0.48.0", ] [[package]] name = "rustix" version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" dependencies = [ "bitflags 2.3.3", "errno", "libc", "linux-raw-sys 0.4.3", "windows-sys 0.48.0", ] [[package]] name = "same-file" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f20c4be53a8a1ff4c1f1b2bd14570d2f634628709752f0702ecdd2b3f9a5267" dependencies = [ "winapi-util", ] [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" [[package]] name = "serial_test" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" dependencies = [ "dashmap", "futures", "lazy_static", "log", "parking_lot", "serial_test_derive", ] [[package]] name = "serial_test_derive" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", "syn 2.0.18", ] [[package]] name = "sha1" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha2" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha3" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ "digest", "keccak", ] [[package]] name = "shlex" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[package]] name = "slab" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" [[package]] name = "sm3" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" dependencies = [ "digest", ] [[package]] name = "smallvec" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] [[package]] name = "syn" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ "autocfg", "cfg-if", "fastrand", "redox_syscall 0.3.5", "rustix 0.37.20", "windows-sys 0.48.0", ] [[package]] name = "termcolor" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e" dependencies = [ "wincolor", ] [[package]] name = "terminal_size" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" dependencies = [ "rustix 0.37.20", "windows-sys 0.48.0", ] [[package]] name = "textwrap" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ "unicode-width", ] [[package]] name = "time" version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", "winapi", ] [[package]] name = "time" version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" dependencies = [ "itoa", "libc", "num_threads", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" dependencies = [ "time-core", ] [[package]] name = "treeline" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" [[package]] name = "typenum" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicode-ident" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-width" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uucore" version = "0.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fcf442f09b351d59f54df1f84cbbba7299028c6b194c712683448b732bafcb7" dependencies = [ "blake2b_simd", "blake3", "clap 4.3.19", "digest", "dunce", "glob", "hex", "libc", "md-5", "memchr", "nix", "once_cell", "os_display", "sha1", "sha2", "sha3", "sm3", "time 0.3.23", "uucore_procs", "wild", "winapi-util", "windows-sys 0.48.0", ] [[package]] name = "uucore_procs" version = "0.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6689d54dd59b145ae94458007cb0508a741716ee8dc494b45d129fb83d4d46" dependencies = [ "proc-macro2", "quote", "uuhelp_parser", ] [[package]] name = "uuhelp_parser" version = "0.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d84a98929941eee8952bed33fe3e7c8731d2596ad64eef53c4c7c6fbae828e9" [[package]] name = "vec_map" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wait-timeout" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" dependencies = [ "libc", ] [[package]] name = "walkdir" version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasm-bindgen" version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn 1.0.94", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" dependencies = [ "proc-macro2", "quote", "syn 1.0.94", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" [[package]] name = "which" version = "4.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" dependencies = [ "either", "lazy_static", "libc", ] [[package]] name = "wild" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b116685a6be0c52f5a103334cbff26db643826c7b3735fc0a3ba9871310a74" dependencies = [ "glob", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "wincolor" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df6193e795abf9e39a5e4926d13ed002dbeca334fd7299398c372be918fd04" dependencies = [ "winapi", "winapi-util", ] [[package]] name = "windows-sys" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ "windows_aarch64_msvc 0.36.1", "windows_i686_gnu 0.36.1", "windows_i686_msvc 0.36.1", "windows_x86_64_gnu 0.36.1", "windows_x86_64_msvc 0.36.1", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc 0.48.0", "windows_i686_gnu 0.48.0", "windows_i686_msvc 0.48.0", "windows_x86_64_gnu 0.48.0", "windows_x86_64_gnullvm", "windows_x86_64_msvc 0.48.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[package]] name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" findutils-0.4.2/Cargo.toml0000644000000030570000000000100110430ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "findutils" version = "0.4.2" authors = ["uutils developers"] description = "Rust implementation of GNU findutils" homepage = "https://github.com/uutils/findutils" readme = "README.md" license = "MIT" repository = "https://github.com/uutils/findutils" [[bin]] name = "find" path = "src/find/main.rs" [[bin]] name = "xargs" path = "src/xargs/main.rs" [[bin]] name = "testing-commandline" path = "src/testing/commandline/main.rs" [dependencies.chrono] version = "0.4" [dependencies.clap] version = "2.34" [dependencies.faccess] version = "0.2.4" [dependencies.once_cell] version = "1.18" [dependencies.onig] version = "6.4" [dependencies.regex] version = "1.7" [dependencies.uucore] version = "0.0.20" features = [ "entries", "fs", "fsext", "mode", ] [dependencies.walkdir] version = "2.3" [dev-dependencies.assert_cmd] version = "2" [dev-dependencies.filetime] version = "0.2" [dev-dependencies.nix] version = "0.26" [dev-dependencies.predicates] version = "3" [dev-dependencies.serial_test] version = "2.0" [dev-dependencies.tempfile] version = "3" findutils-0.4.2/Cargo.toml.orig000064400000000000000000000014241046102023000145200ustar 00000000000000[package] name = "findutils" version = "0.4.2" homepage = "https://github.com/uutils/findutils" repository = "https://github.com/uutils/findutils" edition = "2018" license = "MIT" readme = "README.md" description = "Rust implementation of GNU findutils" authors = ["uutils developers"] [dependencies] chrono = "0.4" clap = "2.34" faccess = "0.2.4" walkdir = "2.3" regex = "1.7" once_cell = "1.18" onig = "6.4" uucore = { version = "0.0.20", features = ["entries", "fs", "fsext", "mode"] } [dev-dependencies] assert_cmd = "2" filetime = "0.2" nix = "0.26" predicates = "3" serial_test = "2.0" tempfile = "3" [[bin]] name = "find" path = "src/find/main.rs" [[bin]] name = "xargs" path = "src/xargs/main.rs" [[bin]] name = "testing-commandline" path = "src/testing/commandline/main.rs" findutils-0.4.2/LICENSE000064400000000000000000000020331046102023000126330ustar 00000000000000Copyright (c) Google Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. findutils-0.4.2/README.md000064400000000000000000000016411046102023000131110ustar 00000000000000# findutils [![Crates.io](https://img.shields.io/crates/v/findutils.svg)](https://crates.io/crates/findutils) [![dependency status](https://deps.rs/repo/github/uutils/findutils/status.svg)](https://deps.rs/repo/github/uutils/findutils) [![codecov](https://codecov.io/gh/uutils/findutils/branch/master/graph/badge.svg)](https://codecov.io/gh/uutils/findutils) Rust implementation of [GNU findutils](https://www.gnu.org/software/findutils/). ## Run the GNU testsuite on rust/findutils: ``` bash util/build-gnu.sh # To run a specific test: bash util/build-gnu.sh tests/misc/help-version.sh ``` ## Comparing with GNU ![Evolution over time - GNU testsuite](https://github.com/uutils/findutils-tracking/blob/main/gnu-results.png?raw=true) ![Evolution over time - BFS testsuite](https://github.com/uutils/findutils-tracking/blob/main/bfs-results.png?raw=true) For more details, see https://github.com/uutils/findutils-tracking/ findutils-0.4.2/src/find/main.rs000064400000000000000000000007201046102023000146300ustar 00000000000000// Copyright 2017 Google Inc. // // 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. fn main() { let args = std::env::args().collect::>(); let strs: Vec<&str> = args.iter().map(std::convert::AsRef::as_ref).collect(); let deps = findutils::find::StandardDependencies::new(); std::process::exit(findutils::find::find_main(&strs, &deps)); } findutils-0.4.2/src/find/matchers/access.rs000064400000000000000000000030071046102023000167540ustar 00000000000000// Copyright 2022 Tavian Barnes // // 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. use faccess::PathExt; use walkdir::DirEntry; use super::{Matcher, MatcherIO}; /// Matcher for -{read,writ,execut}able. pub enum AccessMatcher { Readable, Writable, Executable, } impl Matcher for AccessMatcher { fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { let path = file_info.path(); match self { Self::Readable => path.readable(), Self::Writable => path.writable(), Self::Executable => path.executable(), } } } #[cfg(test)] mod tests { use super::*; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::matchers::Matcher; use crate::find::tests::FakeDependencies; #[test] fn access_matcher() { let file_info = get_dir_entry_for("test_data/simple", "abbbc"); let deps = FakeDependencies::new(); assert!( AccessMatcher::Readable.matches(&file_info, &mut deps.new_matcher_io()), "file should be readable" ); assert!( AccessMatcher::Writable.matches(&file_info, &mut deps.new_matcher_io()), "file should be writable" ); #[cfg(unix)] assert!( !AccessMatcher::Executable.matches(&file_info, &mut deps.new_matcher_io()), "file should not be executable" ); } } findutils-0.4.2/src/find/matchers/delete.rs000064400000000000000000000055551046102023000167670ustar 00000000000000/* * This file is part of the uutils findutils package. * * (c) Arcterus * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use std::fs::{self, FileType}; use std::io::{self, stderr, Write}; use std::path::Path; use walkdir::DirEntry; use super::{Matcher, MatcherIO}; pub struct DeleteMatcher; impl DeleteMatcher { pub fn new() -> Self { DeleteMatcher } fn delete(&self, file_path: &Path, file_type: FileType) -> io::Result<()> { if file_type.is_dir() { fs::remove_dir(file_path) } else { fs::remove_file(file_path) } } } impl Matcher for DeleteMatcher { fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { let path = file_info.path(); let path_str = path.to_string_lossy(); // This is a quirk in find's traditional semantics probably due to // POSIX rmdir() not accepting "." (EINVAL). std::fs::remove_dir() // inherits the same behavior, so no reason to buck tradition. if path_str == "." { return true; } match self.delete(path, file_info.file_type()) { Ok(_) => true, Err(e) => { writeln!(&mut stderr(), "Failed to delete {}: {}", path_str, e).unwrap(); false } } } fn has_side_effects(&self) -> bool { true } } #[cfg(test)] mod tests { use std::fs::{create_dir, File}; use tempfile::Builder; use super::*; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::tests::FakeDependencies; #[test] fn delete_matcher() { let matcher = DeleteMatcher::new(); let deps = FakeDependencies::new(); let temp_dir = Builder::new().prefix("test_data").tempdir().unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); File::create(temp_dir.path().join("test")).expect("created test file"); create_dir(temp_dir.path().join("test_dir")).expect("created test directory"); let test_entry = get_dir_entry_for(&temp_dir_path, "test"); assert!( matcher.matches(&test_entry, &mut deps.new_matcher_io()), "DeleteMatcher should match a simple file", ); assert!( !temp_dir.path().join("test").exists(), "DeleteMatcher should actually delete files it matches", ); let temp_dir_entry = get_dir_entry_for(&temp_dir_path, "test_dir"); assert!( matcher.matches(&temp_dir_entry, &mut deps.new_matcher_io()), "DeleteMatcher should match directories", ); assert!( !temp_dir.path().join("test_dir").exists(), "DeleteMatcher should actually delete (empty) directories it matches", ); } } findutils-0.4.2/src/find/matchers/empty.rs000064400000000000000000000057061046102023000166610ustar 00000000000000// Copyright 2021 Collabora, Ltd. // // 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. use std::{ fs::read_dir, io::{stderr, Write}, }; use super::Matcher; pub struct EmptyMatcher; impl EmptyMatcher { pub fn new() -> EmptyMatcher { EmptyMatcher } } impl Matcher for EmptyMatcher { fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool { if file_info.file_type().is_file() { match file_info.metadata() { Ok(meta) => meta.len() == 0, Err(err) => { writeln!( &mut stderr(), "Error getting size for {}: {}", file_info.path().display(), err ) .unwrap(); false } } } else if file_info.file_type().is_dir() { match read_dir(file_info.path()) { Ok(mut it) => it.next().is_none(), Err(err) => { writeln!( &mut stderr(), "Error getting contents of {}: {}", file_info.path().display(), err ) .unwrap(); false } } } else { false } } } #[cfg(test)] mod tests { use tempfile::Builder; use super::*; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::matchers::Matcher; use crate::find::tests::FakeDependencies; #[test] fn empty_files() { let empty_file_info = get_dir_entry_for("test_data/simple", "abbbc"); let nonempty_file_info = get_dir_entry_for("test_data/size", "512bytes"); let matcher = EmptyMatcher::new(); let deps = FakeDependencies::new(); assert!(matcher.matches(&empty_file_info, &mut deps.new_matcher_io())); assert!(!matcher.matches(&nonempty_file_info, &mut deps.new_matcher_io())); } #[test] fn empty_directories() { let temp_dir = Builder::new() .prefix("empty_directories") .tempdir() .unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); let subdir_name = "subdir"; std::fs::create_dir(temp_dir.path().join(subdir_name)).unwrap(); let matcher = EmptyMatcher::new(); let deps = FakeDependencies::new(); let file_info = get_dir_entry_for(&temp_dir_path, subdir_name); assert!(matcher.matches(&file_info, &mut deps.new_matcher_io())); std::fs::File::create(temp_dir.path().join(subdir_name).join("a")).unwrap(); let file_info = get_dir_entry_for(&temp_dir_path, subdir_name); assert!(!matcher.matches(&file_info, &mut deps.new_matcher_io())); } } findutils-0.4.2/src/find/matchers/exec.rs000064400000000000000000000057501046102023000164460ustar 00000000000000// Copyright 2017 Google Inc. // // 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. use std::error::Error; use std::ffi::OsString; use std::io::{stderr, Write}; use std::path::Path; use std::process::Command; use walkdir::DirEntry; use super::{Matcher, MatcherIO}; enum Arg { FileArg(Vec), LiteralArg(OsString), } pub struct SingleExecMatcher { executable: String, args: Vec, exec_in_parent_dir: bool, } impl SingleExecMatcher { pub fn new( executable: &str, args: &[&str], exec_in_parent_dir: bool, ) -> Result> { let transformed_args = args .iter() .map(|&a| { let parts = a.split("{}").collect::>(); if parts.len() == 1 { // No {} present Arg::LiteralArg(OsString::from(a)) } else { Arg::FileArg(parts.iter().map(OsString::from).collect()) } }) .collect(); Ok(Self { executable: executable.to_string(), args: transformed_args, exec_in_parent_dir, }) } } impl Matcher for SingleExecMatcher { fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { let mut command = Command::new(&self.executable); let path_to_file = if self.exec_in_parent_dir { if let Some(f) = file_info.path().file_name() { Path::new(".").join(f) } else { Path::new(".").join(file_info.path()) } } else { file_info.path().to_path_buf() }; for arg in &self.args { match *arg { Arg::LiteralArg(ref a) => command.arg(a.as_os_str()), Arg::FileArg(ref parts) => command.arg(&parts.join(path_to_file.as_os_str())), }; } if self.exec_in_parent_dir { match file_info.path().parent() { None => { // Root paths like "/" have no parent. Run them from the root to match GNU find. command.current_dir(file_info.path()); } Some(parent) if parent == Path::new("") => { // Paths like "foo" have a parent of "". Avoid chdir(""). } Some(parent) => { command.current_dir(parent); } } } match command.status() { Ok(status) => status.success(), Err(e) => { writeln!(&mut stderr(), "Failed to run {}: {}", self.executable, e).unwrap(); false } } } fn has_side_effects(&self) -> bool { true } } #[cfg(test)] /// No tests here, because we need to call out to an external executable. See /// tests/exec_unit_tests.rs instead. mod tests {} findutils-0.4.2/src/find/matchers/glob.rs000064400000000000000000000177411046102023000164500ustar 00000000000000// Copyright 2022 Tavian Barnes // // 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. use onig::{self, Regex, RegexOptions, Syntax}; /// Parse a string as a POSIX Basic Regular Expression. fn parse_bre(expr: &str, options: RegexOptions) -> Result { let bre = Syntax::posix_basic(); Regex::with_options(expr, bre.options() | options, bre) } /// Push a literal character onto a regex, escaping it if necessary. fn regex_push_literal(regex: &mut String, ch: char) { // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_03 if matches!(ch, '.' | '[' | '\\' | '*' | '^' | '$') { regex.push('\\'); } regex.push(ch); } /// Extracts a bracket expression from a glob. fn extract_bracket_expr(pattern: &str) -> Option<(String, &str)> { // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13_01 // // If an open bracket introduces a bracket expression as in XBD RE Bracket Expression, // except that the character ( '!' ) shall replace the // character ( '^' ) in its role in a non-matching list in the regular expression notation, // it shall introduce a pattern bracket expression. A bracket expression starting with an // unquoted character produces unspecified results. Otherwise, '[' shall match // the character itself. // // To check for valid bracket expressions, we scan for the closing bracket and // attempt to parse that segment as a regex. If that fails, we treat the '[' // literally. let mut expr = "[".to_string(); let mut chars = pattern.chars(); let mut next = chars.next(); // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_05 // // 3. A non-matching list expression begins with a ( '^' ) ... // // (but in a glob, '!' is used instead of '^') if next == Some('!') { expr.push('^'); next = chars.next(); } // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_05 // // 1. ... The ( ']' ) shall lose its special meaning and represent // itself in a bracket expression if it occurs first in the list (after an initial // ( '^' ), if any). if next == Some(']') { expr.push(']'); next = chars.next(); } while let Some(ch) = next { expr.push(ch); match ch { '[' => { // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_05 // // 4. A collating symbol is a collating element enclosed within bracket-period // ( "[." and ".]" ) delimiters. ... // // 5. An equivalence class expression shall ... be expressed by enclosing any // one of the collating elements in the equivalence class within bracket- // equal ( "[=" and "=]" ) delimiters. // // 6. ... A character class expression is expressed as a character class name // enclosed within bracket- ( "[:" and ":]" ) delimiters. next = chars.next(); if let Some(delim) = next { expr.push(delim); if matches!(delim, '.' | '=' | ':') { let rest = chars.as_str(); let end = rest.find([delim, ']'])? + 2; expr.push_str(&rest[..end]); chars = rest[end..].chars(); } } } ']' => { // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_05 // // 1. ... The ( ']' ) shall ... terminate the bracket // expression, unless it appears in a collating symbol (such as "[.].]" ) or is // the ending for a collating symbol, equivalence class, // or character class. break; } _ => {} } next = chars.next(); } if parse_bre(&expr, RegexOptions::REGEX_OPTION_NONE).is_ok() { Some((expr, chars.as_str())) } else { None } } /// Converts a POSIX glob into a POSIX Basic Regular Expression fn glob_to_regex(pattern: &str) -> String { let mut regex = String::new(); let mut chars = pattern.chars(); while let Some(ch) = chars.next() { // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13 match ch { '?' => regex.push('.'), '*' => regex.push_str(".*"), '\\' => { if let Some(ch) = chars.next() { regex_push_literal(&mut regex, ch); } else { // https://pubs.opengroup.org/onlinepubs/9699919799/functions/fnmatch.html // // If pattern ends with an unescaped , fnmatch() shall return a // non-zero value (indicating either no match or an error). // // Most implementations return FNM_NOMATCH in this case, so return a regex that // never matches. return "$.".to_string(); } } '[' => { if let Some((expr, rest)) = extract_bracket_expr(chars.as_str()) { regex.push_str(&expr); chars = rest.chars(); } else { regex_push_literal(&mut regex, ch); } } _ => regex_push_literal(&mut regex, ch), } } regex } /// An fnmatch()-style glob matcher. pub struct Pattern { regex: Regex, } impl Pattern { /// Parse an fnmatch()-style glob. pub fn new(pattern: &str, caseless: bool) -> Self { let options = if caseless { RegexOptions::REGEX_OPTION_IGNORECASE } else { RegexOptions::REGEX_OPTION_NONE }; // As long as glob_to_regex() is correct, this should never fail let regex = parse_bre(&glob_to_regex(pattern), options).unwrap(); Self { regex } } /// Test if this pattern matches a string. pub fn matches(&self, string: &str) -> bool { self.regex.is_match(string) } } #[cfg(test)] mod tests { use super::*; #[test] fn literals() { assert_eq!(glob_to_regex(r"foo.bar"), r"foo\.bar"); } #[test] fn regex_special() { assert_eq!(glob_to_regex(r"^foo.bar$"), r"\^foo\.bar\$"); } #[test] fn wildcards() { assert_eq!(glob_to_regex(r"foo?bar*baz"), r"foo.bar.*baz"); } #[test] fn escapes() { assert_eq!(glob_to_regex(r"fo\o\?bar\*baz\\"), r"foo?bar\*baz\\"); } #[test] fn incomplete_escape() { assert_eq!(glob_to_regex(r"foo\"), r"$.") } #[test] fn valid_brackets() { assert_eq!(glob_to_regex(r"foo[bar][!baz]"), r"foo[bar][^baz]"); } #[test] fn complex_brackets() { assert_eq!( glob_to_regex(r"[!]!.*[\[.].][=]=][:space:]-]"), r"[^]!.*[\[.].][=]=][:space:]-]" ); } #[test] fn invalid_brackets() { assert_eq!(glob_to_regex(r"foo[bar[!baz"), r"foo\[bar\[!baz"); } #[test] fn pattern_matches() { assert!(Pattern::new(r"foo*bar", false).matches("foo--bar")); assert!(!Pattern::new(r"foo*bar", false).matches("bar--foo")); } #[test] fn caseless_matches() { assert!(Pattern::new(r"foo*BAR", true).matches("FOO--bar")); assert!(!Pattern::new(r"foo*BAR", true).matches("BAR--foo")); } } findutils-0.4.2/src/find/matchers/lname.rs000064400000000000000000000061421046102023000166120ustar 00000000000000// Copyright 2017 Google Inc. // // 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. use std::io::{stderr, Write}; use std::path::PathBuf; use walkdir::DirEntry; use super::glob::Pattern; use super::{Matcher, MatcherIO}; fn read_link_target(file_info: &DirEntry) -> Option { match file_info.path().read_link() { Ok(target) => Some(target), Err(err) => { // If it's not a symlink, then it's not an error that should be // shown. if err.kind() != std::io::ErrorKind::InvalidInput { writeln!( &mut stderr(), "Error reading target of {}: {}", file_info.path().display(), err ) .unwrap(); } None } } } /// This matcher makes a comparison of the link target against a shell wildcard /// pattern. See `glob::Pattern` for details on the exact syntax. pub struct LinkNameMatcher { pattern: Pattern, } impl LinkNameMatcher { pub fn new(pattern_string: &str, caseless: bool) -> LinkNameMatcher { let pattern = Pattern::new(pattern_string, caseless); Self { pattern } } } impl Matcher for LinkNameMatcher { fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { if let Some(target) = read_link_target(file_info) { self.pattern.matches(&target.to_string_lossy()) } else { false } } } #[cfg(test)] mod tests { use super::*; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::matchers::Matcher; use crate::find::tests::FakeDependencies; use std::io::ErrorKind; #[cfg(unix)] use std::os::unix::fs::symlink; #[cfg(windows)] use std::os::windows::fs::symlink_file; fn create_file_link() { #[cfg(unix)] if let Err(e) = symlink("abbbc", "test_data/links/link-f") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } #[cfg(windows)] if let Err(e) = symlink_file("abbbc", "test_data/links/link-f") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } } #[test] fn matches_against_link_target() { create_file_link(); let link_f = get_dir_entry_for("test_data/links", "link-f"); let matcher = LinkNameMatcher::new("ab?bc", false); let deps = FakeDependencies::new(); assert!(matcher.matches(&link_f, &mut deps.new_matcher_io())); } #[test] fn caseless_matches_against_link_target() { create_file_link(); let link_f = get_dir_entry_for("test_data/links", "link-f"); let matcher = LinkNameMatcher::new("AbB?c", true); let deps = FakeDependencies::new(); assert!(matcher.matches(&link_f, &mut deps.new_matcher_io())); } } findutils-0.4.2/src/find/matchers/logical_matchers.rs000064400000000000000000000444071046102023000210240ustar 00000000000000// Copyright 2017 Google Inc. // // 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. //! This modules contains the matchers used for combining other matchers and //! performing boolean logic on them (and a couple of trivial always-true and //! always-false matchers). The design is strongly tied to the precedence rules //! when parsing command-line options (e.g. "-foo -o -bar -baz" is equivalent //! to "-foo -o ( -bar -baz )", not "( -foo -o -bar ) -baz"). use std::error::Error; use std::iter::Iterator; use std::path::Path; use walkdir::DirEntry; use super::{Matcher, MatcherIO}; /// This matcher contains a collection of other matchers. A file only matches /// if it matches ALL the contained sub-matchers. For sub-matchers that have /// side effects, the side effects occur in the same order as the sub-matchers /// were pushed into the collection. pub struct AndMatcher { submatchers: Vec>, } impl AndMatcher { pub fn new(submatchers: Vec>) -> Self { Self { submatchers } } } impl Matcher for AndMatcher { /// Returns true if all sub-matchers return true. Short-circuiting does take /// place. If the nth sub-matcher returns false, then we immediately return /// and don't make any further calls. fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool { for matcher in &self.submatchers { if !matcher.matches(dir_entry, matcher_io) { return false; } if matcher_io.should_quit() { break; } } true } fn has_side_effects(&self) -> bool { self.submatchers.iter().any(|x| x.has_side_effects()) } fn finished_dir(&self, dir: &Path) { for m in &self.submatchers { m.finished_dir(dir); } } fn finished(&self) { for m in &self.submatchers { m.finished(); } } } pub struct AndMatcherBuilder { submatchers: Vec>, } impl AndMatcherBuilder { pub fn new() -> Self { Self { submatchers: Vec::new(), } } pub fn new_and_condition(&mut self, matcher: impl Matcher) { self.submatchers.push(matcher.into_box()); } /// Builds a Matcher: consuming the builder in the process. pub fn build(mut self) -> Box { // special case. If there's only one submatcher, just return that directly if self.submatchers.len() == 1 { // safe to unwrap: we've just checked the size return self.submatchers.pop().unwrap(); } AndMatcher::new(self.submatchers).into_box() } } /// This matcher contains a collection of other matchers. A file matches /// if it matches any of the contained sub-matchers. For sub-matchers that have /// side effects, the side effects occur in the same order as the sub-matchers /// were pushed into the collection. pub struct OrMatcher { submatchers: Vec>, } impl OrMatcher { pub fn new(submatchers: Vec>) -> Self { Self { submatchers } } } impl Matcher for OrMatcher { /// Returns true if any sub-matcher returns true. Short-circuiting does take /// place. If the nth sub-matcher returns true, then we immediately return /// and don't make any further calls. fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool { for matcher in &self.submatchers { if matcher.matches(dir_entry, matcher_io) { return true; } if matcher_io.should_quit() { break; } } false } fn has_side_effects(&self) -> bool { self.submatchers.iter().any(|x| x.has_side_effects()) } fn finished_dir(&self, dir: &Path) { for m in &self.submatchers { m.finished_dir(dir); } } fn finished(&self) { for m in &self.submatchers { m.finished(); } } } pub struct OrMatcherBuilder { submatchers: Vec, } impl OrMatcherBuilder { pub fn new_and_condition(&mut self, matcher: impl Matcher) { // safe to unwrap. submatchers always has at least one member self.submatchers .last_mut() .unwrap() .new_and_condition(matcher); } pub fn new_or_condition(&mut self, arg: &str) -> Result<(), Box> { if self.submatchers.last().unwrap().submatchers.is_empty() { return Err(From::from(format!( "invalid expression; you have used a binary operator \ '{}' with nothing before it.", arg ))); } self.submatchers.push(AndMatcherBuilder::new()); Ok(()) } pub fn new() -> Self { let mut o = Self { submatchers: Vec::new(), }; o.submatchers.push(AndMatcherBuilder::new()); o } /// Builds a Matcher: consuming the builder in the process. pub fn build(mut self) -> Box { // Special case: if there's only one submatcher, just return that directly if self.submatchers.len() == 1 { // safe to unwrap: we've just checked the size return self.submatchers.pop().unwrap().build(); } let mut submatchers = vec![]; for x in self.submatchers { submatchers.push(x.build()); } OrMatcher::new(submatchers).into_box() } } /// This matcher contains a collection of other matchers. In contrast to /// `OrMatcher` and `AndMatcher`, all the submatcher objects are called /// regardless of the results of previous submatchers. This is primarily used /// for submatchers with side-effects. For such sub-matchers the side effects /// occur in the same order as the sub-matchers were pushed into the collection. pub struct ListMatcher { submatchers: Vec>, } impl ListMatcher { pub fn new(submatchers: Vec>) -> Self { Self { submatchers } } } impl Matcher for ListMatcher { /// Calls matches on all submatcher objects, with no short-circuiting. /// Returns the result of the call to the final submatcher fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool { let mut rc = false; for matcher in &self.submatchers { rc = matcher.matches(dir_entry, matcher_io); if matcher_io.should_quit() { break; } } rc } fn has_side_effects(&self) -> bool { self.submatchers.iter().any(|x| x.has_side_effects()) } fn finished_dir(&self, dir: &Path) { for m in &self.submatchers { m.finished_dir(dir); } } fn finished(&self) { for m in &self.submatchers { m.finished(); } } } pub struct ListMatcherBuilder { submatchers: Vec, } impl ListMatcherBuilder { pub fn new_and_condition(&mut self, matcher: impl Matcher) { // safe to unwrap. submatchers always has at least one member self.submatchers .last_mut() .unwrap() .new_and_condition(matcher); } pub fn new_or_condition(&mut self, arg: &str) -> Result<(), Box> { self.submatchers.last_mut().unwrap().new_or_condition(arg) } pub fn check_new_and_condition(&mut self) -> Result<(), Box> { { let child_or_matcher = &self.submatchers.last().unwrap(); let grandchild_and_matcher = &child_or_matcher.submatchers.last().unwrap(); if grandchild_and_matcher.submatchers.is_empty() { return Err(From::from( "invalid expression; you have used a binary operator '-a' \ with nothing before it.", )); } } Ok(()) } pub fn new_list_condition(&mut self) -> Result<(), Box> { { let child_or_matcher = &self.submatchers.last().unwrap(); let grandchild_and_matcher = &child_or_matcher.submatchers.last().unwrap(); if grandchild_and_matcher.submatchers.is_empty() { return Err(From::from( "invalid expression; you have used a binary operator ',' \ with nothing before it.", )); } } self.submatchers.push(OrMatcherBuilder::new()); Ok(()) } pub fn new() -> Self { let mut o = Self { submatchers: Vec::new(), }; o.submatchers.push(OrMatcherBuilder::new()); o } /// Builds a Matcher: consuming the builder in the process. pub fn build(mut self) -> Box { // Special case: if there's only one submatcher, just return that directly if self.submatchers.len() == 1 { // safe to unwrap: we've just checked the size return self.submatchers.pop().unwrap().build(); } let mut submatchers = vec![]; for x in self.submatchers { submatchers.push(x.build()); } Box::new(ListMatcher::new(submatchers)) } } /// A simple matcher that always matches. pub struct TrueMatcher; impl Matcher for TrueMatcher { fn matches(&self, _dir_entry: &DirEntry, _: &mut MatcherIO) -> bool { true } } /// A simple matcher that never matches. pub struct FalseMatcher; impl Matcher for FalseMatcher { fn matches(&self, _dir_entry: &DirEntry, _: &mut MatcherIO) -> bool { false } } /// Matcher that wraps another matcher and inverts matching criteria. pub struct NotMatcher { submatcher: Box, } impl NotMatcher { pub fn new(submatcher: impl Matcher) -> Self { Self { submatcher: submatcher.into_box(), } } } impl Matcher for NotMatcher { fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool { !self.submatcher.matches(dir_entry, matcher_io) } fn has_side_effects(&self) -> bool { self.submatcher.has_side_effects() } fn finished_dir(&self, dir: &Path) { self.submatcher.finished_dir(dir); } fn finished(&self) { self.submatcher.finished(); } } #[cfg(test)] mod tests { use super::*; use crate::find::matchers::quit::QuitMatcher; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::matchers::{Matcher, MatcherIO}; use crate::find::tests::FakeDependencies; use std::cell::RefCell; use std::rc::Rc; use walkdir::DirEntry; /// Simple Matcher impl that has side effects pub struct HasSideEffects; impl Matcher for HasSideEffects { fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool { false } fn has_side_effects(&self) -> bool { true } } /// Matcher that counts its invocations struct Counter(Rc>); impl Matcher for Counter { fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool { *self.0.borrow_mut() += 1; true } } #[test] fn and_matches_works() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let mut builder = AndMatcherBuilder::new(); let deps = FakeDependencies::new(); // start with one matcher returning true builder.new_and_condition(TrueMatcher); assert!(builder.build().matches(&abbbc, &mut deps.new_matcher_io())); builder = AndMatcherBuilder::new(); builder.new_and_condition(TrueMatcher); builder.new_and_condition(FalseMatcher); assert!(!builder.build().matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn or_matches_works() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let mut builder = OrMatcherBuilder::new(); let deps = FakeDependencies::new(); // start with one matcher returning false builder.new_and_condition(FalseMatcher); assert!(!builder.build().matches(&abbbc, &mut deps.new_matcher_io())); let mut builder = OrMatcherBuilder::new(); builder.new_and_condition(FalseMatcher); builder.new_or_condition("-o").unwrap(); builder.new_and_condition(TrueMatcher); assert!(builder.build().matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn list_matches_works() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let mut builder = ListMatcherBuilder::new(); let deps = FakeDependencies::new(); // result should always match that of the last pushed submatcher builder.new_and_condition(FalseMatcher); assert!(!builder.build().matches(&abbbc, &mut deps.new_matcher_io())); builder = ListMatcherBuilder::new(); builder.new_and_condition(FalseMatcher); builder.new_list_condition().unwrap(); builder.new_and_condition(TrueMatcher); assert!(builder.build().matches(&abbbc, &mut deps.new_matcher_io())); builder = ListMatcherBuilder::new(); builder.new_and_condition(FalseMatcher); builder.new_list_condition().unwrap(); builder.new_and_condition(TrueMatcher); builder.new_list_condition().unwrap(); builder.new_and_condition(FalseMatcher); assert!(!builder.build().matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn true_matches_works() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = TrueMatcher {}; let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn false_matches_works() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = FalseMatcher {}; let deps = FakeDependencies::new(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn and_has_side_effects_works() { let mut builder = AndMatcherBuilder::new(); // start with one matcher with no side effects false builder.new_and_condition(TrueMatcher); assert!(!builder.build().has_side_effects()); builder = AndMatcherBuilder::new(); builder.new_and_condition(TrueMatcher); builder.new_and_condition(HasSideEffects); assert!(builder.build().has_side_effects()); } #[test] fn or_has_side_effects_works() { let mut builder = OrMatcherBuilder::new(); // start with one matcher with no side effects false builder.new_and_condition(TrueMatcher); assert!(!builder.build().has_side_effects()); builder = OrMatcherBuilder::new(); builder.new_and_condition(TrueMatcher); builder.new_and_condition(HasSideEffects); assert!(builder.build().has_side_effects()); } #[test] fn list_has_side_effects_works() { let mut builder = ListMatcherBuilder::new(); // start with one matcher with no side effects false builder.new_and_condition(TrueMatcher); assert!(!builder.build().has_side_effects()); builder = ListMatcherBuilder::new(); builder.new_and_condition(TrueMatcher); builder.new_and_condition(HasSideEffects); assert!(builder.build().has_side_effects()); } #[test] fn true_has_side_effects_works() { let matcher = TrueMatcher {}; assert!(!matcher.has_side_effects()); } #[test] fn false_has_side_effects_works() { let matcher = FalseMatcher {}; assert!(!matcher.has_side_effects()); } #[test] fn not_matches_works() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let not_true = NotMatcher::new(TrueMatcher); let not_false = NotMatcher::new(FalseMatcher); let deps = FakeDependencies::new(); assert!(!not_true.matches(&abbbc, &mut deps.new_matcher_io())); assert!(not_false.matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn not_has_side_effects_works() { let has_fx = NotMatcher::new(HasSideEffects); let has_no_fx = NotMatcher::new(FalseMatcher); assert!(has_fx.has_side_effects()); assert!(!has_no_fx.has_side_effects()); } #[test] fn and_quit_works() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let mut builder = AndMatcherBuilder::new(); let deps = FakeDependencies::new(); let before = Rc::new(RefCell::new(0)); let after = Rc::new(RefCell::new(0)); builder.new_and_condition(Counter(before.clone())); builder.new_and_condition(QuitMatcher); builder.new_and_condition(Counter(after.clone())); builder.build().matches(&abbbc, &mut deps.new_matcher_io()); assert_eq!(*before.borrow(), 1); assert_eq!(*after.borrow(), 0); } #[test] fn or_quit_works() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let mut builder = OrMatcherBuilder::new(); let deps = FakeDependencies::new(); let before = Rc::new(RefCell::new(0)); let after = Rc::new(RefCell::new(0)); builder.new_and_condition(Counter(before.clone())); builder.new_or_condition("-o").unwrap(); builder.new_and_condition(QuitMatcher); builder.new_or_condition("-o").unwrap(); builder.new_and_condition(Counter(after.clone())); builder.build().matches(&abbbc, &mut deps.new_matcher_io()); assert_eq!(*before.borrow(), 1); assert_eq!(*after.borrow(), 0); } #[test] fn list_quit_works() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let mut builder = ListMatcherBuilder::new(); let deps = FakeDependencies::new(); let before = Rc::new(RefCell::new(0)); let after = Rc::new(RefCell::new(0)); builder.new_and_condition(Counter(before.clone())); builder.new_list_condition().unwrap(); builder.new_and_condition(QuitMatcher); builder.new_list_condition().unwrap(); builder.new_and_condition(Counter(after.clone())); builder.build().matches(&abbbc, &mut deps.new_matcher_io()); assert_eq!(*before.borrow(), 1); assert_eq!(*after.borrow(), 0); } } findutils-0.4.2/src/find/matchers/mod.rs000064400000000000000000001217101046102023000162740ustar 00000000000000// Copyright 2017 Google Inc. // // 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. mod access; mod delete; mod empty; pub mod exec; mod glob; mod lname; mod logical_matchers; mod name; mod path; mod perm; mod printer; mod printf; mod prune; mod quit; mod regex; mod size; mod stat; mod time; mod type_matcher; use ::regex::Regex; use std::path::Path; use std::time::SystemTime; use std::{error::Error, str::FromStr}; use walkdir::DirEntry; use self::access::AccessMatcher; use self::delete::DeleteMatcher; use self::empty::EmptyMatcher; use self::exec::SingleExecMatcher; use self::lname::LinkNameMatcher; use self::logical_matchers::{ AndMatcherBuilder, FalseMatcher, ListMatcherBuilder, NotMatcher, TrueMatcher, }; use self::name::NameMatcher; use self::path::PathMatcher; use self::perm::PermMatcher; use self::printer::{PrintDelimiter, Printer}; use self::printf::Printf; use self::prune::PruneMatcher; use self::quit::QuitMatcher; use self::regex::RegexMatcher; use self::size::SizeMatcher; use self::stat::{InodeMatcher, LinksMatcher}; use self::time::{FileTimeMatcher, FileTimeType, NewerMatcher}; use self::type_matcher::TypeMatcher; use super::{Config, Dependencies}; /// Struct holding references to outputs and any inputs that can't be derived /// from the file/directory info. pub struct MatcherIO<'a> { should_skip_dir: bool, quit: bool, deps: &'a dyn Dependencies<'a>, } impl<'a> MatcherIO<'a> { pub fn new(deps: &'a dyn Dependencies<'a>) -> MatcherIO<'a> { MatcherIO { deps, should_skip_dir: false, quit: false, } } pub fn mark_current_dir_to_be_skipped(&mut self) { self.should_skip_dir = true; } pub fn should_skip_current_dir(&self) -> bool { self.should_skip_dir } pub fn quit(&mut self) { self.quit = true; } pub fn should_quit(&self) -> bool { self.quit } pub fn now(&self) -> SystemTime { self.deps.now() } } /// A basic interface that can be used to determine whether a directory entry /// is what's being searched for. To a first order approximation, find consists /// of building a chain of Matcher objects, and then walking a directory tree, /// passing each entry to the chain of Matchers. pub trait Matcher: 'static { /// Boxes this matcher as a trait object. fn into_box(self) -> Box where Self: Sized, { Box::new(self) } /// Returns whether the given file matches the object's predicate. fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool; /// Returns whether the matcher has any side-effects (e.g. executing a /// command, deleting a file). Iff no such matcher exists in the chain, then /// the filename will be printed to stdout. While this is a compile-time /// fact for most matchers, it's run-time for matchers that contain a /// collection of sub-Matchers. fn has_side_effects(&self) -> bool { // most matchers don't have side-effects, so supply a default implementation. false } /// Notification that find has finished processing a given directory. fn finished_dir(&self, _finished_directory: &Path) {} /// Notification that find has finished processing all directories - /// allowing for any cleanup that isn't suitable for destructors (e.g. /// blocking calls, I/O etc.) fn finished(&self) {} } impl Matcher for Box { fn into_box(self) -> Box { self } fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool { (**self).matches(file_info, matcher_io) } fn has_side_effects(&self) -> bool { (**self).has_side_effects() } fn finished_dir(&self, finished_directory: &Path) { (**self).finished_dir(finished_directory) } fn finished(&self) { (**self).finished() } } pub enum ComparableValue { MoreThan(u64), EqualTo(u64), LessThan(u64), } impl ComparableValue { fn matches(&self, value: u64) -> bool { match *self { ComparableValue::MoreThan(limit) => value > limit, ComparableValue::EqualTo(limit) => value == limit, ComparableValue::LessThan(limit) => value < limit, } } /// same as matches, but takes a signed value fn imatches(&self, value: i64) -> bool { match *self { ComparableValue::MoreThan(limit) => value >= 0 && (value as u64) > limit, ComparableValue::EqualTo(limit) => value >= 0 && (value as u64) == limit, ComparableValue::LessThan(limit) => value < 0 || (value as u64) < limit, } } } /// Builds a single `AndMatcher` containing the Matcher objects corresponding /// to the passed in predicate arguments. pub fn build_top_level_matcher( args: &[&str], config: &mut Config, ) -> Result, Box> { let (_, top_level_matcher) = (build_matcher_tree(args, config, 0, false))?; // if the matcher doesn't have any side-effects, then we default to printing if !top_level_matcher.has_side_effects() { let mut new_and_matcher = AndMatcherBuilder::new(); new_and_matcher.new_and_condition(top_level_matcher); new_and_matcher.new_and_condition(Printer::new(PrintDelimiter::Newline)); return Ok(new_and_matcher.build()); } Ok(top_level_matcher) } /// Helper function for `build_matcher_tree`. fn are_more_expressions(args: &[&str], index: usize) -> bool { (index < args.len() - 1) && args[index + 1] != ")" } fn convert_arg_to_number( option_name: &str, value_as_string: &str, ) -> Result> { match value_as_string.parse::() { Ok(val) => Ok(val), _ => Err(From::from(format!( "Expected a positive decimal integer argument to {}, but got \ `{}'", option_name, value_as_string ))), } } fn convert_arg_to_comparable_value( option_name: &str, value_as_string: &str, ) -> Result> { let re = Regex::new(r"([+-]?)(\d+)$")?; if let Some(groups) = re.captures(value_as_string) { if let Ok(val) = groups[2].parse::() { return Ok(match &groups[1] { "+" => ComparableValue::MoreThan(val), "-" => ComparableValue::LessThan(val), _ => ComparableValue::EqualTo(val), }); } } Err(From::from(format!( "Expected a decimal integer (with optional + or - prefix) argument \ to {}, but got `{}'", option_name, value_as_string ))) } fn convert_arg_to_comparable_value_and_suffix( option_name: &str, value_as_string: &str, ) -> Result<(ComparableValue, String), Box> { let re = Regex::new(r"([+-]?)(\d+)(.*)$")?; if let Some(groups) = re.captures(value_as_string) { if let Ok(val) = groups[2].parse::() { return Ok(( match &groups[1] { "+" => ComparableValue::MoreThan(val), "-" => ComparableValue::LessThan(val), _ => ComparableValue::EqualTo(val), }, groups[3].to_string(), )); } } Err(From::from(format!( "Expected a decimal integer (with optional + or - prefix) and \ (optional suffix) argument to {}, but got `{}'", option_name, value_as_string ))) } /// The main "translate command-line args into a matcher" function. Will call /// itself recursively if it encounters an opening bracket. A successful return /// consists of a tuple containing the new index into the args array to use (if /// called recursively) and the resulting matcher. fn build_matcher_tree( args: &[&str], config: &mut Config, arg_index: usize, expecting_bracket: bool, ) -> Result<(usize, Box), Box> { let mut top_level_matcher = ListMatcherBuilder::new(); let mut regex_type = regex::RegexType::default(); // can't use getopts for a variety or reasons: // order of arguments is important // arguments can start with + as well as - // multiple-character flags don't start with a double dash let mut i = arg_index; let mut invert_next_matcher = false; while i < args.len() { let possible_submatcher = match args[i] { "-print" => Some(Printer::new(PrintDelimiter::Newline).into_box()), "-print0" => Some(Printer::new(PrintDelimiter::Null).into_box()), "-printf" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } i += 1; Some(Printf::new(args[i])?.into_box()) } "-true" => Some(TrueMatcher.into_box()), "-false" => Some(FalseMatcher.into_box()), "-lname" | "-ilname" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } i += 1; Some(LinkNameMatcher::new(args[i], args[i - 1].starts_with("-i")).into_box()) } "-name" | "-iname" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } i += 1; Some(NameMatcher::new(args[i], args[i - 1].starts_with("-i")).into_box()) } "-path" | "-ipath" | "-wholename" | "-iwholename" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } i += 1; Some(PathMatcher::new(args[i], args[i - 1].starts_with("-i")).into_box()) } "-readable" => Some(AccessMatcher::Readable.into_box()), "-regextype" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } i += 1; regex_type = regex::RegexType::from_str(args[i])?; None } "-regex" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } i += 1; Some(RegexMatcher::new(regex_type, args[i], false)?.into_box()) } "-iregex" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } i += 1; Some(RegexMatcher::new(regex_type, args[i], true)?.into_box()) } "-type" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } i += 1; Some(TypeMatcher::new(args[i])?.into_box()) } "-delete" => { // -delete implicitly requires -depth config.depth_first = true; Some(DeleteMatcher::new().into_box()) } "-newer" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } i += 1; Some(NewerMatcher::new(args[i])?.into_box()) } "-mtime" | "-atime" | "-ctime" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } let file_time_type = match args[i] { "-atime" => FileTimeType::Accessed, "-ctime" => FileTimeType::Created, "-mtime" => FileTimeType::Modified, // This shouldn't be possible. We've already checked the value // is one of those three values. _ => unreachable!("Encountered unexpected value {}", args[i]), }; let days = convert_arg_to_comparable_value(args[i], args[i + 1])?; i += 1; Some(FileTimeMatcher::new(file_time_type, days).into_box()) } "-size" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } let (size, unit) = convert_arg_to_comparable_value_and_suffix(args[i], args[i + 1])?; i += 1; Some(SizeMatcher::new(size, &unit)?.into_box()) } "-empty" => Some(EmptyMatcher::new().into_box()), "-exec" | "-execdir" => { let mut arg_index = i + 1; while arg_index < args.len() && args[arg_index] != ";" { if args[arg_index - 1] == "{}" && args[arg_index] == "+" { // MultiExecMatcher isn't written yet return Err(From::from(format!( "{} [args...] + isn't supported yet. \ Only {} [args...] ;", args[i], args[i] ))); } arg_index += 1; } if arg_index < i + 2 || arg_index == args.len() { // at the minimum we need the executable and the ';' return Err(From::from(format!("missing argument to {}", args[i]))); } let expression = args[i]; let executable = args[i + 1]; let exec_args = &args[i + 2..arg_index]; i = arg_index; Some( SingleExecMatcher::new(executable, exec_args, expression == "-execdir")? .into_box(), ) } "-inum" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } let inum = convert_arg_to_comparable_value(args[i], args[i + 1])?; i += 1; Some(InodeMatcher::new(inum)?.into_box()) } "-links" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } let inum = convert_arg_to_comparable_value(args[i], args[i + 1])?; i += 1; Some(LinksMatcher::new(inum)?.into_box()) } "-executable" => Some(AccessMatcher::Executable.into_box()), "-perm" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } i += 1; Some(PermMatcher::new(args[i])?.into_box()) } "-prune" => Some(PruneMatcher::new().into_box()), "-quit" => Some(QuitMatcher.into_box()), "-writable" => Some(AccessMatcher::Writable.into_box()), "-not" | "!" => { if !are_more_expressions(args, i) { return Err(From::from(format!( "expected an expression after {}", args[i] ))); } invert_next_matcher = !invert_next_matcher; None } "-and" | "-a" => { if !are_more_expressions(args, i) { return Err(From::from(format!( "expected an expression after {}", args[i] ))); } top_level_matcher.check_new_and_condition()?; None } "-or" | "-o" => { if !are_more_expressions(args, i) { return Err(From::from(format!( "expected an expression after {}", args[i] ))); } top_level_matcher.new_or_condition(args[i])?; None } "," => { if !are_more_expressions(args, i) { return Err(From::from(format!( "expected an expression after {}", args[i] ))); } top_level_matcher.new_list_condition()?; None } "(" => { let (new_arg_index, sub_matcher) = build_matcher_tree(args, config, i + 1, true)?; i = new_arg_index; Some(sub_matcher) } ")" => { if !expecting_bracket { return Err(From::from("you have too many ')'")); } return Ok((i, top_level_matcher.build())); } "-d" | "-depth" => { // TODO add warning if it appears after actual testing criterion config.depth_first = true; None } "-mount" | "-xdev" => { // TODO add warning if it appears after actual testing criterion config.same_file_system = true; None } "-sorted" => { // TODO add warning if it appears after actual testing criterion config.sorted_output = true; None } "-maxdepth" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } config.max_depth = convert_arg_to_number(args[i], args[i + 1])?; i += 1; None } "-mindepth" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } config.min_depth = convert_arg_to_number(args[i], args[i + 1])?; i += 1; None } "-help" | "--help" => { config.help_requested = true; None } "-version" | "--version" => { config.version_requested = true; None } _ => return Err(From::from(format!("Unrecognized flag: '{}'", args[i]))), }; if let Some(submatcher) = possible_submatcher { if invert_next_matcher { top_level_matcher.new_and_condition(NotMatcher::new(submatcher)); invert_next_matcher = false; } else { top_level_matcher.new_and_condition(submatcher); } } i += 1; } if expecting_bracket { return Err(From::from( "invalid expression; I was expecting to find a ')' somewhere but \ did not see one.", )); } Ok((i, top_level_matcher.build())) } #[cfg(test)] mod tests { use super::*; use crate::find::tests::fix_up_slashes; use crate::find::tests::FakeDependencies; use crate::find::Config; use walkdir::{DirEntry, WalkDir}; /// Helper function for tests to get a DirEntry object. directory should /// probably be a string starting with "test_data/" (cargo's tests run with /// a working directory set to the root findutils folder). pub fn get_dir_entry_for(directory: &str, filename: &str) -> DirEntry { for wrapped_dir_entry in WalkDir::new(fix_up_slashes(directory)) { let dir_entry = wrapped_dir_entry.unwrap(); if dir_entry .path() .strip_prefix(directory) .unwrap() .to_string_lossy() == fix_up_slashes(filename) { return dir_entry; } } panic!("Couldn't find {} in {}", filename, directory); } #[test] fn build_top_level_matcher_name() { let abbbc_lower = get_dir_entry_for("./test_data/simple", "abbbc"); let abbbc_upper = get_dir_entry_for("./test_data/simple/subdir", "ABBBC"); let mut config = Config::default(); let deps = FakeDependencies::new(); let matcher = build_top_level_matcher(&["-name", "a*c"], &mut config).unwrap(); assert!(matcher.matches(&abbbc_lower, &mut deps.new_matcher_io())); assert!(!matcher.matches(&abbbc_upper, &mut deps.new_matcher_io())); assert_eq!( deps.get_output_as_string(), fix_up_slashes("./test_data/simple/abbbc\n") ); } #[test] fn build_top_level_matcher_iname() { let abbbc_lower = get_dir_entry_for("./test_data/simple", "abbbc"); let abbbc_upper = get_dir_entry_for("./test_data/simple/subdir", "ABBBC"); let mut config = Config::default(); let deps = FakeDependencies::new(); let matcher = build_top_level_matcher(&["-iname", "a*c"], &mut config).unwrap(); assert!(matcher.matches(&abbbc_lower, &mut deps.new_matcher_io())); assert!(matcher.matches(&abbbc_upper, &mut deps.new_matcher_io())); assert_eq!( deps.get_output_as_string(), fix_up_slashes("./test_data/simple/abbbc\n./test_data/simple/subdir/ABBBC\n") ); } #[test] fn build_top_level_matcher_not() { for arg in &["-not", "!"] { let abbbc_lower = get_dir_entry_for("./test_data/simple", "abbbc"); let mut config = Config::default(); let deps = FakeDependencies::new(); let matcher = build_top_level_matcher(&[arg, "-name", "does_not_exist"], &mut config).unwrap(); assert!(matcher.matches(&abbbc_lower, &mut deps.new_matcher_io())); assert_eq!( deps.get_output_as_string(), fix_up_slashes("./test_data/simple/abbbc\n") ); } } #[test] fn build_top_level_matcher_not_needs_expression() { for arg in &["-not", "!"] { let mut config = Config::default(); if let Err(e) = build_top_level_matcher(&[arg], &mut config) { assert!(e.to_string().contains("expected an expression")); } else { panic!("parsing argument lists that end in -not should fail"); } } } #[test] fn build_top_level_matcher_not_double_negation() { for arg in &["-not", "!"] { let abbbc_lower = get_dir_entry_for("./test_data/simple", "abbbc"); let mut config = Config::default(); let deps = FakeDependencies::new(); let matcher = build_top_level_matcher(&[arg, arg, "-name", "abbbc"], &mut config).unwrap(); assert!(matcher.matches(&abbbc_lower, &mut deps.new_matcher_io())); assert_eq!( deps.get_output_as_string(), fix_up_slashes("./test_data/simple/abbbc\n") ); config = Config::default(); let matcher = build_top_level_matcher(&[arg, arg, "-name", "does_not_exist"], &mut config) .unwrap(); assert!(!matcher.matches(&abbbc_lower, &mut deps.new_matcher_io())); } } #[test] fn build_top_level_matcher_missing_args() { for arg in &["-iname", "-name", "-type"] { let mut config = Config::default(); if let Err(e) = build_top_level_matcher(&[arg], &mut config) { assert!(e.to_string().contains("missing argument to")); assert!(e.to_string().contains(arg)); } else { panic!("parsing argument lists that end in -not should fail"); } } } #[test] fn build_top_level_matcher_or_without_expr1() { for arg in &["-or", "-o"] { let mut config = Config::default(); if let Err(e) = build_top_level_matcher(&[arg, "-true"], &mut config) { assert!(e.to_string().contains("you have used a binary operator")); } else { panic!("parsing argument list that begins with -or should fail"); } } } #[test] fn build_top_level_matcher_or_without_expr2() { for arg in &["-or", "-o"] { let mut config = Config::default(); if let Err(e) = build_top_level_matcher(&["-true", arg], &mut config) { assert!(e.to_string().contains("expected an expression")); } else { panic!("parsing argument list that ends with -or should fail"); } } } #[test] fn build_top_level_matcher_and_without_expr1() { let mut config = Config::default(); if let Err(e) = build_top_level_matcher(&["-a", "-true"], &mut config) { assert!(e.to_string().contains("you have used a binary operator")); } else { panic!("parsing argument list that begins with -a should fail"); } } #[test] fn build_top_level_matcher_and_without_expr2() { let mut config = Config::default(); if let Err(e) = build_top_level_matcher(&["-true", "-a"], &mut config) { assert!(e.to_string().contains("expected an expression")); } else { panic!("parsing argument list that ends with -or should fail"); } } #[test] fn build_top_level_matcher_dash_a_works() { for arg in &["-a", "-and"] { let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); let mut config = Config::default(); let deps = FakeDependencies::new(); // build a matcher using an explicit -a argument let matcher = build_top_level_matcher(&["-true", arg, "-true"], &mut config).unwrap(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); assert_eq!( deps.get_output_as_string(), fix_up_slashes("./test_data/simple/abbbc\n") ); } } #[test] fn build_top_level_matcher_or_works() { let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); for args in &[ ["-true", "-o", "-false"], ["-false", "-o", "-true"], ["-true", "-o", "-true"], ] { let mut config = Config::default(); let deps = FakeDependencies::new(); let matcher = build_top_level_matcher(args, &mut config).unwrap(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); assert_eq!( deps.get_output_as_string(), fix_up_slashes("./test_data/simple/abbbc\n") ); } let mut config = Config::default(); let deps = FakeDependencies::new(); let matcher = build_top_level_matcher(&["-false", "-o", "-false"], &mut config).unwrap(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); assert_eq!(deps.get_output_as_string(), ""); } #[test] fn build_top_level_matcher_and_works() { let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); for args in &[ ["-true", "-false"], ["-false", "-true"], ["-false", "-false"], ] { let mut config = Config::default(); let deps = FakeDependencies::new(); let matcher = build_top_level_matcher(args, &mut config).unwrap(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); assert_eq!(deps.get_output_as_string(), ""); } let mut config = Config::default(); let deps = FakeDependencies::new(); let matcher = build_top_level_matcher(&["-true", "-true"], &mut config).unwrap(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); assert_eq!( deps.get_output_as_string(), fix_up_slashes("./test_data/simple/abbbc\n") ); } #[test] fn build_top_level_matcher_list_works() { let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); let args = ["-true", "-print", "-false", ",", "-print", "-false"]; let mut config = Config::default(); let deps = FakeDependencies::new(); let matcher = build_top_level_matcher(&args, &mut config).unwrap(); // final matcher returns false, so list matcher should too assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); // two print matchers means doubled output assert_eq!( deps.get_output_as_string(), fix_up_slashes("./test_data/simple/abbbc\n./test_data/simple/abbbc\n") ); } #[test] fn build_top_level_matcher_list_without_expr1() { let mut config = Config::default(); if let Err(e) = build_top_level_matcher(&[",", "-true"], &mut config) { assert!(e.to_string().contains("you have used a binary operator")); } else { panic!("parsing argument list that begins with , should fail"); } if let Err(e) = build_top_level_matcher(&["-true", "-o", ",", "-true"], &mut config) { assert!(e.to_string().contains("you have used a binary operator")); } else { panic!("parsing argument list that contains '-o ,' should fail"); } } #[test] fn build_top_level_matcher_list_without_expr2() { let mut config = Config::default(); if let Err(e) = build_top_level_matcher(&["-true", ","], &mut config) { assert!(e.to_string().contains("expected an expression")); } else { panic!("parsing argument list that ends with , should fail"); } } #[test] fn build_top_level_matcher_not_enough_brackets() { let mut config = Config::default(); if let Err(e) = build_top_level_matcher(&["-true", "("], &mut config) { assert!(e.to_string().contains("I was expecting to find a ')'")); } else { panic!("parsing argument list with not enough closing brackets should fail"); } } #[test] fn build_top_level_matcher_too_many_brackets() { let mut config = Config::default(); if let Err(e) = build_top_level_matcher(&["-true", "(", ")", ")"], &mut config) { assert!(e.to_string().contains("too many ')'")); } else { panic!("parsing argument list with too many closing brackets should fail"); } } #[test] fn build_top_level_matcher_can_use_bracket_as_arg() { let mut config = Config::default(); // make sure that if we use a bracket as an argument (e.g. to -name) // then it isn't viewed as a bracket build_top_level_matcher(&["-name", "("], &mut config).unwrap(); build_top_level_matcher(&["-name", ")"], &mut config).unwrap(); } #[test] fn build_top_level_matcher_brackets_work() { let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); // same as true | ( false & false) = true let args_without = ["-true", "-o", "-false", "-false"]; // same as (true | false) & false = false let args_with = ["(", "-true", "-o", "-false", ")", "-false"]; let mut config = Config::default(); let deps = FakeDependencies::new(); { let matcher = build_top_level_matcher(&args_without, &mut config).unwrap(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); } { let matcher = build_top_level_matcher(&args_with, &mut config).unwrap(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); } } #[test] fn build_top_level_matcher_not_and_brackets_work() { let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); // same as (true & !(false)) | true = true let args_without = ["-true", "-not", "-false", "-o", "-true"]; // same as true & !(false | true) = false let args_with = ["-true", "-not", "(", "-false", "-o", "-true", ")"]; let mut config = Config::default(); let deps = FakeDependencies::new(); { let matcher = build_top_level_matcher(&args_without, &mut config).unwrap(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); } { let matcher = build_top_level_matcher(&args_with, &mut config).unwrap(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); } } #[test] fn comparable_value_matches() { assert!( !ComparableValue::LessThan(0).matches(0), "0 should not be less than 0" ); assert!( ComparableValue::LessThan(u64::max_value()).matches(0), "0 should be less than max_value" ); assert!( !ComparableValue::LessThan(0).matches(u64::max_value()), "max_value should not be less than 0" ); assert!( !ComparableValue::LessThan(u64::max_value()).matches(u64::max_value()), "max_value should not be less than max_value" ); assert!( ComparableValue::EqualTo(0).matches(0), "0 should be equal to 0" ); assert!( !ComparableValue::EqualTo(u64::max_value()).matches(0), "0 should not be equal to max_value" ); assert!( !ComparableValue::EqualTo(0).matches(u64::max_value()), "max_value should not be equal to 0" ); assert!( ComparableValue::EqualTo(u64::max_value()).matches(u64::max_value()), "max_value should be equal to max_value" ); assert!( !ComparableValue::MoreThan(0).matches(0), "0 should not be more than 0" ); assert!( !ComparableValue::MoreThan(u64::max_value()).matches(0), "0 should not be more than max_value" ); assert!( ComparableValue::MoreThan(0).matches(u64::max_value()), "max_value should be more than 0" ); assert!( !ComparableValue::MoreThan(u64::max_value()).matches(u64::max_value()), "max_value should not be more than max_value" ); } #[test] fn comparable_value_imatches() { assert!( !ComparableValue::LessThan(0).imatches(0), "0 should not be less than 0" ); assert!( ComparableValue::LessThan(u64::max_value()).imatches(0), "0 should be less than max_value" ); assert!( !ComparableValue::LessThan(0).imatches(i64::max_value()), "max_value should not be less than 0" ); assert!( ComparableValue::LessThan(u64::max_value()).imatches(i64::max_value()), "max_value should be less than max_value" ); assert!( ComparableValue::LessThan(0).imatches(i64::min_value()), "min_value should be less than 0" ); assert!( ComparableValue::LessThan(u64::max_value()).imatches(i64::min_value()), "min_value should be less than max_value" ); assert!( ComparableValue::EqualTo(0).imatches(0), "0 should be equal to 0" ); assert!( !ComparableValue::EqualTo(u64::max_value()).imatches(0), "0 should not be equal to max_value" ); assert!( !ComparableValue::EqualTo(0).imatches(i64::max_value()), "max_value should not be equal to 0" ); assert!( !ComparableValue::EqualTo(u64::max_value()).imatches(i64::max_value()), "max_value should not be equal to i64::max_value" ); assert!( ComparableValue::EqualTo(i64::max_value() as u64).imatches(i64::max_value()), "i64::max_value should be equal to i64::max_value" ); assert!( !ComparableValue::EqualTo(0).imatches(i64::min_value()), "min_value should not be equal to 0" ); assert!( !ComparableValue::EqualTo(u64::max_value()).imatches(i64::min_value()), "min_value should not be equal to max_value" ); assert!( !ComparableValue::MoreThan(0).imatches(0), "0 should not be more than 0" ); assert!( !ComparableValue::MoreThan(u64::max_value()).imatches(0), "0 should not be more than max_value" ); assert!( ComparableValue::MoreThan(0).imatches(i64::max_value()), "max_value should be more than 0" ); assert!( !ComparableValue::MoreThan(u64::max_value()).imatches(i64::max_value()), "max_value should not be more than max_value" ); assert!( !ComparableValue::MoreThan(0).imatches(i64::min_value()), "min_value should not be more than 0" ); assert!( !ComparableValue::MoreThan(u64::max_value()).imatches(i64::min_value()), "min_value should not be more than max_value" ); } #[test] fn build_top_level_matcher_bad_ctime_value() { let mut config = Config::default(); if let Err(e) = build_top_level_matcher(&["-ctime", "-123."], &mut config) { assert!( e.to_string().contains("Expected a decimal integer"), "bad description: {}", e ); } else { panic!("parsing a bad ctime value should fail"); } } #[test] fn build_top_level_exec_not_enough_args() { let mut config = Config::default(); if let Err(e) = build_top_level_matcher(&["-exec"], &mut config) { assert!(e.to_string().contains("missing argument")); } else { panic!("parsing argument list with exec and no executable or semi-colon should fail"); } if let Err(e) = build_top_level_matcher(&["-exec", ";"], &mut config) { assert!(e.to_string().contains("missing argument")); } else { panic!("parsing argument list with exec and no executable should fail"); } if let Err(e) = build_top_level_matcher(&["-exec", "foo"], &mut config) { assert!(e.to_string().contains("missing argument")); } else { panic!("parsing argument list with exec and no executable should fail"); } } #[test] fn build_top_level_exec_should_eat_args() { let mut config = Config::default(); build_top_level_matcher(&["-exec", "foo", "-o", "(", ";"], &mut config) .expect("parsing argument list with exec that takes brackets and -os should work"); } #[test] fn build_top_level_exec_plus_semicolon() { let mut config = Config::default(); build_top_level_matcher(&["-exec", "foo", "{}", "foo", "+", ";"], &mut config) .expect("only {} + should be considered a multi-exec"); } #[test] #[cfg(unix)] fn build_top_level_matcher_perm() { let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); let mut config = Config::default(); // this should match: abbbc is readable let matcher_readable = build_top_level_matcher(&["-perm", "-u+r"], &mut config).unwrap(); // this shouldn't match: abbbc isn't executable let matcher_executable = build_top_level_matcher(&["-perm", "-u+x"], &mut config).unwrap(); let deps = FakeDependencies::new(); assert!(matcher_readable.matches(&abbbc, &mut deps.new_matcher_io())); assert_eq!(deps.get_output_as_string(), "./test_data/simple/abbbc\n"); let deps = FakeDependencies::new(); assert!(!matcher_executable.matches(&abbbc, &mut deps.new_matcher_io())); assert_eq!(deps.get_output_as_string(), ""); } #[test] #[cfg(unix)] fn build_top_level_matcher_perm_bad() { let mut config = Config::default(); if let Err(e) = build_top_level_matcher(&["-perm", "foo"], &mut config) { assert!(e.to_string().contains("invalid operator")); } else { panic!("-perm with bad mode pattern should fail"); } if let Err(e) = build_top_level_matcher(&["-perm"], &mut config) { assert!(e.to_string().contains("missing argument")); } else { panic!("-perm with no mode pattern should fail"); } } #[test] #[cfg(not(unix))] fn build_top_level_matcher_perm_not_unix() { let mut config = Config::default(); if let Err(e) = build_top_level_matcher(&["-perm", "444"], &mut config) { assert!(e.to_string().contains("not available")); } else { panic!("-perm on non-unix systems shouldn't be available"); } if let Err(e) = build_top_level_matcher(&["-perm"], &mut config) { assert!(e.to_string().contains("missing argument")); } else { panic!("-perm with no mode pattern should fail"); } } } findutils-0.4.2/src/find/matchers/name.rs000064400000000000000000000102541046102023000164350ustar 00000000000000// Copyright 2017 Google Inc. // // 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. use walkdir::DirEntry; use super::glob::Pattern; use super::{Matcher, MatcherIO}; /// This matcher makes a comparison of the name against a shell wildcard /// pattern. See `glob::Pattern` for details on the exact syntax. pub struct NameMatcher { pattern: Pattern, } impl NameMatcher { pub fn new(pattern_string: &str, caseless: bool) -> Self { let pattern = Pattern::new(pattern_string, caseless); Self { pattern } } } impl Matcher for NameMatcher { fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { let name = file_info.file_name().to_string_lossy(); self.pattern.matches(&name) } } #[cfg(test)] mod tests { use super::*; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::matchers::Matcher; use crate::find::tests::FakeDependencies; use std::io::ErrorKind; #[cfg(unix)] use std::os::unix::fs::symlink; #[cfg(windows)] use std::os::windows::fs::symlink_file; fn create_file_link() { #[cfg(unix)] if let Err(e) = symlink("abbbc", "test_data/links/link-f") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } #[cfg(windows)] if let Err(e) = symlink_file("abbbc", "test_data/links/link-f") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } } #[test] fn matching_with_wrong_case_returns_false() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = NameMatcher::new("A*C", false); let deps = FakeDependencies::new(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn matching_with_right_case_returns_true() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = NameMatcher::new("abb?c", false); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn not_matching_returns_false() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = NameMatcher::new("shouldn't match", false); let deps = FakeDependencies::new(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn matches_against_link_file_name() { create_file_link(); let link_f = get_dir_entry_for("test_data/links", "link-f"); let matcher = NameMatcher::new("link?f", false); let deps = FakeDependencies::new(); assert!(matcher.matches(&link_f, &mut deps.new_matcher_io())); } #[test] fn caseless_matching_with_wrong_case_returns_true() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = NameMatcher::new("A*C", true); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn caseless_matching_with_right_case_returns_true() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = NameMatcher::new("abb?c", true); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn caseless_not_matching_returns_false() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = NameMatcher::new("shouldn't match", true); let deps = FakeDependencies::new(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn caseless_matches_against_link_file_name() { create_file_link(); let link_f = get_dir_entry_for("test_data/links", "link-f"); let matcher = NameMatcher::new("linK?f", true); let deps = FakeDependencies::new(); assert!(matcher.matches(&link_f, &mut deps.new_matcher_io())); } } findutils-0.4.2/src/find/matchers/path.rs000064400000000000000000000050571046102023000164560ustar 00000000000000// Copyright 2017 Google Inc. // // 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. use walkdir::DirEntry; use super::glob::Pattern; use super::{Matcher, MatcherIO}; /// This matcher makes a comparison of the path against a shell wildcard /// pattern. See `glob::Pattern` for details on the exact syntax. pub struct PathMatcher { pattern: Pattern, } impl PathMatcher { pub fn new(pattern_string: &str, caseless: bool) -> Self { let pattern = Pattern::new(pattern_string, caseless); Self { pattern } } } impl Matcher for PathMatcher { fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { let path = file_info.path().to_string_lossy(); self.pattern.matches(&path) } } #[cfg(test)] mod tests { use super::*; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::matchers::Matcher; use crate::find::tests::FakeDependencies; // Variants of fix_up_slashes that properly escape the forward slashes for // being in a glob. #[cfg(windows)] fn fix_up_glob_slashes(re: &str) -> String { re.replace("/", "\\\\") } #[cfg(not(windows))] fn fix_up_glob_slashes(re: &str) -> String { re.to_owned() } #[test] fn matching_against_whole_path() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = PathMatcher::new(&fix_up_glob_slashes("test_*/*/a*c"), false); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn not_matching_against_just_name() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = PathMatcher::new("a*c", false); let deps = FakeDependencies::new(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn not_matching_against_wrong_case() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = PathMatcher::new(&fix_up_glob_slashes("test_*/*/A*C"), false); let deps = FakeDependencies::new(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn caseless_matching() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = PathMatcher::new(&fix_up_glob_slashes("test_*/*/A*C"), true); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); } } findutils-0.4.2/src/find/matchers/perm.rs000064400000000000000000000235361046102023000164670ustar 00000000000000// Copyright 2017 Google Inc. // // 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. //! find's permission matching uses a very unix-centric approach, that would //! be tricky to both implement and use on a windows platform. So we don't //! even try. use std::error::Error; use std::io::{stderr, Write}; #[cfg(unix)] use uucore::mode::{parse_numeric, parse_symbolic}; use walkdir::DirEntry; use super::{Matcher, MatcherIO}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[cfg(unix)] pub enum ComparisonType { /// mode bits have to match exactly Exact, /// all specified mode bits must be set. Others can be as well AtLeast, /// at least one of the specified bits must be set (or if no bits are /// specified then any mode will match) AnyOf, } #[cfg(unix)] impl ComparisonType { fn mode_bits_match(self, pattern: u32, value: u32) -> bool { match self { ComparisonType::Exact => (0o7777 & value) == pattern, ComparisonType::AtLeast => (value & pattern) == pattern, ComparisonType::AnyOf => pattern == 0 || (value & pattern) > 0, } } } #[cfg(unix)] mod parsing { use super::*; pub fn split_comparison_type(pattern: &str) -> (ComparisonType, &str) { let mut chars = pattern.chars(); match chars.next() { Some('-') => (ComparisonType::AtLeast, chars.as_str()), Some('/') => (ComparisonType::AnyOf, chars.as_str()), _ => (ComparisonType::Exact, pattern), } } pub fn parse_mode(pattern: &str, for_dir: bool) -> Result> { let mode = if pattern.contains(|c: char| c.is_ascii_digit()) { parse_numeric(0, pattern, for_dir)? } else { let mut mode = 0; for chunk in pattern.split(',') { mode = parse_symbolic(mode, chunk, 0, for_dir)?; } mode }; Ok(mode) } } #[cfg(unix)] #[derive(Debug)] pub struct PermMatcher { comparison_type: ComparisonType, file_pattern: u32, dir_pattern: u32, } #[cfg(not(unix))] pub struct PermMatcher {} impl PermMatcher { #[cfg(unix)] pub fn new(pattern: &str) -> Result> { let (comparison_type, pattern) = parsing::split_comparison_type(pattern); let file_pattern = parsing::parse_mode(pattern, false)?; let dir_pattern = parsing::parse_mode(pattern, false)?; Ok(Self { comparison_type, file_pattern, dir_pattern, }) } #[cfg(not(unix))] pub fn new(_dummy_pattern: &str) -> Result> { Err(From::from( "Permission matching is not available on this platform", )) } } impl Matcher for PermMatcher { #[cfg(unix)] fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { use std::os::unix::fs::PermissionsExt; match file_info.metadata() { Ok(metadata) => { let pattern = if metadata.is_dir() { self.dir_pattern } else { self.file_pattern }; self.comparison_type .mode_bits_match(pattern, metadata.permissions().mode()) } Err(e) => { writeln!( &mut stderr(), "Error getting permissions for {}: {}", file_info.path().to_string_lossy(), e ) .unwrap(); false } } } #[cfg(not(unix))] fn matches(&self, _dummy_file_info: &DirEntry, _: &mut MatcherIO) -> bool { writeln!( &mut stderr(), "Permission matching not available on this platform!" ) .unwrap(); return false; } } #[cfg(test)] #[cfg(unix)] mod tests { use super::ComparisonType::*; use super::*; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::matchers::Matcher; use crate::find::tests::FakeDependencies; #[track_caller] fn assert_parse(pattern: &str, comparison_type: ComparisonType, mode: u32) { let matcher = PermMatcher::new(pattern).unwrap(); assert_eq!(matcher.comparison_type, comparison_type); assert_eq!(matcher.file_pattern, mode); assert_eq!(matcher.dir_pattern, mode); } #[test] fn parsing_prefix() { assert_parse("u=rwx", Exact, 0o700); assert_parse("-u=rwx", AtLeast, 0o700); assert_parse("/u=rwx", AnyOf, 0o700); assert_parse("700", Exact, 0o700); assert_parse("-700", AtLeast, 0o700); assert_parse("/700", AnyOf, 0o700); } #[test] fn parsing_octal() { assert_parse("/1", AnyOf, 0o001); assert_parse("/7777", AnyOf, 0o7777); } #[test] fn parsing_human_readable_individual_bits() { assert_parse("/u=r", AnyOf, 0o400); assert_parse("/u=w", AnyOf, 0o200); assert_parse("/u=x", AnyOf, 0o100); assert_parse("/g=r", AnyOf, 0o040); assert_parse("/g=w", AnyOf, 0o020); assert_parse("/g=x", AnyOf, 0o010); assert_parse("/o+r", AnyOf, 0o004); assert_parse("/o+w", AnyOf, 0o002); assert_parse("/o+x", AnyOf, 0o001); assert_parse("/a+r", AnyOf, 0o444); assert_parse("/a+w", AnyOf, 0o222); assert_parse("/a+x", AnyOf, 0o111); } #[test] fn parsing_human_readable_multiple_bits() { assert_parse("/u=rwx", AnyOf, 0o700); assert_parse("/a=rwx", AnyOf, 0o777); } #[test] fn parsing_human_readable_multiple_categories() { assert_parse("/u=rwx,g=rx,o+r", AnyOf, 0o754); assert_parse("/u=rwx,g=rx,o+r,a+w", AnyOf, 0o776); assert_parse("/ug=rwx,o+r", AnyOf, 0o774); } #[test] fn parsing_human_readable_set_id_bits() { assert_parse("/u=s", AnyOf, 0o4000); assert_parse("/g=s", AnyOf, 0o2000); assert_parse("/ug=s", AnyOf, 0o6000); assert_parse("/o=s", AnyOf, 0o0000); } #[test] fn parsing_human_readable_sticky_bit() { assert_parse("/o=t", AnyOf, 0o1000); } #[test] fn parsing_fails() { PermMatcher::new("urwx,g=rx,o+r").expect_err("missing equals should fail"); PermMatcher::new("d=rwx,g=rx,o+r").expect_err("invalid category should fail"); PermMatcher::new("u=dwx,g=rx,o+r").expect_err("invalid permission bit should fail"); PermMatcher::new("u_rwx,g=rx,o+r") .expect_err("invalid category/permission separator should fail"); PermMatcher::new("77777777777777").expect_err("overflowing octal value should fail"); // FIXME: uucore::mode shouldn't accept this // PermMatcher::new("u=rwxg=rx,o+r") // .expect_err("missing comma should fail"); } #[test] fn comparison_type_matching() { let c = ComparisonType::Exact; assert!( c.mode_bits_match(0, 0), "Exact: only 0 should match if pattern is 0" ); assert!( !c.mode_bits_match(0, 0o444), "Exact: only 0 should match if pattern is 0" ); assert!( c.mode_bits_match(0o444, 0o444), "Exact: identical bits should match" ); assert!( !c.mode_bits_match(0o444, 0o777), "Exact: non-identical bits should fail" ); assert!( c.mode_bits_match(0o444, 0o70444), "Exact:high-end bits should be ignored" ); let c = ComparisonType::AtLeast; assert!( c.mode_bits_match(0, 0), "AtLeast: anything should match if pattern is 0" ); assert!( c.mode_bits_match(0, 0o444), "AtLeast: anything should match if pattern is 0" ); assert!( c.mode_bits_match(0o444, 0o777), "AtLeast: identical bits should match" ); assert!( c.mode_bits_match(0o444, 0o777), "AtLeast: extra bits should match" ); assert!( !c.mode_bits_match(0o444, 0o700), "AtLeast: missing bits should fail" ); assert!( c.mode_bits_match(0o444, 0o70444), "AtLeast: high-end bits should be ignored" ); let c = ComparisonType::AnyOf; assert!( c.mode_bits_match(0, 0), "AnyOf: anything should match if pattern is 0" ); assert!( c.mode_bits_match(0, 0o444), "AnyOf: anything should match if pattern is 0" ); assert!( c.mode_bits_match(0o444, 0o777), "AnyOf: identical bits should match" ); assert!( c.mode_bits_match(0o444, 0o777), "AnyOf: extra bits should match" ); assert!( c.mode_bits_match(0o777, 0o001), "AnyOf: anything should match as long as it has one bit in common" ); assert!( !c.mode_bits_match(0o010, 0o001), "AnyOf: no matching bits shouldn't match" ); assert!( c.mode_bits_match(0o444, 0o70444), "AnyOf: high-end bits should be ignored" ); } #[test] fn perm_matches() { let file_info = get_dir_entry_for("test_data/simple", "abbbc"); let deps = FakeDependencies::new(); let matcher = PermMatcher::new("-u+r").unwrap(); assert!( matcher.matches(&file_info, &mut deps.new_matcher_io()), "user-readable pattern should match file" ); let matcher = PermMatcher::new("-u+x").unwrap(); assert!( !matcher.matches(&file_info, &mut deps.new_matcher_io()), "user-executable pattern should not match file" ); } } findutils-0.4.2/src/find/matchers/printer.rs000064400000000000000000000043551046102023000172050ustar 00000000000000// Copyright 2017 Google Inc. // // 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. use walkdir::DirEntry; use super::{Matcher, MatcherIO}; pub enum PrintDelimiter { Newline, Null, } impl std::fmt::Display for PrintDelimiter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { PrintDelimiter::Newline => writeln!(f), PrintDelimiter::Null => write!(f, "\0"), } } } /// This matcher just prints the name of the file to stdout. pub struct Printer { delimiter: PrintDelimiter, } impl Printer { pub fn new(delimiter: PrintDelimiter) -> Self { Self { delimiter } } } impl Matcher for Printer { fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool { let mut out = matcher_io.deps.get_output().borrow_mut(); write!( out, "{}{}", file_info.path().to_string_lossy(), self.delimiter ) .unwrap(); out.flush().unwrap(); true } fn has_side_effects(&self) -> bool { true } } #[cfg(test)] mod tests { use super::*; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::matchers::Matcher; use crate::find::tests::fix_up_slashes; use crate::find::tests::FakeDependencies; #[test] fn prints_newline() { let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); let matcher = Printer::new(PrintDelimiter::Newline); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); assert_eq!( fix_up_slashes("./test_data/simple/abbbc\n"), deps.get_output_as_string() ); } #[test] fn prints_null() { let abbbc = get_dir_entry_for("./test_data/simple", "abbbc"); let matcher = Printer::new(PrintDelimiter::Null); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); assert_eq!( fix_up_slashes("./test_data/simple/abbbc\0"), deps.get_output_as_string() ); } } findutils-0.4.2/src/find/matchers/printf.rs000064400000000000000000001157231046102023000170260ustar 00000000000000// Copyright 2021 Collabora, Ltd. // // 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. use std::{borrow::Cow, error::Error, fs, path::Path, time::SystemTime}; use chrono::{format::StrftimeItems, DateTime, Local}; use once_cell::unsync::OnceCell; use super::{Matcher, MatcherIO}; #[cfg(unix)] use std::os::unix::prelude::{FileTypeExt, MetadataExt}; const STANDARD_BLOCK_SIZE: u64 = 512; #[derive(Debug, PartialEq, Eq)] enum Justify { Left, Right, } #[derive(Debug, PartialEq, Eq)] enum TimeFormat { /// Follow ctime(3). Ctime, /// Seconds since the epoch, as a float w/ nanosecond part. SinceEpoch, /// Follow strftime-compatible syntax Strftime(String), } impl TimeFormat { fn apply(&self, time: SystemTime) -> Result, Box> { const CTIME_FORMAT: &str = "%a %b %d %H:%M:%S.%f0 %Y"; let formatted = match self { TimeFormat::SinceEpoch => { let duration = time.duration_since(SystemTime::UNIX_EPOCH)?; format!("{}.{:09}0", duration.as_secs(), duration.subsec_nanos()) } TimeFormat::Ctime => DateTime::::from(time) .format(CTIME_FORMAT) .to_string(), TimeFormat::Strftime(format) => { DateTime::::from(time).format(format).to_string() } }; Ok(formatted.into()) } } #[derive(Debug, PartialEq, Eq)] enum PermissionsFormat { Octal, // trwxrwxrwx Symbolic, } /// A single % directive in a format string. #[derive(Debug, PartialEq, Eq)] enum FormatDirective { // %a, %Ak AccessTime(TimeFormat), // %b, %k Blocks { large_blocks: bool }, // %c, %Ck ChangeTime(TimeFormat), // %d Depth, // %D Device, // %f Basename, // %F Filesystem, // %g, %G Group { as_name: bool }, // %h Dirname, // %H StartingPoint, // %i Inode, // %l SymlinkTarget, // %m Permissions(PermissionsFormat), // %n HardlinkCount, // %p, %P Path { strip_starting_point: bool }, // %s Size, // %S Sparseness, // %t, %Tk ModificationTime(TimeFormat), // %u, %U User { as_name: bool }, // %y, %Y Type { follow_links: bool }, } /// A component in a full format string. #[derive(Debug, PartialEq, Eq)] enum FormatComponent { Literal(String), Flush, Directive { directive: FormatDirective, width: Option, justify: Justify, }, } struct FormatStringParser<'a> { string: &'a str, } impl FormatStringParser<'_> { fn front(&self) -> Result> { self.string .chars() .next() .ok_or_else(|| "Unexpected EOF".into()) } fn peek(&self, count: usize) -> Result<&str, Box> { if self.string.len() < count { return Err("Unexpected EOF".into()); } Ok(&self.string[0..count]) } fn advance_one(&mut self) -> Result> { let c = self.front()?; self.string = &self.string[1..]; Ok(c) } fn advance_by(&mut self, count: usize) -> Result<&str, Box> { self.peek(count)?; let skipped = &self.string[0..count]; self.string = &self.string[count..]; Ok(skipped) } fn parse_escape_sequence(&mut self) -> Result> { const OCTAL_LEN: usize = 3; const OCTAL_RADIX: u32 = 8; // 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(|e| e.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()), }; } } self.advance_one()?; if first == 'c' { Ok(FormatComponent::Flush) } else { let c = match first { 'a' => "\x07", 'b' => "\x08", 'f' => "\x0C", 'n' => "\n", 'r' => "\r", 't' => "\t", 'v' => "\x0B", '0' => "\0", '\\' => "\\", c => return Err(format!("Invalid escape sequence: \\{}", c).into()), }; Ok(FormatComponent::Literal(c.to_string())) } } fn parse_format_width(&mut self) -> Option { let start = self.string; let mut digits = 0; while self.front().map(|c| c.is_ascii_digit()).unwrap_or(false) { digits += 1; // safe to unwrap: the front() check already succeeded above. self.advance_one().unwrap(); } if digits > 0 { // safe to unwrap: we already know all the digits are valid due to // the above checks. Some((start[0..digits]).parse().unwrap()) } else { None } } fn parse_time_specifier(&mut self, first: char) -> Result> { match self.advance_one()? { '@' => Ok(TimeFormat::SinceEpoch), 'S' => Ok(TimeFormat::Strftime("%S.%f0".to_string())), c => { // We can't store the parsed items inside TimeFormat, because the items // take a reference to the full format string, but we still try to parse // it here so that errors get caught early. let format = format!("%{}", c); match StrftimeItems::new(&format).next() { None | Some(chrono::format::Item::Error) => { Err(format!("Invalid time specifier: %{}{}", first, c).into()) } Some(_item) => Ok(TimeFormat::Strftime(format)), } } } } fn parse_format_specifier(&mut self) -> Result> { let mut justify = Justify::Right; loop { match self.front()? { ' ' => (), '-' => justify = Justify::Left, _ => break, } // safe to unwrap: .front() already succeeded above. self.advance_one().unwrap(); } let width = self.parse_format_width(); let first = self.advance_one()?; if first == '%' { return Ok(FormatComponent::Literal("%".to_owned())); } let directive = match first { 'a' => FormatDirective::AccessTime(TimeFormat::Ctime), 'A' => FormatDirective::AccessTime(self.parse_time_specifier(first)?), 'b' => FormatDirective::Blocks { large_blocks: false, }, 'c' => FormatDirective::ChangeTime(TimeFormat::Ctime), 'C' => FormatDirective::ChangeTime(self.parse_time_specifier(first)?), 'd' => FormatDirective::Depth, 'D' => FormatDirective::Device, 'f' => FormatDirective::Basename, 'F' => FormatDirective::Filesystem, 'g' => FormatDirective::Group { as_name: true }, 'G' => FormatDirective::Group { as_name: false }, 'h' => FormatDirective::Dirname, 'H' => FormatDirective::StartingPoint, 'k' => FormatDirective::Blocks { large_blocks: true }, 'i' => FormatDirective::Inode, 'l' => FormatDirective::SymlinkTarget, 'm' => FormatDirective::Permissions(PermissionsFormat::Octal), 'M' => FormatDirective::Permissions(PermissionsFormat::Symbolic), 'n' => FormatDirective::HardlinkCount, 'p' => FormatDirective::Path { strip_starting_point: false, }, 'P' => FormatDirective::Path { strip_starting_point: true, }, 's' => FormatDirective::Size, 'S' => FormatDirective::Sparseness, 't' => FormatDirective::ModificationTime(TimeFormat::Ctime), 'T' => FormatDirective::ModificationTime(self.parse_time_specifier(first)?), 'u' => FormatDirective::User { as_name: true }, 'U' => FormatDirective::User { as_name: false }, 'y' => FormatDirective::Type { follow_links: false, }, 'Y' => FormatDirective::Type { follow_links: true }, // TODO: %Z _ => return Ok(FormatComponent::Literal(first.to_string())), }; Ok(FormatComponent::Directive { directive, width, justify, }) } pub fn parse(&mut self) -> Result> { let mut components = vec![]; while let Some(i) = self.string.find(|c| c == '%' || c == '\\') { if i > 0 { // safe to unwrap: i is an index into the string, so it cannot // be any shorter. let literal = self.advance_by(i).unwrap(); if !literal.is_empty() { components.push(FormatComponent::Literal(literal.to_owned())); } } // safe to unwrap: we've only advanced as far as 'i', which is right // before the character it identified. let component = match self.advance_one().unwrap() { '\\' => self.parse_escape_sequence()?, '%' => self.parse_format_specifier()?, _ => panic!("Stopped at unexpected character: {}", self.string), }; components.push(component); } if !self.string.is_empty() { components.push(FormatComponent::Literal(self.string.to_owned())); } Ok(FormatString { components }) } } struct FormatString { components: Vec, } impl FormatString { fn parse(string: &str) -> Result> { FormatStringParser { string }.parse() } } fn get_starting_point(file_info: &walkdir::DirEntry) -> &Path { file_info .path() .ancestors() .nth(file_info.depth()) // safe to unwrap: the file's depth should never be longer than its path // (...right?). .unwrap() } fn format_non_link_file_type(file_type: fs::FileType) -> char { if file_type.is_file() { 'f' } else if file_type.is_dir() { 'd' } else { #[cfg(unix)] if file_type.is_block_device() { 'b' } else if file_type.is_char_device() { 'c' } else if file_type.is_fifo() { 'p' } else if file_type.is_socket() { 's' } else { 'U' } #[cfg(not(unix))] 'U' } } fn format_directive<'entry>( file_info: &'entry walkdir::DirEntry, directive: &FormatDirective, meta_cell: &OnceCell, ) -> Result, Box> { let meta = || { meta_cell.get_or_try_init(|| { if file_info.path_is_symlink() && !file_info.file_type().is_symlink() { // The file_info already followed the symlink, meaning that the // metadata will be for the target file, which isn't the // behavior we want, so manually re-compute the metadata for the // symlink itself instead. file_info.path().symlink_metadata() } else { file_info.metadata().map_err(|e| e.into()) } }) }; // NOTE ON QUOTING: // GNU find's man page claims that several directives that print names (like // %f) are quoted like ls; however, I could not reproduce this at all in // practice, thus the set of rules is undoubtedly very different (if this is // still done at all). let res: Cow<'entry, str> = match directive { FormatDirective::AccessTime(tf) => tf.apply(meta()?.accessed()?)?, FormatDirective::Basename => file_info.file_name().to_string_lossy(), FormatDirective::Blocks { large_blocks } => { #[cfg(unix)] let blocks = meta()?.blocks(); #[cfg(not(unix))] // Estimate using a ceiling division by the block size. let blocks = (meta()?.len() + STANDARD_BLOCK_SIZE - 1) / STANDARD_BLOCK_SIZE; // GNU find says it returns the number of 512-byte blocks for %b, // but in reality it just returns the number of blocks, *regardless // of their size on the filesystem*. That behavior is copied here, // even though it's arguably not 100% correct. if *large_blocks { // Ceiling divide in half. (blocks + 1) / 2 } else { blocks } .to_string() .into() } #[cfg(not(unix))] FormatDirective::ChangeTime(tf) => tf.apply(meta()?.modified()?)?, #[cfg(unix)] FormatDirective::ChangeTime(tf) => { use std::time::Duration; let meta = meta()?; let ctime = SystemTime::UNIX_EPOCH + Duration::from_secs(meta.ctime() as u64) + Duration::from_nanos(meta.ctime_nsec() as u64); tf.apply(ctime)? } FormatDirective::Depth => file_info.depth().to_string().into(), #[cfg(not(unix))] FormatDirective::Device => "0".into(), #[cfg(unix)] FormatDirective::Device => meta()?.dev().to_string().into(), // GNU find's behavior for this is a bit...odd: // - Both the root directory and the paths immediately underneath return an empty string // - Any path without any slashes (i.e. relative to cwd) returns "." // - "." also returns "." // - ".." returns "." (???) // These are all (thankfully) documented on the find(1) man page. FormatDirective::Dirname => match file_info.path().parent() { None => "".into(), Some(p) if p == Path::new("/") => "".into(), Some(p) if p == Path::new("") => ".".into(), Some(parent) => parent.to_string_lossy(), }, #[cfg(not(unix))] FormatDirective::Filesystem => "".into(), #[cfg(unix)] FormatDirective::Filesystem => { let dev_id = meta()?.dev().to_string(); let fs_list = uucore::fsext::read_fs_list().expect("Could not find the filesystem info"); fs_list .into_iter() .find(|fs| fs.dev_id == dev_id) .map(|fs| fs.fs_type) .unwrap_or_else(String::new) .into() } #[cfg(not(unix))] FormatDirective::Group { .. } => "0".into(), #[cfg(unix)] FormatDirective::Group { as_name } => { let gid = meta()?.gid(); if *as_name { uucore::entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()) } else { gid.to_string() } .into() } #[cfg(not(unix))] FormatDirective::HardlinkCount => "0".into(), #[cfg(unix)] FormatDirective::HardlinkCount => meta()?.nlink().to_string().into(), #[cfg(not(unix))] FormatDirective::Inode => "0".into(), #[cfg(unix)] FormatDirective::Inode => meta()?.ino().to_string().into(), FormatDirective::ModificationTime(tf) => tf.apply(meta()?.modified()?)?, FormatDirective::Path { strip_starting_point, } => file_info .path() .strip_prefix(if *strip_starting_point { get_starting_point(file_info) } else { Path::new("") }) // safe to unwrap: the prefix is derived *from* the path to begin // with, so it cannot be invalid. .unwrap() .to_string_lossy(), FormatDirective::Permissions(PermissionsFormat::Symbolic) => { uucore::fs::display_permissions(meta()?, true).into() } #[cfg(not(unix))] FormatDirective::Permissions(PermissionsFormat::Octal) => "777".into(), #[cfg(unix)] FormatDirective::Permissions(PermissionsFormat::Octal) => { format!("{:>03o}", meta()?.mode() & 0o777).into() } FormatDirective::Size => meta()?.len().to_string().into(), #[cfg(not(unix))] FormatDirective::Sparseness => "1.0".into(), #[cfg(unix)] FormatDirective::Sparseness => { let meta = meta()?; if meta.len() > 0 { format!( "{:.1}", // GNU find hardcodes a block size of 512 bytes, regardless // of the true filesystem block size. (meta.blocks() * STANDARD_BLOCK_SIZE) as f64 / (meta.len() as f64) ) .into() } else { "1.0".into() } } FormatDirective::StartingPoint => get_starting_point(file_info).to_string_lossy(), FormatDirective::SymlinkTarget => { if file_info.path_is_symlink() { fs::read_link(file_info.path())? .to_string_lossy() .into_owned() .into() } else { "".into() } } FormatDirective::Type { follow_links } => if file_info.path_is_symlink() { if *follow_links { match file_info.path().metadata() { Ok(meta) => format_non_link_file_type(meta.file_type()), Err(e) if e.kind() == std::io::ErrorKind::NotFound => 'N', // The ErrorKinds corresponding to ELOOP and ENOTDIR are // nightly-only: // https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.FilesystemLoop // so we need to use the raw errno values instead. #[cfg(unix)] Err(e) if e.raw_os_error().unwrap_or(0) == uucore::libc::ENOTDIR => 'N', #[cfg(unix)] Err(e) if e.raw_os_error().unwrap_or(0) == uucore::libc::ELOOP => 'L', Err(_) => '?', } } else { 'l' } } else { format_non_link_file_type(file_info.file_type()) } .to_string() .into(), #[cfg(not(unix))] FormatDirective::User { .. } => "0".into(), #[cfg(unix)] FormatDirective::User { as_name } => { let uid = meta()?.uid(); if *as_name { uucore::entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()) } else { uid.to_string() } .into() } }; Ok(res) } /// This matcher prints information about its files to stdout, following GNU /// find's printf syntax. pub struct Printf { format: FormatString, } impl Printf { pub fn new(format: &str) -> Result> { Ok(Self { format: FormatString::parse(format)?, }) } } impl Matcher for Printf { fn matches(&self, file_info: &walkdir::DirEntry, matcher_io: &mut MatcherIO) -> bool { let mut out = matcher_io.deps.get_output().borrow_mut(); // The metadata is computed lazily, so that anything being printed // without needing metadata won't incur any performance overhead. let meta_cell = OnceCell::new(); for component in &self.format.components { match component { FormatComponent::Literal(literal) => write!(out, "{}", literal).unwrap(), FormatComponent::Flush => out.flush().unwrap(), FormatComponent::Directive { directive, width, justify, } => match format_directive(file_info, directive, &meta_cell) { Ok(content) => { if let Some(width) = width { match justify { Justify::Left => { write!(out, "{: { write!(out, "{:>width$}", content, width = width).unwrap(); } } } else { write!(out, "{}", content).unwrap(); } } Err(e) => { eprintln!( "Error processing '{}': {}", file_info.path().to_string_lossy(), e ); break; } }, } } true } fn has_side_effects(&self) -> bool { true } } #[cfg(test)] mod tests { use std::fs::File; use std::io::ErrorKind; use chrono::TimeZone; use tempfile::Builder; use super::*; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::matchers::Matcher; use crate::find::tests::fix_up_slashes; use crate::find::tests::FakeDependencies; #[cfg(unix)] use std::os::unix::fs::{symlink, PermissionsExt}; #[cfg(windows)] use std::os::windows::fs::{symlink_dir, symlink_file}; #[test] fn test_parse_basics() { assert_eq!(FormatString::parse("").unwrap().components, vec![]); assert_eq!( FormatString::parse("test stuff").unwrap().components, vec![FormatComponent::Literal("test stuff".to_owned()),] ); } #[test] fn test_parse_escapes() { assert_eq!( FormatString::parse("abc\\0\\t\\n\\\\\\141de\\cf") .unwrap() .components, vec![ FormatComponent::Literal("abc".to_owned()), FormatComponent::Literal("\0".to_owned()), FormatComponent::Literal("\t".to_owned()), FormatComponent::Literal("\n".to_owned()), FormatComponent::Literal("\\".to_owned()), FormatComponent::Literal("a".to_owned()), FormatComponent::Literal("de".to_owned()), FormatComponent::Flush, FormatComponent::Literal("f".to_owned()) ] ); assert!(FormatString::parse("\\X").is_err()); assert!(FormatString::parse("\\").is_err()); } #[test] fn test_parse_formatting() { fn unaligned_directive(directive: FormatDirective) -> FormatComponent { FormatComponent::Directive { directive, width: None, justify: Justify::Right, } } assert_eq!( FormatString::parse("%%%a%A@%Ak%b%c%C@%CH%d%DTEST%f%F%g%G%h%H") .unwrap() .components, vec![ FormatComponent::Literal("%".to_owned()), unaligned_directive(FormatDirective::AccessTime(TimeFormat::Ctime)), unaligned_directive(FormatDirective::AccessTime(TimeFormat::SinceEpoch)), unaligned_directive(FormatDirective::AccessTime(TimeFormat::Strftime( "%k".to_string() ))), unaligned_directive(FormatDirective::Blocks { large_blocks: false }), unaligned_directive(FormatDirective::ChangeTime(TimeFormat::Ctime)), unaligned_directive(FormatDirective::ChangeTime(TimeFormat::SinceEpoch)), unaligned_directive(FormatDirective::ChangeTime(TimeFormat::Strftime( "%H".to_string() ))), unaligned_directive(FormatDirective::Depth), unaligned_directive(FormatDirective::Device), FormatComponent::Literal("TEST".to_owned()), unaligned_directive(FormatDirective::Basename), unaligned_directive(FormatDirective::Filesystem), unaligned_directive(FormatDirective::Group { as_name: true }), unaligned_directive(FormatDirective::Group { as_name: false }), unaligned_directive(FormatDirective::Dirname), unaligned_directive(FormatDirective::StartingPoint), ] ); assert_eq!( FormatString::parse("%i%k%l%m%M%n%p%P%s%S%t%T@%Td%u%U%y%Y%?") .unwrap() .components, vec![ unaligned_directive(FormatDirective::Inode), unaligned_directive(FormatDirective::Blocks { large_blocks: true }), unaligned_directive(FormatDirective::SymlinkTarget), unaligned_directive(FormatDirective::Permissions(PermissionsFormat::Octal)), unaligned_directive(FormatDirective::Permissions(PermissionsFormat::Symbolic)), unaligned_directive(FormatDirective::HardlinkCount), unaligned_directive(FormatDirective::Path { strip_starting_point: false }), unaligned_directive(FormatDirective::Path { strip_starting_point: true }), unaligned_directive(FormatDirective::Size), unaligned_directive(FormatDirective::Sparseness), unaligned_directive(FormatDirective::ModificationTime(TimeFormat::Ctime)), unaligned_directive(FormatDirective::ModificationTime(TimeFormat::SinceEpoch)), unaligned_directive(FormatDirective::ModificationTime(TimeFormat::Strftime( "%d".to_string() ))), unaligned_directive(FormatDirective::User { as_name: true }), unaligned_directive(FormatDirective::User { as_name: false }), unaligned_directive(FormatDirective::Type { follow_links: false }), unaligned_directive(FormatDirective::Type { follow_links: true }), FormatComponent::Literal("?".to_owned()), ] ); assert!(FormatString::parse("%").is_err()); assert!(FormatString::parse("%A!").is_err()); } #[test] fn test_parse_formatting_justified() { assert_eq!( FormatString::parse("%d%-s%5S%-12n% 3f% -- 4i") .unwrap() .components, vec![ FormatComponent::Directive { directive: FormatDirective::Depth, justify: Justify::Right, width: None }, FormatComponent::Directive { directive: FormatDirective::Size, justify: Justify::Left, width: None }, FormatComponent::Directive { directive: FormatDirective::Sparseness, justify: Justify::Right, width: Some(5) }, FormatComponent::Directive { directive: FormatDirective::HardlinkCount, justify: Justify::Left, width: Some(12) }, FormatComponent::Directive { directive: FormatDirective::Basename, justify: Justify::Right, width: Some(3) }, FormatComponent::Directive { directive: FormatDirective::Inode, justify: Justify::Left, width: Some(4) }, ] ); } #[test] fn test_printf_justified() { let file_info = get_dir_entry_for("test_data/simple", "abbbc"); let deps = FakeDependencies::new(); let matcher = Printf::new("%f,%7f,%-7f").unwrap(); assert!(matcher.matches(&file_info, &mut deps.new_matcher_io())); assert_eq!("abbbc, abbbc,abbbc ", deps.get_output_as_string()); } #[test] fn test_printf_paths() { let file_info = get_dir_entry_for("test_data/simple", "abbbc"); let deps = FakeDependencies::new(); let matcher = Printf::new("%h %H %p %P").unwrap(); assert!(matcher.matches(&file_info, &mut deps.new_matcher_io())); assert_eq!( format!( "{} {} {} {}", fix_up_slashes("test_data/simple"), fix_up_slashes("test_data/simple"), fix_up_slashes("test_data/simple/abbbc"), fix_up_slashes("abbbc") ), deps.get_output_as_string() ); } #[test] fn test_printf_paths_in_subdir() { let file_info = get_dir_entry_for("test_data/simple", "subdir/ABBBC"); let deps = FakeDependencies::new(); let matcher = Printf::new("%h %H %p %P").unwrap(); assert!(matcher.matches(&file_info, &mut deps.new_matcher_io())); assert_eq!( format!( "{} {} {} {}", fix_up_slashes("test_data/simple/subdir"), fix_up_slashes("test_data/simple"), fix_up_slashes("test_data/simple/subdir/ABBBC"), fix_up_slashes("subdir/ABBBC") ), deps.get_output_as_string() ); } #[test] fn test_printf_depth() { let file_info_1 = get_dir_entry_for("test_data/depth/1", "f1"); let file_info_2 = get_dir_entry_for("test_data/depth/1", "2/f2"); let deps = FakeDependencies::new(); let matcher = Printf::new("%d.").unwrap(); assert!(matcher.matches(&file_info_1, &mut deps.new_matcher_io())); assert!(matcher.matches(&file_info_2, &mut deps.new_matcher_io())); assert_eq!("1.2.", deps.get_output_as_string()); } #[test] fn test_printf_basic_types() { let file_info_f = get_dir_entry_for("test_data/simple", "abbbc"); let file_info_d = get_dir_entry_for("test_data/simple", "subdir"); let deps = FakeDependencies::new(); let matcher = Printf::new("%y").unwrap(); assert!(matcher.matches(&file_info_f, &mut deps.new_matcher_io())); assert!(matcher.matches(&file_info_d, &mut deps.new_matcher_io())); assert_eq!("fd", deps.get_output_as_string()); } #[test] #[cfg(unix)] fn test_printf_special_types() { use std::os::unix::net::UnixListener; use nix::sys::stat::Mode; let temp_dir = Builder::new().prefix("example").tempdir().unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); let fifo_name = "fifo"; let fifo_path = temp_dir.path().join(fifo_name); nix::unistd::mkfifo(&fifo_path, Mode::from_bits(0o644).unwrap()).unwrap(); let socket_name = "socket"; let socket_path = temp_dir.path().join(socket_name); UnixListener::bind(socket_path).unwrap(); let fifo_info = get_dir_entry_for(&temp_dir_path, fifo_name); let socket_info = get_dir_entry_for(&temp_dir_path, socket_name); let deps = FakeDependencies::new(); let matcher = Printf::new("%y").unwrap(); assert!(matcher.matches(&fifo_info, &mut deps.new_matcher_io())); assert!(matcher.matches(&socket_info, &mut deps.new_matcher_io())); assert_eq!("ps", deps.get_output_as_string()); } #[test] fn test_printf_size() { let file_info = get_dir_entry_for("test_data/size", "512bytes"); let deps = FakeDependencies::new(); let matcher = Printf::new("%s").unwrap(); assert!(matcher.matches(&file_info, &mut deps.new_matcher_io())); assert_eq!("512", deps.get_output_as_string()); } #[test] fn test_printf_symlinks() { #[cfg(unix)] { if let Err(e) = symlink("abbbc", "test_data/links/link-f") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } if let Err(e) = symlink("subdir", "test_data/links/link-d") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } if let Err(e) = symlink("missing", "test_data/links/link-missing") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } if let Err(e) = symlink("abbbc/x", "test_data/links/link-notdir") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } if let Err(e) = symlink("link-loop", "test_data/links/link-loop") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } } #[cfg(windows)] { if let Err(e) = symlink_file("abbbc", "test_data/links/link-f") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } if let Err(e) = symlink_dir("subdir", "test_data/links/link-d") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } if let Err(e) = symlink_file("missing", "test_data/links/link-missing") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } if let Err(e) = symlink_file("abbbc/x", "test_data/links/link-notdir") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } } let regular_file = get_dir_entry_for("test_data/simple", "abbbc"); let link_f = get_dir_entry_for("test_data/links", "link-f"); let link_d = get_dir_entry_for("test_data/links", "link-d"); let link_missing = get_dir_entry_for("test_data/links", "link-missing"); let link_notdir = get_dir_entry_for("test_data/links", "link-notdir"); #[cfg(unix)] let link_loop = get_dir_entry_for("test_data/links", "link-loop"); let deps = FakeDependencies::new(); let matcher = Printf::new("%y %Y %l\n").unwrap(); assert!(matcher.matches(®ular_file, &mut deps.new_matcher_io())); assert!(matcher.matches(&link_f, &mut deps.new_matcher_io())); assert!(matcher.matches(&link_d, &mut deps.new_matcher_io())); assert!(matcher.matches(&link_missing, &mut deps.new_matcher_io())); assert!(matcher.matches(&link_notdir, &mut deps.new_matcher_io())); #[cfg(unix)] assert!(matcher.matches(&link_loop, &mut deps.new_matcher_io())); assert_eq!( vec![ "f f ", "l f abbbc", "l d subdir", "l N missing", // We can't detect ENOTDIR on non-unix platforms yet. #[cfg(not(unix))] "l ? abbbc/x", #[cfg(unix)] "l N abbbc/x", #[cfg(unix)] "l L link-loop", ], deps.get_output_as_string().lines().collect::>() ); } #[test] fn test_printf_times() { let temp_dir = Builder::new().prefix("example").tempdir().unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); let new_file_name = "newFile"; let file_path = temp_dir.path().join(new_file_name); File::create(&file_path).expect("create temp file"); let mtime = chrono::Local .ymd(2000, 01, 15) .and_hms_nano(09, 30, 21, 2000000); filetime::set_file_mtime( &file_path, filetime::FileTime::from_unix_time(mtime.timestamp(), mtime.timestamp_subsec_nanos()), ) .expect("set temp file mtime"); let file_info = get_dir_entry_for(&temp_dir_path, new_file_name); let deps = FakeDependencies::new(); let matcher = Printf::new("%t,%T@,%TF").unwrap(); assert!(matcher.matches(&file_info, &mut deps.new_matcher_io())); assert_eq!( format!( "Sat Jan 15 09:30:21.0020000000 2000,{}.0020000000,2000-01-15", mtime.timestamp() ), deps.get_output_as_string() ); } #[test] #[cfg(unix)] fn test_printf_user_group() { let temp_dir = Builder::new().prefix("example").tempdir().unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); let new_file_name = "newFile"; File::create(temp_dir.path().join(new_file_name)).expect("create temp file"); let uid = unsafe { uucore::libc::getuid() }; let user = uucore::entries::uid2usr(uid).unwrap_or(uid.to_string()); let gid = unsafe { uucore::libc::getgid() }; let group = uucore::entries::gid2grp(gid).unwrap_or(gid.to_string()); let file_info = get_dir_entry_for(&temp_dir_path, new_file_name); let deps = FakeDependencies::new(); let matcher = Printf::new("%u %U %g %G").unwrap(); assert!(matcher.matches(&file_info, &mut deps.new_matcher_io())); assert_eq!( format!("{} {} {} {}", user, uid, group, gid), deps.get_output_as_string() ); } #[test] #[cfg(unix)] fn test_printf_permissions() { use std::fs::File; let temp_dir = Builder::new().prefix("example").tempdir().unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); let new_file_name = "newFile"; let file = File::create(temp_dir.path().join(new_file_name)).expect("create temp file"); let file_info = get_dir_entry_for(&temp_dir_path, new_file_name); let deps = FakeDependencies::new(); let mut perms = file_info.metadata().unwrap().permissions(); perms.set_mode(0o755); file.set_permissions(perms).unwrap(); let matcher = Printf::new("%m %M").unwrap(); assert!(matcher.matches(&file_info, &mut deps.new_matcher_io())); assert_eq!("755 -rwxr-xr-x", deps.get_output_as_string()); } } findutils-0.4.2/src/find/matchers/prune.rs000064400000000000000000000032051046102023000166440ustar 00000000000000// Copyright 2017 Google Inc. // // 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. use walkdir::DirEntry; use super::{Matcher, MatcherIO}; /// This matcher checks the type of the file. pub struct PruneMatcher; impl PruneMatcher { pub fn new() -> Self { Self {} } } impl Matcher for PruneMatcher { fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool { if file_info.file_type().is_dir() { matcher_io.mark_current_dir_to_be_skipped(); } true } } #[cfg(test)] mod tests { use super::*; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::matchers::Matcher; use crate::find::tests::FakeDependencies; #[test] fn file_type_matcher() { let dir = get_dir_entry_for("test_data", "simple"); let deps = FakeDependencies::new(); let mut matcher_io = deps.new_matcher_io(); assert!(!matcher_io.should_skip_current_dir()); let matcher = PruneMatcher::new(); assert!(matcher.matches(&dir, &mut matcher_io)); assert!(matcher_io.should_skip_current_dir()); } #[test] fn only_skips_directories() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let deps = FakeDependencies::new(); let mut matcher_io = deps.new_matcher_io(); assert!(!matcher_io.should_skip_current_dir()); let matcher = PruneMatcher::new(); assert!(matcher.matches(&abbbc, &mut matcher_io)); assert!(!matcher_io.should_skip_current_dir()); } } findutils-0.4.2/src/find/matchers/quit.rs000064400000000000000000000020221046102023000164710ustar 00000000000000// Copyright 2017 Tavian Barnes // // 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. use walkdir::DirEntry; use super::{Matcher, MatcherIO}; /// This matcher quits the search immediately. pub struct QuitMatcher; impl Matcher for QuitMatcher { fn matches(&self, _: &DirEntry, matcher_io: &mut MatcherIO) -> bool { matcher_io.quit(); true } } #[cfg(test)] mod tests { use super::*; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::matchers::Matcher; use crate::find::tests::FakeDependencies; #[test] fn quits_when_matched() { let dir = get_dir_entry_for("test_data", "simple"); let deps = FakeDependencies::new(); let mut matcher_io = deps.new_matcher_io(); assert!(!matcher_io.should_quit()); let matcher = QuitMatcher; assert!(matcher.matches(&dir, &mut matcher_io)); assert!(matcher_io.should_quit()); } } findutils-0.4.2/src/find/matchers/regex.rs000064400000000000000000000150671046102023000166360ustar 00000000000000// Copyright 2022 Collabora, Ltd. // // 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. use std::{error::Error, fmt, str::FromStr}; use onig::{Regex, RegexOptions, Syntax}; use super::Matcher; #[derive(Debug)] pub struct ParseRegexTypeError(String); impl Error for ParseRegexTypeError {} impl fmt::Display for ParseRegexTypeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "Invalid regex type: {} (must be one of {})", self.0, RegexType::VALUES .iter() .map(|t| format!("'{}'", t)) .collect::>() .join(", ") ) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RegexType { Emacs, Grep, PosixBasic, PosixExtended, } impl RegexType { pub const VALUES: &'static [Self] = &[ Self::Emacs, Self::Grep, Self::PosixBasic, Self::PosixExtended, ]; } impl fmt::Display for RegexType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { RegexType::Emacs => write!(f, "emacs"), RegexType::Grep => write!(f, "grep"), RegexType::PosixBasic => write!(f, "posix-basic"), RegexType::PosixExtended => write!(f, "posix-extended"), } } } impl FromStr for RegexType { type Err = ParseRegexTypeError; fn from_str(s: &str) -> Result { match s { "emacs" => Ok(Self::Emacs), "grep" => Ok(Self::Grep), "posix-basic" => Ok(Self::PosixBasic), "posix-extended" => Ok(Self::PosixExtended), // ed and sed are the same as posix-basic "ed" | "sed" => Ok(Self::PosixBasic), _ => Err(ParseRegexTypeError(s.to_owned())), } } } impl Default for RegexType { fn default() -> Self { Self::Emacs } } pub struct RegexMatcher { regex: Regex, } impl RegexMatcher { pub fn new( regex_type: RegexType, pattern: &str, ignore_case: bool, ) -> Result> { let syntax = match regex_type { RegexType::Emacs => Syntax::emacs(), RegexType::Grep => Syntax::grep(), RegexType::PosixBasic => Syntax::posix_basic(), RegexType::PosixExtended => Syntax::posix_extended(), }; let regex = Regex::with_options( pattern, if ignore_case { RegexOptions::REGEX_OPTION_IGNORECASE } else { RegexOptions::REGEX_OPTION_NONE }, syntax, )?; Ok(Self { regex }) } } impl Matcher for RegexMatcher { fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool { self.regex .is_match(file_info.path().to_string_lossy().as_ref()) } } #[cfg(test)] mod tests { use super::*; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::matchers::Matcher; use crate::find::tests::FakeDependencies; const POSIX_BASIC_INTERVALS_RE: &str = r".*/ab\{1,3\}c"; const POSIX_EXTENDED_INTERVALS_RE: &str = r".*/ab{1,3}c"; const EMACS_AND_POSIX_EXTENDED_KLEENE_PLUS: &str = r".*/ab+c"; // Variants of fix_up_slashes that properly escape the forward slashes for // being in a regex. #[cfg(windows)] fn fix_up_regex_slashes(re: &str) -> String { re.replace("/", r"\\") } #[cfg(not(windows))] fn fix_up_regex_slashes(re: &str) -> String { re.to_owned() } #[test] fn case_sensitive_matching() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = RegexMatcher::new(RegexType::Emacs, &fix_up_regex_slashes(".*/ab.BC"), false).unwrap(); let deps = FakeDependencies::new(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn case_insensitive_matching() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = RegexMatcher::new(RegexType::Emacs, &fix_up_regex_slashes(".*/ab.BC"), true).unwrap(); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn emacs_regex() { // Emacs syntax is mostly the same as POSIX extended but with escaped // brace intervals. let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = RegexMatcher::new( RegexType::Emacs, &fix_up_regex_slashes(EMACS_AND_POSIX_EXTENDED_KLEENE_PLUS), true, ) .unwrap(); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); let matcher = RegexMatcher::new( RegexType::Emacs, &fix_up_regex_slashes(POSIX_EXTENDED_INTERVALS_RE), true, ) .unwrap(); let deps = FakeDependencies::new(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn posix_basic_regex() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = RegexMatcher::new( RegexType::PosixBasic, &fix_up_regex_slashes(POSIX_BASIC_INTERVALS_RE), true, ) .unwrap(); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); let matcher = RegexMatcher::new( RegexType::PosixBasic, &fix_up_regex_slashes(POSIX_EXTENDED_INTERVALS_RE), true, ) .unwrap(); let deps = FakeDependencies::new(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); } #[test] fn posix_extended_regex() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = RegexMatcher::new( RegexType::PosixExtended, &fix_up_regex_slashes(POSIX_EXTENDED_INTERVALS_RE), true, ) .unwrap(); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); let matcher = RegexMatcher::new( RegexType::PosixExtended, &fix_up_regex_slashes(POSIX_BASIC_INTERVALS_RE), true, ) .unwrap(); let deps = FakeDependencies::new(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); } } findutils-0.4.2/src/find/matchers/size.rs000064400000000000000000000127571046102023000165010ustar 00000000000000// Copyright 2017 Google Inc. // // 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. use std::error::Error; use std::io::{stderr, Write}; use std::str::FromStr; use walkdir::DirEntry; use super::{ComparableValue, Matcher, MatcherIO}; #[derive(Clone, Copy, Debug)] enum Unit { Byte, TwoByteWord, Block, KibiByte, MebiByte, GibiByte, } impl FromStr for Unit { type Err = Box; fn from_str(s: &str) -> Result> { Ok(match s { "c" => Self::Byte, "w" => Self::TwoByteWord, "" | "b" => Self::Block, "k" => Self::KibiByte, "M" => Self::MebiByte, "G" => Self::GibiByte, _ => { return Err(From::from(format!( "Invalid suffix {} for -size. Only allowed \ values are , b, c, w, k, M or G", s ))); } }) } } fn byte_size_to_unit_size(unit: Unit, byte_size: u64) -> u64 { // Short circuit (to avoid a overflow error when subtracting 1 later on) if byte_size == 0 { return 0; } let bits_to_shift = match unit { Unit::Byte => 0, Unit::TwoByteWord => 1, Unit::Block => 9, Unit::KibiByte => 10, Unit::MebiByte => 20, Unit::GibiByte => 30, }; // Skip pointless arithmetic. if bits_to_shift == 0 { return byte_size; } // We want to round up (e.g. 1 byte - 1024 bytes = 1k. // 1025 bytes to 2048 bytes = 2k etc. ((byte_size - 1) >> bits_to_shift) + 1 } /// Matcher that checks whether a file's size if {less than | equal to | more than} /// N units in size. pub struct SizeMatcher { value_to_match: ComparableValue, unit: Unit, } impl SizeMatcher { pub fn new( value_to_match: ComparableValue, suffix_string: &str, ) -> Result> { Ok(Self { unit: suffix_string.parse()?, value_to_match, }) } } impl Matcher for SizeMatcher { fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { match file_info.metadata() { Ok(metadata) => self .value_to_match .matches(byte_size_to_unit_size(self.unit, metadata.len())), Err(e) => { writeln!( &mut stderr(), "Error getting file size for {}: {}", file_info.path().to_string_lossy(), e ) .unwrap(); false } } } } #[cfg(test)] mod tests { use super::*; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::matchers::{ComparableValue, Matcher}; use crate::find::tests::FakeDependencies; // need to explicitly use non-pub members use super::{byte_size_to_unit_size, Unit}; #[test] fn test_byte_size_to_unit_size() { assert_eq!(byte_size_to_unit_size(Unit::KibiByte, 0), 0); assert_eq!(byte_size_to_unit_size(Unit::KibiByte, 1), 1); assert_eq!(byte_size_to_unit_size(Unit::KibiByte, 1024), 1); assert_eq!(byte_size_to_unit_size(Unit::KibiByte, 1025), 2); assert_eq!(byte_size_to_unit_size(Unit::Byte, 1025), 1025); assert_eq!(byte_size_to_unit_size(Unit::TwoByteWord, 1025), 513); assert_eq!(byte_size_to_unit_size(Unit::Block, 1025), 3); assert_eq!(byte_size_to_unit_size(Unit::KibiByte, 1025), 2); assert_eq!(byte_size_to_unit_size(Unit::MebiByte, 1024 * 1024 + 1), 2); assert_eq!( byte_size_to_unit_size(Unit::GibiByte, 1024 * 1024 * 1024 + 1), 2 ); } #[test] fn unit_from_string() { assert_eq!(byte_size_to_unit_size("c".parse().unwrap(), 2), 2); assert_eq!(byte_size_to_unit_size("w".parse().unwrap(), 3), 2); assert_eq!(byte_size_to_unit_size("b".parse().unwrap(), 513), 2); assert_eq!(byte_size_to_unit_size("".parse().unwrap(), 513), 2); assert_eq!(byte_size_to_unit_size("k".parse().unwrap(), 1025), 2); assert_eq!( byte_size_to_unit_size("M".parse().unwrap(), 1024 * 1024 + 1), 2 ); assert_eq!( byte_size_to_unit_size("G".parse().unwrap(), 2024 * 1024 * 1024 + 1), 2 ); } #[test] fn size_matcher_bad_unit() { if let Err(e) = SizeMatcher::new(ComparableValue::EqualTo(2), "xyz") { assert!( e.to_string().contains("Invalid suffix") && e.to_string().contains("xyz"), "bad description: {}", e ); } else { panic!("parsing a unit string should fail"); } } #[test] fn size_matcher() { let file_info = get_dir_entry_for("./test_data/size", "512bytes"); let equal_to_2_blocks = SizeMatcher::new(ComparableValue::EqualTo(2), "b").unwrap(); let equal_to_1_blocks = SizeMatcher::new(ComparableValue::EqualTo(1), "b").unwrap(); let deps = FakeDependencies::new(); assert!( !equal_to_2_blocks.matches(&file_info, &mut deps.new_matcher_io()), "512-byte file should not match size of 2 blocks" ); assert!( equal_to_1_blocks.matches(&file_info, &mut deps.new_matcher_io()), "512-byte file should match size of 1 block" ); } } findutils-0.4.2/src/find/matchers/stat.rs000064400000000000000000000076011046102023000164720ustar 00000000000000// Copyright 2022 Tavian Barnes // // 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. #[cfg(unix)] use std::os::unix::fs::MetadataExt; use std::error::Error; use walkdir::DirEntry; use super::{ComparableValue, Matcher, MatcherIO}; /// Inode number matcher. pub struct InodeMatcher { ino: ComparableValue, } impl InodeMatcher { #[cfg(unix)] pub fn new(ino: ComparableValue) -> Result> { Ok(Self { ino }) } #[cfg(not(unix))] pub fn new(_ino: ComparableValue) -> Result> { Err(From::from( "Inode numbers are not available on this platform", )) } } impl Matcher for InodeMatcher { #[cfg(unix)] fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { match file_info.metadata() { Ok(metadata) => self.ino.matches(metadata.ino()), Err(_) => false, } } #[cfg(not(unix))] fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool { unreachable!("Inode numbers are not available on this platform") } } /// Link count matcher. pub struct LinksMatcher { nlink: ComparableValue, } impl LinksMatcher { #[cfg(unix)] pub fn new(nlink: ComparableValue) -> Result> { Ok(Self { nlink }) } #[cfg(not(unix))] pub fn new(_nlink: ComparableValue) -> Result> { Err(From::from("Link counts are not available on this platform")) } } impl Matcher for LinksMatcher { #[cfg(unix)] fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { match file_info.metadata() { Ok(metadata) => self.nlink.matches(metadata.nlink()), Err(_) => false, } } #[cfg(not(unix))] fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool { unreachable!("Link counts are not available on this platform") } } #[cfg(test)] #[cfg(unix)] mod tests { use super::*; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::matchers::Matcher; use crate::find::tests::FakeDependencies; #[test] fn inode_matcher() { let file_info = get_dir_entry_for("test_data/simple", "abbbc"); let metadata = file_info.metadata().unwrap(); let deps = FakeDependencies::new(); let matcher = InodeMatcher::new(ComparableValue::EqualTo(metadata.ino())).unwrap(); assert!( matcher.matches(&file_info, &mut deps.new_matcher_io()), "inode number should match" ); let matcher = InodeMatcher::new(ComparableValue::LessThan(metadata.ino())).unwrap(); assert!( !matcher.matches(&file_info, &mut deps.new_matcher_io()), "inode number should not match" ); let matcher = InodeMatcher::new(ComparableValue::MoreThan(metadata.ino())).unwrap(); assert!( !matcher.matches(&file_info, &mut deps.new_matcher_io()), "inode number should not match" ); } #[test] fn links_matcher() { let file_info = get_dir_entry_for("test_data/simple", "abbbc"); let deps = FakeDependencies::new(); let matcher = LinksMatcher::new(ComparableValue::EqualTo(1)).unwrap(); assert!( matcher.matches(&file_info, &mut deps.new_matcher_io()), "link count should match" ); let matcher = LinksMatcher::new(ComparableValue::LessThan(1)).unwrap(); assert!( !matcher.matches(&file_info, &mut deps.new_matcher_io()), "link count should not match" ); let matcher = LinksMatcher::new(ComparableValue::MoreThan(1)).unwrap(); assert!( !matcher.matches(&file_info, &mut deps.new_matcher_io()), "link count should not match" ); } } findutils-0.4.2/src/find/matchers/time.rs000064400000000000000000000326541046102023000164630ustar 00000000000000// Copyright 2017 Google Inc. // // 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. use std::error::Error; use std::fs::{self, Metadata}; use std::io::{stderr, Write}; use std::time::SystemTime; use walkdir::DirEntry; use super::{ComparableValue, Matcher, MatcherIO}; const SECONDS_PER_DAY: i64 = 60 * 60 * 24; /// This matcher checks whether a file is newer than the file the matcher is initialized with. pub struct NewerMatcher { given_modification_time: SystemTime, } impl NewerMatcher { pub fn new(path_to_file: &str) -> Result> { let metadata = fs::metadata(path_to_file)?; Ok(Self { given_modification_time: metadata.modified()?, }) } /// Implementation of matches that returns a result, allowing use to use try! /// to deal with the errors. fn matches_impl(&self, file_info: &DirEntry) -> Result> { let this_time = file_info.metadata()?.modified()?; // duration_since returns an Ok duration if this_time <= given_modification_time // and returns an Err (with a duration) otherwise. So if this_time > // given_modification_time (in which case we want to return true) then // duration_since will return an error. Ok(self .given_modification_time .duration_since(this_time) .is_err()) } } impl Matcher for NewerMatcher { fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { match self.matches_impl(file_info) { Err(e) => { writeln!( &mut stderr(), "Error getting modification time for {}: {}", file_info.path().to_string_lossy(), e ) .unwrap(); false } Ok(t) => t, } } } #[derive(Clone, Copy, Debug)] pub enum FileTimeType { Accessed, Created, Modified, } impl FileTimeType { fn get_file_time(self, metadata: Metadata) -> std::io::Result { match self { FileTimeType::Accessed => metadata.accessed(), FileTimeType::Created => metadata.created(), FileTimeType::Modified => metadata.modified(), } } } /// This matcher checks whether a file's accessed|creation|modification time is /// {less than | exactly | more than} N days old. pub struct FileTimeMatcher { days: ComparableValue, file_time_type: FileTimeType, } impl Matcher for FileTimeMatcher { fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool { match self.matches_impl(file_info, matcher_io.now()) { Err(e) => { writeln!( &mut stderr(), "Error getting {:?} time for {}: {}", self.file_time_type, file_info.path().to_string_lossy(), e ) .unwrap(); false } Ok(t) => t, } } } impl FileTimeMatcher { /// Implementation of matches that returns a result, allowing use to use try! /// to deal with the errors. fn matches_impl(&self, file_info: &DirEntry, now: SystemTime) -> Result> { let this_time = self.file_time_type.get_file_time(file_info.metadata()?)?; let mut is_negative = false; // durations can't be negative. So duration_since returns a duration // wrapped in an error if now < this_time. let age = match now.duration_since(this_time) { Ok(duration) => duration, Err(e) => { is_negative = true; e.duration() } }; let age_in_seconds: i64 = age.as_secs() as i64 * if is_negative { -1 } else { 1 }; // rust division truncates towards zero (see // https://github.com/rust-lang/rust/blob/master/src/libcore/ops.rs#L580 ) // so a simple age_in_seconds / SECONDS_PER_DAY gives the wrong answer // for negative ages: a file whose age is 1 second in the future needs to // count as -1 day old, not 0. let age_in_days = age_in_seconds / SECONDS_PER_DAY + if is_negative { -1 } else { 0 }; Ok(self.days.imatches(age_in_days)) } pub fn new(file_time_type: FileTimeType, days: ComparableValue) -> Self { Self { days, file_time_type, } } } #[cfg(test)] mod tests { use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; use std::thread; use std::time::{Duration, SystemTime}; use tempfile::Builder; use walkdir::DirEntry; use super::*; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::matchers::{ComparableValue, Matcher}; use crate::find::tests::FakeDependencies; #[test] fn newer_matcher() { // this file should already exist let old_file = get_dir_entry_for("test_data", "simple"); let temp_dir = Builder::new().prefix("example").tempdir().unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); // this has just been created, so should be newer let new_file_name = "newFile"; File::create(temp_dir.path().join(new_file_name)).expect("create temp file"); let new_file = get_dir_entry_for(&temp_dir_path, new_file_name); let matcher_for_new = NewerMatcher::new(&temp_dir.path().join(new_file_name).to_string_lossy()).unwrap(); let matcher_for_old = NewerMatcher::new(&old_file.path().to_string_lossy()).unwrap(); let deps = FakeDependencies::new(); assert!( !matcher_for_new.matches(&old_file, &mut deps.new_matcher_io()), "old_file shouldn't be newer than new_dir" ); assert!( matcher_for_old.matches(&new_file, &mut deps.new_matcher_io()), "new_file should be newer than old_dir" ); assert!( !matcher_for_old.matches(&old_file, &mut deps.new_matcher_io()), "old_file shouldn't be newer than itself" ); } #[test] fn file_time_matcher() { // this file should already exist let file = get_dir_entry_for("test_data", "simple"); let files_mtime = file.metadata().unwrap().modified().unwrap(); let exactly_one_day_matcher = FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::EqualTo(1)); let more_than_one_day_matcher = FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::MoreThan(1)); let less_than_one_day_matcher = FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::LessThan(1)); let zero_day_matcher = FileTimeMatcher::new(FileTimeType::Modified, ComparableValue::EqualTo(0)); // set "now" to 2 days after the file was modified. let mut deps = FakeDependencies::new(); deps.set_time(files_mtime + Duration::new(2 * super::SECONDS_PER_DAY as u64, 0)); assert!( !exactly_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), "2 day old file shouldn't match exactly 1 day old" ); assert!( more_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), "2 day old file should match more than 1 day old" ); assert!( !less_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), "2 day old file shouldn't match less than 1 day old" ); assert!( !zero_day_matcher.matches(&file, &mut deps.new_matcher_io()), "2 day old file shouldn't match exactly 0 days old" ); // set "now" to 1 day after the file was modified. deps.set_time(files_mtime + Duration::new((3 * super::SECONDS_PER_DAY / 2) as u64, 0)); assert!( exactly_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), "1 day old file should match exactly 1 day old" ); assert!( !more_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), "1 day old file shouldn't match more than 1 day old" ); assert!( !less_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), "1 day old file shouldn't match less than 1 day old" ); assert!( !zero_day_matcher.matches(&file, &mut deps.new_matcher_io()), "1 day old file shouldn't match exactly 0 days old" ); // set "now" to exactly the same time file was modified. deps.set_time(files_mtime); assert!( !exactly_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), "0 day old file shouldn't match exactly 1 day old" ); assert!( !more_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), "0 day old file shouldn't match more than 1 day old" ); assert!( less_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), "0 day old file should match less than 1 day old" ); assert!( zero_day_matcher.matches(&file, &mut deps.new_matcher_io()), "0 day old file should match exactly 0 days old" ); // set "now" to a second before the file was modified (e.g. the file was // modified after find started running deps.set_time(files_mtime - Duration::new(1_u64, 0)); assert!( !exactly_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), "future-modified file shouldn't match exactly 1 day old" ); assert!( !more_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), "future-modified file shouldn't match more than 1 day old" ); assert!( less_than_one_day_matcher.matches(&file, &mut deps.new_matcher_io()), "future-modified file should match less than 1 day old" ); assert!( !zero_day_matcher.matches(&file, &mut deps.new_matcher_io()), "future-modified file shouldn't match exactly 0 days old" ); } #[test] fn file_time_matcher_modified_created_accessed() { let temp_dir = Builder::new() .prefix("file_time_matcher_modified_created_accessed") .tempdir() .unwrap(); // No easy way to independently set file times. So create it - setting creation time let foo_path = temp_dir.path().join("foo"); { File::create(&foo_path).expect("create temp file"); } thread::sleep(Duration::from_secs(2)); // read the file - potentially changing accessed time let mut buffer = [0; 10]; { let mut f = File::open(&foo_path).expect("open temp file"); let _ = f.read(&mut buffer); } thread::sleep(Duration::from_secs(2)); // write to the file - changing the modification and potentially the accessed time let mut buffer = [0; 10]; { let mut f = OpenOptions::new() .read(true) .write(true) .open(&foo_path) .expect("open temp file"); let _ = f.write(&mut buffer); } thread::sleep(Duration::from_secs(2)); // read the file again - potentially changing accessed time { let mut f = File::open(&foo_path).expect("open temp file"); let _ = f.read(&mut buffer); } // OK our modification time and creation time should definitely be different // and depending on our platform and file system, our accessed time might be // different too. let file_info = get_dir_entry_for(&temp_dir.path().to_string_lossy(), "foo"); let metadata = file_info.metadata().unwrap(); // metadata can return errors like StringError("creation time is not available on this platform currently") // so skip tests that won't pass due to shortcomings in std:;fs. if let Ok(accessed_time) = metadata.accessed() { test_matcher_for_file_time_type(&file_info, accessed_time, FileTimeType::Accessed); } if let Ok(creation_time) = metadata.created() { test_matcher_for_file_time_type(&file_info, creation_time, FileTimeType::Created); } if let Ok(modified_time) = metadata.modified() { test_matcher_for_file_time_type(&file_info, modified_time, FileTimeType::Modified); } } /// helper function for file_time_matcher_modified_created_accessed fn test_matcher_for_file_time_type( file_info: &DirEntry, file_time: SystemTime, file_time_type: FileTimeType, ) { { let matcher = FileTimeMatcher::new(file_time_type, ComparableValue::EqualTo(0)); let mut deps = FakeDependencies::new(); deps.set_time(file_time); assert!( matcher.matches(file_info, &mut deps.new_matcher_io()), "{:?} time matcher should match", file_time_type ); deps.set_time(file_time - Duration::from_secs(1)); assert!( !matcher.matches(file_info, &mut deps.new_matcher_io()), "{:?} time matcher shouldn't match a second before", file_time_type ); } } } findutils-0.4.2/src/find/matchers/type_matcher.rs000064400000000000000000000131641046102023000202040ustar 00000000000000// Copyright 2017 Google Inc. // // 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. use std::error::Error; use std::fs::FileType; use walkdir::DirEntry; #[cfg(unix)] use std::os::unix::fs::FileTypeExt; use super::{Matcher, MatcherIO}; /// This matcher checks the type of the file. pub struct TypeMatcher { file_type_fn: fn(&FileType) -> bool, } impl TypeMatcher { pub fn new(type_string: &str) -> Result> { #[cfg(unix)] let function = match type_string { "f" => FileType::is_file, "d" => FileType::is_dir, "l" => FileType::is_symlink, "b" => FileType::is_block_device, "c" => FileType::is_char_device, "p" => FileType::is_fifo, // named pipe (FIFO) "s" => FileType::is_socket, // D: door (Solaris) "D" => { return Err(From::from(format!( "Type argument {} not supported yet", type_string ))) } _ => { return Err(From::from(format!( "Unrecognised type argument {}", type_string ))) } }; #[cfg(not(unix))] let function = match type_string { "f" => FileType::is_file, "d" => FileType::is_dir, "l" => FileType::is_symlink, _ => { return Err(From::from(format!( "Unrecognised type argument {}", type_string ))) } }; Ok(Self { file_type_fn: function, }) } } impl Matcher for TypeMatcher { fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { (self.file_type_fn)(&file_info.file_type()) } } #[cfg(test)] mod tests { use super::*; use crate::find::matchers::tests::get_dir_entry_for; use crate::find::matchers::Matcher; use crate::find::tests::FakeDependencies; use std::io::ErrorKind; #[cfg(unix)] use std::os::unix::fs::symlink; #[cfg(windows)] use std::os::windows::fs::{symlink_dir, symlink_file}; #[test] fn file_type_matcher() { let file = get_dir_entry_for("test_data/simple", "abbbc"); let dir = get_dir_entry_for("test_data", "simple"); let deps = FakeDependencies::new(); let matcher = TypeMatcher::new("f").unwrap(); assert!(!matcher.matches(&dir, &mut deps.new_matcher_io())); assert!(matcher.matches(&file, &mut deps.new_matcher_io())); } #[test] fn dir_type_matcher() { let file = get_dir_entry_for("test_data/simple", "abbbc"); let dir = get_dir_entry_for("test_data", "simple"); let deps = FakeDependencies::new(); let matcher = TypeMatcher::new("d").unwrap(); assert!(matcher.matches(&dir, &mut deps.new_matcher_io())); assert!(!matcher.matches(&file, &mut deps.new_matcher_io())); } // git does not translate links (in test_data) to Windows links // so we have to create links in test #[test] fn link_type_matcher() { #[cfg(unix)] { if let Err(e) = symlink("abbbc", "test_data/links/link-f") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } if let Err(e) = symlink("subdir", "test_data/links/link-d") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } }; #[cfg(windows)] let _ = { if let Err(e) = symlink_file("abbbc", "test_data/links/link-f") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } if let Err(e) = symlink_dir("subdir", "test_data/links/link-d") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } }; let link_f = get_dir_entry_for("test_data/links", "link-f"); let link_d = get_dir_entry_for("test_data/links", "link-d"); let file = get_dir_entry_for("test_data/links", "abbbc"); let dir = get_dir_entry_for("test_data", "links"); let deps = FakeDependencies::new(); let matcher = TypeMatcher::new("l").unwrap(); assert!(!matcher.matches(&dir, &mut deps.new_matcher_io())); assert!(!matcher.matches(&file, &mut deps.new_matcher_io())); assert!(matcher.matches(&link_f, &mut deps.new_matcher_io())); assert!(matcher.matches(&link_d, &mut deps.new_matcher_io())); } #[cfg(unix)] #[test] fn unix_extra_type_matcher() { let file = get_dir_entry_for("test_data/simple", "abbbc"); let dir = get_dir_entry_for("test_data", "simple"); let deps = FakeDependencies::new(); for typ in &["b", "c", "p", "s"] { let matcher = TypeMatcher::new(typ.as_ref()).unwrap(); assert!(!matcher.matches(&dir, &mut deps.new_matcher_io())); assert!(!matcher.matches(&file, &mut deps.new_matcher_io())); } } #[test] fn cant_create_with_invalid_pattern() { let result = TypeMatcher::new("xxx"); assert!(result.is_err()); } } findutils-0.4.2/src/find/mod.rs000064400000000000000000000537101046102023000144720ustar 00000000000000// Copyright 2017 Google Inc. // // 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. pub mod matchers; use std::cell::RefCell; use std::error::Error; use std::io::{stderr, stdout, Write}; use std::rc::Rc; use std::time::SystemTime; use walkdir::WalkDir; pub struct Config { same_file_system: bool, depth_first: bool, min_depth: usize, max_depth: usize, sorted_output: bool, help_requested: bool, version_requested: bool, } impl Default for Config { fn default() -> Self { Self { same_file_system: false, depth_first: false, min_depth: 0, max_depth: usize::max_value(), sorted_output: false, help_requested: false, version_requested: false, } } } /// Trait that encapsulates various dependencies (output, clocks, etc.) that we /// might want to fake out for unit tests. pub trait Dependencies<'a> { fn get_output(&'a self) -> &'a RefCell; fn now(&'a self) -> SystemTime; } /// Struct that holds the dependencies we use when run as the real executable. pub struct StandardDependencies { output: Rc>, now: SystemTime, } impl StandardDependencies { pub fn new() -> Self { Self { output: Rc::new(RefCell::new(stdout())), now: SystemTime::now(), } } } impl Default for StandardDependencies { fn default() -> Self { Self::new() } } impl<'a> Dependencies<'a> for StandardDependencies { fn get_output(&'a self) -> &'a RefCell { self.output.as_ref() } fn now(&'a self) -> SystemTime { self.now } } /// The result of parsing the command-line arguments into useful forms. struct ParsedInfo { matcher: Box, paths: Vec, config: Config, } /// Function to generate a `ParsedInfo` from the strings supplied on the command-line. fn parse_args(args: &[&str]) -> Result> { let mut paths = vec![]; let mut i = 0; let mut config = Config::default(); while i < args.len() { match args[i] { "-O0" | "-O1" | "-O2" | "-O3" => { // GNU find optimization level flag (ignored) } "-P" => { // Never follow symlinks (the default) } "--" => { // End of flags i += 1; break; } _ => break, } i += 1; } let paths_start = i; while i < args.len() && (args[i] == "-" || !args[i].starts_with('-')) && args[i] != "!" && args[i] != "(" { paths.push(args[i].to_string()); i += 1; } if i == paths_start { paths.push(".".to_string()); } let matcher = matchers::build_top_level_matcher(&args[i..], &mut config)?; Ok(ParsedInfo { matcher, paths, config, }) } fn process_dir<'a>( dir: &str, config: &Config, deps: &'a dyn Dependencies<'a>, matcher: &dyn matchers::Matcher, quit: &mut bool, ) -> u64 { let mut found_count: u64 = 0; let mut walkdir = WalkDir::new(dir) .contents_first(config.depth_first) .max_depth(config.max_depth) .min_depth(config.min_depth) .same_file_system(config.same_file_system); if config.sorted_output { walkdir = walkdir.sort_by(|a, b| a.file_name().cmp(b.file_name())); } // Slightly yucky loop handling here :-(. See docs for // WalkDirIterator::skip_current_dir for explanation. let mut it = walkdir.into_iter(); while let Some(result) = it.next() { match result { Err(err) => writeln!(&mut stderr(), "Error: {}: {}", dir, err).unwrap(), Ok(entry) => { let mut matcher_io = matchers::MatcherIO::new(deps); if matcher.matches(&entry, &mut matcher_io) { found_count += 1; } if matcher_io.should_quit() { *quit = true; break; } if matcher_io.should_skip_current_dir() { it.skip_current_dir(); } } } } found_count } fn do_find<'a>(args: &[&str], deps: &'a dyn Dependencies<'a>) -> Result> { let paths_and_matcher = parse_args(args)?; if paths_and_matcher.config.help_requested { print_help(); return Ok(0); } if paths_and_matcher.config.version_requested { print_version(); return Ok(0); } let mut found_count: u64 = 0; let mut quit = false; for path in paths_and_matcher.paths { found_count += process_dir( &path, &paths_and_matcher.config, deps, &*paths_and_matcher.matcher, &mut quit, ); if quit { break; } } Ok(found_count) } fn print_help() { println!( r"Usage: find [path...] [expression] If no path is supplied then the current working directory is used by default. Early alpha implementation. Currently the only expressions supported are -print -print0 -printf -name case-sensitive_filename_pattern -lname case-sensitive_filename_pattern -iname case-insensitive_filename_pattern -ilname case-insensitive_filename_pattern -regextype type -regex pattern -iregex pattern -type type_char currently type_char can only be f (for file) or d (for directory) -size [+-]N[bcwkMG] -delete -prune -not -a -o[r] , () -true -false -maxdepth N -mindepth N -d[epth] -xdev -ctime [+-]N -atime [+-]N -mtime [+-]N -perm [-/]{{octal|u=rwx,go=w}} -newer path_to_file -exec[dir] executable [args] [{{}}] [more args] ; -sorted a non-standard extension that sorts directory contents by name before processing them. Less efficient, but allows for deterministic output. " ); } fn print_version() { println!("find (Rust) {}", env!("CARGO_PKG_VERSION")); } /// Does all the work for find. /// /// All main has to do is pass in the command-line args and exit the process /// with the exit code. Note that the first string in args is expected to be /// the name of the executable. pub fn find_main<'a>(args: &[&str], deps: &'a dyn Dependencies<'a>) -> i32 { match do_find(&args[1..], deps) { Ok(_) => 0, Err(e) => { writeln!(&mut stderr(), "Error: {}", e).unwrap(); 1 } } } #[cfg(test)] mod tests { use std::cell::RefCell; use std::fs; use std::io::{Cursor, ErrorKind, Read, Write}; use std::time::{Duration, SystemTime}; use std::vec::Vec; use tempfile::Builder; #[cfg(unix)] use std::os::unix::fs::symlink; #[cfg(windows)] use std::os::windows::fs::{symlink_dir, symlink_file}; use crate::find::matchers::MatcherIO; use super::*; #[cfg(windows)] /// Windows-only bodge for converting between path separators. pub fn fix_up_slashes(path: &str) -> String { path.replace("/", "\\") } #[cfg(not(windows))] /// Do nothing equivalent of the above for non-windows systems. pub fn fix_up_slashes(path: &str) -> String { path.to_string() } /// A struct that implements Dependencies, but uses faked implementations, /// allowing us to check output, set the time returned by clocks etc. pub struct FakeDependencies { pub output: RefCell>>, now: SystemTime, } impl<'a> FakeDependencies { pub fn new() -> Self { Self { output: RefCell::new(Cursor::new(Vec::::new())), now: SystemTime::now(), } } pub fn set_time(&mut self, new_time: SystemTime) { self.now = new_time; } pub fn new_matcher_io(&'a self) -> MatcherIO<'a> { MatcherIO::new(self) } pub fn get_output_as_string(&self) -> String { let mut cursor = self.output.borrow_mut(); cursor.set_position(0); let mut contents = String::new(); cursor.read_to_string(&mut contents).unwrap(); contents } } impl<'a> Dependencies<'a> for FakeDependencies { fn get_output(&'a self) -> &'a RefCell { &self.output } fn now(&'a self) -> SystemTime { self.now } } fn create_file_link() { #[cfg(unix)] if let Err(e) = symlink("abbbc", "test_data/links/link-f") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } #[cfg(windows)] if let Err(e) = symlink_file("abbbc", "test_data/links/link-f") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } } #[test] fn parse_args_handles_single_dash() { // Apparently "-" should be treated as a directory name. let parsed_info = super::parse_args(&["-"]).expect("parsing should succeed"); assert_eq!(parsed_info.paths, ["-"]); } #[test] fn parse_args_bad_flag() { // let result = super::parse_args(&["-asdadsafsfsadcs"]); if let Err(e) = result { assert_eq!(e.to_string(), "Unrecognized flag: '-asdadsafsfsadcs'"); } else { panic!("parse_args should have returned an error"); } } #[test] fn parse_optimize_flag() { let parsed_info = super::parse_args(&["-O0", ".", "-print"]).expect("parsing should succeed"); assert_eq!(parsed_info.paths, ["."]); } #[test] fn parse_p_flag() { super::parse_args(&["-P"]).expect("parsing should succeed"); } #[test] fn parse_flag_then_double_dash() { super::parse_args(&["-P", "--"]).expect("parsing should succeed"); } #[test] fn parse_double_dash_then_flag() { super::parse_args(&["--", "-P"]) .err() .expect("parsing should fail"); } #[test] fn find_main_not_depth_first() { let deps = FakeDependencies::new(); let rc = find_main( &["find", &fix_up_slashes("./test_data/simple"), "-sorted"], &deps, ); assert_eq!(rc, 0); assert_eq!( deps.get_output_as_string(), fix_up_slashes( "./test_data/simple\n\ ./test_data/simple/abbbc\n\ ./test_data/simple/subdir\n\ ./test_data/simple/subdir/ABBBC\n" ) ); } #[test] fn find_main_depth_first() { let deps = FakeDependencies::new(); let rc = find_main( &[ "find", &fix_up_slashes("./test_data/simple"), "-sorted", "-depth", ], &deps, ); assert_eq!(rc, 0); assert_eq!( deps.get_output_as_string(), fix_up_slashes( "./test_data/simple/abbbc\n\ ./test_data/simple/subdir/ABBBC\n\ ./test_data/simple/subdir\n\ ./test_data/simple\n" ) ); } #[test] fn find_maxdepth() { let deps = FakeDependencies::new(); let rc = find_main( &[ "find", &fix_up_slashes("./test_data/depth"), "-sorted", "-maxdepth", "2", ], &deps, ); assert_eq!(rc, 0); assert_eq!( deps.get_output_as_string(), fix_up_slashes( "./test_data/depth\n\ ./test_data/depth/1\n\ ./test_data/depth/1/2\n\ ./test_data/depth/1/f1\n\ ./test_data/depth/f0\n" ) ); } #[test] fn find_maxdepth_depth_first() { let deps = FakeDependencies::new(); let rc = find_main( &[ "find", &fix_up_slashes("./test_data/depth"), "-sorted", "-maxdepth", "2", "-depth", ], &deps, ); assert_eq!(rc, 0); assert_eq!( deps.get_output_as_string(), fix_up_slashes( "./test_data/depth/1/2\n\ ./test_data/depth/1/f1\n\ ./test_data/depth/1\n\ ./test_data/depth/f0\n\ ./test_data/depth\n" ) ); } #[test] fn find_prune() { let deps = FakeDependencies::new(); let rc = find_main( &[ "find", &fix_up_slashes("./test_data/depth"), "-sorted", "-print", ",", "-name", "1", "-prune", ], &deps, ); assert_eq!(rc, 0); assert_eq!( deps.get_output_as_string(), fix_up_slashes( "./test_data/depth\n\ ./test_data/depth/1\n\ ./test_data/depth/f0\n" ) ); } #[test] fn find_zero_maxdepth() { let deps = FakeDependencies::new(); let rc = find_main( &[ "find", &fix_up_slashes("./test_data/depth"), "-maxdepth", "0", ], &deps, ); assert_eq!(rc, 0); assert_eq!( deps.get_output_as_string(), fix_up_slashes("./test_data/depth\n") ); } #[test] fn find_zero_maxdepth_depth_first() { let deps = FakeDependencies::new(); let rc = find_main( &[ "find", &fix_up_slashes("./test_data/depth"), "-maxdepth", "0", "-depth", ], &deps, ); assert_eq!(rc, 0); assert_eq!( deps.get_output_as_string(), fix_up_slashes("./test_data/depth\n") ); } #[test] fn find_mindepth() { let deps = FakeDependencies::new(); let rc = find_main( &[ "find", &fix_up_slashes("./test_data/depth"), "-sorted", "-mindepth", "3", ], &deps, ); assert_eq!(rc, 0); assert_eq!( deps.get_output_as_string(), fix_up_slashes( "./test_data/depth/1/2/3\n\ ./test_data/depth/1/2/3/f3\n\ ./test_data/depth/1/2/f2\n" ) ); } #[test] fn find_mindepth_depth_first() { let deps = FakeDependencies::new(); let rc = find_main( &[ "find", &fix_up_slashes("./test_data/depth"), "-sorted", "-mindepth", "3", "-depth", ], &deps, ); assert_eq!(rc, 0); assert_eq!( deps.get_output_as_string(), fix_up_slashes( "./test_data/depth/1/2/3/f3\n\ ./test_data/depth/1/2/3\n\ ./test_data/depth/1/2/f2\n" ) ); } #[test] fn find_newer() { // create a temp directory and file that are newer than the static // files in the source tree. let new_dir = Builder::new().prefix("find_newer").tempdir().unwrap(); let deps = FakeDependencies::new(); let rc = find_main( &[ "find", &new_dir.path().to_string_lossy(), "-newer", &fix_up_slashes("./test_data/simple/abbbc"), ], &deps, ); assert_eq!(rc, 0); assert_eq!( deps.get_output_as_string(), new_dir.path().to_string_lossy().to_string() + "\n" ); // now do it the other way around, and nothing should be output let deps = FakeDependencies::new(); let rc = find_main( &[ "find", &fix_up_slashes("./test_data/simple/abbbc"), "-newer", &new_dir.path().to_string_lossy(), ], &deps, ); assert_eq!(rc, 0); assert_eq!(deps.get_output_as_string(), ""); } #[test] fn find_mtime() { let meta = fs::metadata("./test_data/simple/subdir/ABBBC").unwrap(); // metadata can return errors like StringError("creation time is not available on this platform currently") // so skip tests that won't pass due to shortcomings in std:;fs. if let Ok(file_time) = meta.modified() { file_time_helper(file_time, "-mtime"); } } #[test] fn find_ctime() { let meta = fs::metadata("./test_data/simple/subdir/ABBBC").unwrap(); // metadata can return errors like StringError("creation time is not available on this platform currently") // so skip tests that won't pass due to shortcomings in std:;fs. if let Ok(file_time) = meta.created() { file_time_helper(file_time, "-ctime"); } } #[test] fn find_atime() { let meta = fs::metadata("./test_data/simple/subdir/ABBBC").unwrap(); // metadata can return errors like StringError("creation time is not available on this platform currently") // so skip tests that won't pass due to shortcomings in std:;fs. if let Ok(file_time) = meta.accessed() { file_time_helper(file_time, "-atime"); } } /// Helper function for the find_ctime/find_atime/find_mtime tests. fn file_time_helper(file_time: SystemTime, arg: &str) { // check file time matches a file that's old enough { let mut deps = FakeDependencies::new(); deps.set_time(file_time); let rc = find_main( &[ "find", &fix_up_slashes("./test_data/simple/subdir"), "-type", "f", arg, "0", ], &deps, ); assert_eq!(rc, 0); assert_eq!( deps.get_output_as_string(), fix_up_slashes("./test_data/simple/subdir/ABBBC\n") ); } // now Check file time doesn't match a file that's too new { let mut deps = FakeDependencies::new(); deps.set_time(file_time - Duration::from_secs(1)); let rc = find_main( &["find", "./test_data/simple/subdir", "-type", "f", arg, "0"], &deps, ); assert_eq!(rc, 0); assert_eq!(deps.get_output_as_string(), ""); } } #[test] fn find_size() { let deps = FakeDependencies::new(); // only look at files because the "size" of a directory is a system (and filesystem) // dependent thing and we want these tests to be universal. let rc = find_main( &[ "find", &fix_up_slashes("./test_data/size"), "-type", "f", "-size", "1b", ], &deps, ); assert_eq!(rc, 0); assert_eq!( deps.get_output_as_string(), fix_up_slashes("./test_data/size/512bytes\n") ); let deps = FakeDependencies::new(); let rc = find_main( &["find", "./test_data/size", "-type", "f", "-size", "+1b"], &deps, ); assert_eq!(rc, 0); assert_eq!(deps.get_output_as_string(), ""); } #[test] fn find_name_links() { create_file_link(); let deps = FakeDependencies::new(); let rc = find_main( &[ "find", &fix_up_slashes("./test_data/links"), "-name", "abbbc", ], &deps, ); assert_eq!(rc, 0); assert_eq!( deps.get_output_as_string(), fix_up_slashes("./test_data/links/abbbc\n") ); } #[test] fn find_lname_links() { create_file_link(); let deps = FakeDependencies::new(); let rc = find_main( &[ "find", &fix_up_slashes("./test_data/links"), "-lname", "abbbc", "-sorted", ], &deps, ); assert_eq!(rc, 0); assert_eq!( deps.get_output_as_string(), fix_up_slashes("./test_data/links/link-f\n") ); } #[test] fn find_ilname_links() { create_file_link(); let deps = FakeDependencies::new(); let rc = find_main( &[ "find", &fix_up_slashes("./test_data/links"), "-ilname", "abBbc", ], &deps, ); assert_eq!(rc, 0); assert_eq!( deps.get_output_as_string(), fix_up_slashes("./test_data/links/link-f\n") ); } #[test] fn find_print_then_quit() { let deps = FakeDependencies::new(); let rc = find_main( &[ "find", &fix_up_slashes("./test_data/simple"), &fix_up_slashes("./test_data/simple"), "-print", "-quit", ], &deps, ); assert_eq!(rc, 0); assert_eq!( deps.get_output_as_string(), fix_up_slashes("./test_data/simple\n"), ); } } findutils-0.4.2/src/lib.rs000064400000000000000000000003241046102023000135320ustar 00000000000000// Copyright 2017 Google Inc. // // 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. pub mod find; pub mod xargs; findutils-0.4.2/src/testing/commandline/main.rs000064400000000000000000000071511046102023000176600ustar 00000000000000// Copyright 2017 Google Inc. // // 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. use std::env; use std::fs::{self, File, OpenOptions}; use std::io::{stdin, stdout, Read, Write}; use std::path::PathBuf; fn usage() -> ! { println!("Simple command-line app just used for testing -exec flags!"); std::process::exit(2); } enum ExitWith { Failure, UrgentFailure, #[cfg(unix)] Signal, } #[derive(Default)] struct Config { exit_with: Option, print_stdin: bool, no_print_cwd: bool, destination_dir: Option, } fn open_file(destination_dir: &str) -> File { let mut file_number = fs::read_dir(destination_dir) .expect("failed to read destination") .count(); loop { file_number += 1; let mut file_path: PathBuf = PathBuf::from(destination_dir); file_path.push(format!("{file_number}.txt")); if let Ok(f) = OpenOptions::new() .write(true) .create_new(true) .open(file_path) { return f; } } } fn write_content(mut f: impl Write, config: &Config, args: &[String]) { if !config.no_print_cwd { writeln!(f, "cwd={}", env::current_dir().unwrap().to_string_lossy()) .expect("failed to write to file"); } if config.print_stdin { let mut s = String::new(); stdin() .read_to_string(&mut s) .expect("failed to read from stdin"); writeln!(f, "stdin={}", s.trim()).expect("failed to write to file"); } writeln!(f, "args=").expect("failed to write to file"); // first two args are going to be the path to this executable and // the destination_dir we want to write to. Don't write either of those // as they'll be non-deterministic. for arg in &args[2..] { writeln!(f, "{arg}").expect("failed to write to file"); } } fn main() { let args = env::args().collect::>(); if args.len() < 2 || args[1] == "-h" || args[1] == "--help" { usage(); } let mut config = Config { destination_dir: if args[1] == "-" { None } else { Some(args[1].clone()) }, ..Default::default() }; for arg in &args[2..] { if arg.starts_with("--") { match arg.as_ref() { "--exit_with_failure" => { config.exit_with = Some(ExitWith::Failure); } "--exit_with_urgent_failure" => { config.exit_with = Some(ExitWith::UrgentFailure); } #[cfg(unix)] "--exit_with_signal" => { config.exit_with = Some(ExitWith::Signal); } "--no_print_cwd" => { config.no_print_cwd = true; } "--print_stdin" => { config.print_stdin = true; } _ => { usage(); } } } } if let Some(destination_dir) = &config.destination_dir { write_content(open_file(destination_dir), &config, &args); } else { write_content(stdout(), &config, &args); } match config.exit_with { None => std::process::exit(0), Some(ExitWith::Failure) => std::process::exit(2), Some(ExitWith::UrgentFailure) => std::process::exit(255), #[cfg(unix)] Some(ExitWith::Signal) => unsafe { uucore::libc::raise(uucore::libc::SIGINT); }, } } findutils-0.4.2/src/xargs/main.rs000064400000000000000000000006641046102023000150430ustar 00000000000000// Copyright 2021 Collabora, Ltd. // // 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. fn main() { let args = std::env::args().collect::>(); std::process::exit(findutils::xargs::xargs_main( &args .iter() .map(std::convert::AsRef::as_ref) .collect::>(), )) } findutils-0.4.2/src/xargs/mod.rs000064400000000000000000001101651046102023000146740ustar 00000000000000// Copyright 2021 Collabora, Ltd. // // 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. use std::{ collections::HashMap, error::Error, ffi::{OsStr, OsString}, fmt::Display, fs, io::{self, BufRead, BufReader, Read}, process::{Command, Stdio}, }; use clap::{crate_version, App, AppSettings, Arg}; mod options { pub const COMMAND: &str = "COMMAND"; pub const ARG_FILE: &str = "arg-file"; pub const DELIMITER: &str = "delimiter"; pub const EXIT: &str = "exit"; pub const MAX_ARGS: &str = "max-args"; pub const MAX_LINES: &str = "max-lines"; pub const MAX_PROCS: &str = "max-procs"; pub const NO_RUN_IF_EMPTY: &str = "no-run-if-empty"; pub const NULL: &str = "null"; pub const SIZE: &str = "size"; pub const VERBOSE: &str = "verbose"; } struct Options { arg_file: Option, delimiter: Option, exit_if_pass_char_limit: bool, max_args: Option, max_lines: Option, no_run_if_empty: bool, null: bool, size: Option, verbose: bool, } #[derive(Debug, PartialEq, Eq)] enum ArgumentKind { /// An argument provided as part of the initial command line. Initial, /// An argument that was terminated by a newline or custom delimiter. HardTerminated, /// An argument that was terminated by non-newline whitespace. SoftTerminated, } #[derive(Debug, PartialEq, Eq)] struct Argument { arg: OsString, kind: ArgumentKind, } struct ExhaustedCommandSpace { arg: Argument, out_of_chars: bool, } /// A "limiter" to constrain the size of a single command line. Given a cursor /// pointing to the next limiter that should be tried. trait CommandSizeLimiter { fn try_arg( &mut self, arg: Argument, cursor: LimiterCursor<'_>, ) -> Result; fn dyn_clone(&self) -> Box; } /// A pointer to the next limiter. A limiter should *always* call the cursor's /// try_next *before* updating its own state, to ensure that all other limiters /// are okay with the argument first. struct LimiterCursor<'collection> { limiters: &'collection mut [Box], } impl LimiterCursor<'_> { fn try_next(self, arg: Argument) -> Result { if self.limiters.is_empty() { Ok(arg) } else { let (current, remaining) = self.limiters.split_at_mut(1); current[0].try_arg( arg, LimiterCursor { limiters: remaining, }, ) } } } struct LimiterCollection { limiters: Vec>, } impl LimiterCollection { fn new() -> Self { Self { limiters: vec![] } } fn add(&mut self, limiter: impl CommandSizeLimiter + 'static) { self.limiters.push(Box::new(limiter)); } fn try_arg(&mut self, arg: Argument) -> Result { let cursor = LimiterCursor { limiters: &mut self.limiters[..], }; cursor.try_next(arg) } } impl Clone for LimiterCollection { fn clone(&self) -> Self { Self { limiters: self .limiters .iter() .map(|limiter| limiter.dyn_clone()) .collect(), } } } #[cfg(windows)] fn count_osstr_chars_for_exec(s: &OsStr) -> usize { use std::os::windows::ffi::OsStrExt; // Include +1 for either the null terminator or trailing space. s.encode_wide().count() + 1 } #[cfg(unix)] fn count_osstr_chars_for_exec(s: &OsStr) -> usize { use std::os::unix::ffi::OsStrExt; // Include +1 for the null terminator. s.as_bytes().len() + 1 } #[derive(Clone)] struct MaxCharsCommandSizeLimiter { current_size: usize, max_chars: usize, } impl MaxCharsCommandSizeLimiter { fn new(max_chars: usize) -> Self { Self { current_size: 0, max_chars, } } #[cfg(windows)] fn new_system(_env: &HashMap) -> MaxCharsCommandSizeLimiter { // Taken from the CreateProcess docs. const MAX_CMDLINE: usize = 32767; MaxCharsCommandSizeLimiter::new(MAX_CMDLINE) } #[cfg(unix)] fn new_system(env: &HashMap) -> Self { // POSIX requires that we leave 2048 bytes of space so that the child processes // can have room to set their own environment variables. const ARG_HEADROOM: usize = 2048; let arg_max = unsafe { uucore::libc::sysconf(uucore::libc::_SC_ARG_MAX) } as usize; let env_size: usize = env .iter() .map(|(var, value)| count_osstr_chars_for_exec(var) + count_osstr_chars_for_exec(value)) .sum(); Self::new(arg_max - ARG_HEADROOM - env_size) } } impl CommandSizeLimiter for MaxCharsCommandSizeLimiter { fn try_arg( &mut self, arg: Argument, cursor: LimiterCursor<'_>, ) -> Result { let chars = count_osstr_chars_for_exec(&arg.arg); if self.current_size + chars <= self.max_chars { let arg = cursor.try_next(arg)?; self.current_size += chars; Ok(arg) } else { Err(ExhaustedCommandSpace { arg, out_of_chars: true, }) } } fn dyn_clone(&self) -> Box { Box::new(self.clone()) } } #[derive(Clone)] struct MaxArgsCommandSizeLimiter { current_args: usize, max_args: usize, } impl MaxArgsCommandSizeLimiter { fn new(max_args: usize) -> Self { Self { current_args: 0, max_args, } } } impl CommandSizeLimiter for MaxArgsCommandSizeLimiter { fn try_arg( &mut self, arg: Argument, cursor: LimiterCursor<'_>, ) -> Result { if self.current_args < self.max_args { let arg = cursor.try_next(arg)?; if arg.kind != ArgumentKind::Initial { self.current_args += 1; } Ok(arg) } else { Err(ExhaustedCommandSpace { arg, out_of_chars: false, }) } } fn dyn_clone(&self) -> Box { Box::new(self.clone()) } } #[derive(Clone)] struct MaxLinesCommandSizeLimiter { current_line: usize, max_lines: usize, } impl MaxLinesCommandSizeLimiter { fn new(max_lines: usize) -> Self { Self { current_line: 1, max_lines, } } } impl CommandSizeLimiter for MaxLinesCommandSizeLimiter { fn try_arg( &mut self, arg: Argument, cursor: LimiterCursor<'_>, ) -> Result { if self.current_line <= self.max_lines { let arg = cursor.try_next(arg)?; // The name of this limiter is a bit of a lie: although this limits // by max "lines", if a custom delimiter is used, xargs uses that // instead. So, this actually limits based on the max amount of hard // terminations. if arg.kind == ArgumentKind::HardTerminated { self.current_line += 1; } Ok(arg) } else { Err(ExhaustedCommandSpace { arg, out_of_chars: false, }) } } fn dyn_clone(&self) -> Box { Box::new(self.clone()) } } enum CommandResult { Success, Failure, } impl CommandResult { fn combine(&mut self, other: Self) { if matches!(*self, CommandResult::Success) { *self = other; } } } #[derive(Debug)] enum CommandExecutionError { // exit code 255 UrgentlyFailed, Killed { signal: i32 }, CannotRun(io::Error), NotFound, Unknown, } impl Display for CommandExecutionError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { CommandExecutionError::UrgentlyFailed => write!(f, "Command exited with code 255"), CommandExecutionError::Killed { signal } => { write!(f, "Command was killed with signal {}", signal) } CommandExecutionError::CannotRun(err) => write!(f, "Command could not be run: {}", err), CommandExecutionError::NotFound => write!(f, "Command not found"), CommandExecutionError::Unknown => write!(f, "Unknown error running command"), } } } impl Error for CommandExecutionError {} enum ExecAction { Command(Vec), Echo, } struct CommandBuilderOptions { action: ExecAction, env: HashMap, limiters: LimiterCollection, verbose: bool, close_stdin: bool, } impl CommandBuilderOptions { fn new( action: ExecAction, env: HashMap, mut limiters: LimiterCollection, ) -> Result { let initial_args = match &action { ExecAction::Command(args) => args.iter().map(|arg| arg.as_ref()).collect(), ExecAction::Echo => vec![OsStr::new("echo")], }; for arg in initial_args { limiters.try_arg(Argument { arg: arg.to_owned(), kind: ArgumentKind::Initial, })?; } Ok(Self { action, env, limiters, verbose: false, close_stdin: false, }) } } struct CommandBuilder<'options> { options: &'options CommandBuilderOptions, extra_args: Vec, limiters: LimiterCollection, } impl CommandBuilder<'_> { fn new(options: &CommandBuilderOptions) -> CommandBuilder<'_> { CommandBuilder { options, extra_args: vec![], limiters: options.limiters.clone(), } } fn add_arg(&mut self, arg: Argument) -> Result<(), ExhaustedCommandSpace> { let arg = self.limiters.try_arg(arg)?; self.extra_args.push(arg.arg); Ok(()) } fn execute(self) -> Result { let (entry_point, initial_args): (&OsStr, &[OsString]) = match &self.options.action { ExecAction::Command(args) => (&args[0], &args[1..]), ExecAction::Echo => (OsStr::new("echo"), &[]), }; let mut command = Command::new(entry_point); command .args(initial_args) .args(&self.extra_args) .env_clear() .envs(&self.options.env); if self.options.close_stdin { command.stdin(Stdio::null()); } if self.options.verbose { eprintln!("{:?}", command); } match &self.options.action { ExecAction::Command(_) => match command.status() { Ok(status) => { if status.success() { Ok(CommandResult::Success) } else if let Some(err) = status.code() { if err == 255 { Err(CommandExecutionError::UrgentlyFailed) } else { Ok(CommandResult::Failure) } } else { #[cfg(unix)] { use std::os::unix::process::ExitStatusExt; if let Some(signal) = status.signal() { Err(CommandExecutionError::Killed { signal }) } else { Err(CommandExecutionError::Unknown) } } #[cfg(not(unix))] Err(CommandExecutionError::Unknown) } } Err(e) if e.kind() == io::ErrorKind::NotFound => { Err(CommandExecutionError::NotFound) } Err(e) => Err(CommandExecutionError::CannotRun(e)), }, ExecAction::Echo => { println!( "{}", self.extra_args .iter() .map(|arg| arg.to_string_lossy()) .collect::>() .join(" ") ); Ok(CommandResult::Success) } } } } trait ArgumentReader { fn next(&mut self) -> io::Result>; } struct WhitespaceDelimitedArgumentReader { rd: R, pending: Vec, } impl WhitespaceDelimitedArgumentReader where R: Read, { fn new(rd: R) -> Self { Self { rd, pending: vec![], } } } impl ArgumentReader for WhitespaceDelimitedArgumentReader where R: Read, { fn next(&mut self) -> io::Result> { let mut result = vec![]; let mut terminated_by_newline = false; let mut pending = vec![]; std::mem::swap(&mut pending, &mut self.pending); enum Escape { Slash, Quote(u8), } let mut escape: Option = None; let mut i = 0; loop { if i == pending.len() { pending.resize(4096, 0); // Already hit the end of our buffer, so read in some more data. let bytes_read = loop { match self.rd.read(&mut pending[..]) { Ok(bytes_read) => break bytes_read, Err(e) if e.kind() == io::ErrorKind::Interrupted => continue, Err(e) => return Err(e), } }; if bytes_read == 0 { if let Some(Escape::Quote(q)) = &escape { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("Unterminated quote: {}", q), )); } else if i == 0 { return Ok(None); } else { pending.clear(); break; } } pending.resize(bytes_read, 0); i = 0; } match (&escape, pending[i]) { (Some(Escape::Quote(quote)), c) if c == *quote => escape = None, (Some(Escape::Quote(_)), c) => result.push(c), (Some(Escape::Slash), c) => { result.push(c); escape = None; } (None, c @ (b'"' | b'\'')) => escape = Some(Escape::Quote(c)), (None, b'\\') => escape = Some(Escape::Slash), (None, c) if c.is_ascii_whitespace() => { if !result.is_empty() { terminated_by_newline = c == b'\n'; break; } } (None, c) => result.push(c), } i += 1; } if i < pending.len() { self.pending = pending.split_off(i + 1); } Ok(Some(Argument { arg: String::from_utf8_lossy(&result[..]).into_owned().into(), kind: if terminated_by_newline { ArgumentKind::HardTerminated } else { ArgumentKind::SoftTerminated }, })) } } struct ByteDelimitedArgumentReader { rd: BufReader, delimiter: u8, } impl ByteDelimitedArgumentReader where R: Read, { fn new(rd: R, delimiter: u8) -> Self { Self { rd: BufReader::new(rd), delimiter, } } } impl ArgumentReader for ByteDelimitedArgumentReader where R: Read, { fn next(&mut self) -> io::Result> { Ok(loop { let mut buf = vec![]; let bytes_read = self.rd.read_until(self.delimiter, &mut buf)?; if bytes_read > 0 { let need_to_trim_delimiter = buf[buf.len() - 1] == self.delimiter; let bytes = if need_to_trim_delimiter { if buf.len() == 1 { // This was *only* a delimiter, so we didn't actually // read anything interesting. Try again. continue; } &buf[..buf.len() - 1] } else { &buf[..] }; break Some(Argument { arg: String::from_utf8_lossy(bytes).into_owned().into(), kind: ArgumentKind::HardTerminated, }); } else { break None; } }) } } #[derive(Debug)] enum XargsError { ArgumentTooLarge, CommandExecution(CommandExecutionError), Io(io::Error), Untyped(String), } impl Display for XargsError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { XargsError::ArgumentTooLarge => write!(f, "Argument too large"), XargsError::CommandExecution(e) => write!(f, "{}", e), XargsError::Io(e) => write!(f, "{}", e), XargsError::Untyped(s) => write!(f, "{}", s), } } } impl Error for XargsError {} impl From for XargsError { fn from(s: String) -> Self { Self::Untyped(s) } } impl From<&'_ str> for XargsError { fn from(s: &'_ str) -> Self { s.to_owned().into() } } impl From for XargsError { fn from(e: CommandExecutionError) -> Self { Self::CommandExecution(e) } } impl From for XargsError { fn from(e: io::Error) -> Self { Self::Io(e) } } fn process_input( builder_options: CommandBuilderOptions, mut args: Box, options: &Options, ) -> Result { let mut current_builder = CommandBuilder::new(&builder_options); let mut have_pending_command = false; let mut result = CommandResult::Success; while let Some(arg) = args.next()? { if let Err(ExhaustedCommandSpace { arg, out_of_chars }) = current_builder.add_arg(arg) { if out_of_chars && options.exit_if_pass_char_limit && (options.max_args.is_some() || options.max_lines.is_some()) { return Err(XargsError::ArgumentTooLarge); } else if have_pending_command { result.combine(current_builder.execute()?); } current_builder = CommandBuilder::new(&builder_options); if let Err(ExhaustedCommandSpace { .. }) = current_builder.add_arg(arg) { return Err(XargsError::ArgumentTooLarge); } } have_pending_command = true; } if !options.no_run_if_empty || have_pending_command { result.combine(current_builder.execute()?); } Ok(result) } fn parse_delimiter(s: &str) -> Result { if let Some(hex) = s.strip_prefix("\\x") { u8::from_str_radix(hex, 16).map_err(|e| format!("Invalid hex sequence: {}", e)) } else if let Some(oct) = s.strip_prefix("\\0") { u8::from_str_radix(oct, 8).map_err(|e| format!("Invalid octal sequence: {}", e)) } else if let Some(special) = s.strip_prefix('\\') { match special { "a" => Ok(b'\x07'), "b" => Ok(b'\x08'), "f" => Ok(b'\x0C'), "n" => Ok(b'\n'), "r" => Ok(b'\r'), "t" => Ok(b'\t'), "v" => Ok(b'\x0B'), "0" => Ok(b'\0'), "\\" => Ok(b'\\'), _ => Err(format!("Invalid escape sequence: {}", s)), } } else { let bytes = s.as_bytes(); if bytes.len() == 1 { Ok(bytes[0]) } else { Err("Delimiter must be one byte".to_owned()) } } } fn validate_positive_usize(s: String) -> Result<(), String> { match s.parse::() { Ok(v) if v > 0 => Ok(()), Ok(v) => Err(format!("Value must be > 0, not: {}", v)), Err(e) => Err(e.to_string()), } } fn do_xargs(args: &[&str]) -> Result { let matches = App::new("xargs") .version(crate_version!()) .about("Run commands using arguments derived from standard input") .settings(&[AppSettings::TrailingVarArg]) .arg( Arg::with_name(options::COMMAND) .takes_value(true) .multiple(true) .help("The command to run"), ) .arg( Arg::with_name(options::ARG_FILE) .short("a") .long(options::ARG_FILE) .takes_value(true) .help("Read arguments from the given file instead of stdin"), ) .arg( Arg::with_name(options::DELIMITER) .short("d") .long(options::DELIMITER) .takes_value(true) .validator(|s| parse_delimiter(&s).map(|_| ())) .help("Use the given delimiter to split the input"), ) .arg( Arg::with_name(options::EXIT) .short("x") .long(options::EXIT) .help( "Exit if the number of arguments allowed by -L or -n do not \ fit into the number of allowed characters", ), ) .arg( Arg::with_name(options::MAX_ARGS) .short("n") .takes_value(true) .long(options::MAX_ARGS) .validator(validate_positive_usize) .help( "Set the max number of arguments read from stdin to be passed \ to each command invocation (mutually exclusive with -L)", ), ) .arg( Arg::with_name(options::MAX_LINES) .short("L") .takes_value(true) .validator(validate_positive_usize) .help( "Set the max number of lines from stdin to be passed to each \ command invocation (mutually exclusive with -n)", ), ) .arg( Arg::with_name(options::MAX_PROCS) .short("P") .takes_value(true) .long(options::MAX_PROCS) .help("Run up to this many commands in parallel [NOT IMPLEMENTED]"), ) .arg( Arg::with_name(options::NO_RUN_IF_EMPTY) .short("r") .long(options::NO_RUN_IF_EMPTY) .help("If there are no input arguments, do not run the command at all"), ) .arg( Arg::with_name(options::NULL) .short("0") .long(options::NULL) .help("Split the input by null terminators rather than whitespace"), ) .arg( Arg::with_name(options::SIZE) .short("s") .long(options::SIZE) .takes_value(true) .validator(validate_positive_usize) .help( "Set the max number of characters to be passed to each \ invocation", ), ) .arg( Arg::with_name(options::VERBOSE) .short("t") .long(options::VERBOSE) .help("Be verbose"), ) .get_matches_from(args); let options = Options { arg_file: matches .value_of(options::ARG_FILE) .map(|value| value.to_owned()), delimiter: matches .value_of(options::DELIMITER) .map(|value| parse_delimiter(value).unwrap()), exit_if_pass_char_limit: matches.is_present(options::EXIT), max_args: matches .value_of(options::MAX_ARGS) .map(|value| value.parse().unwrap()), max_lines: matches .value_of(options::MAX_LINES) .map(|value| value.parse().unwrap()), no_run_if_empty: matches.is_present(options::NO_RUN_IF_EMPTY), null: matches.is_present(options::NULL), size: matches .value_of(options::SIZE) .map(|value| value.parse().unwrap()), verbose: matches.is_present(options::VERBOSE), }; let delimiter = match (options.delimiter, options.null) { (Some(delimiter), true) => { if matches.indices_of(options::NULL).unwrap().last() > matches.indices_of(options::DELIMITER).unwrap().last() { Some(b'\0') } else { Some(delimiter) } } (Some(delimiter), false) => Some(delimiter), (None, true) => Some(b'\0'), (None, false) => None, }; let action = match matches.values_of_os(options::COMMAND) { Some(args) if args.len() > 0 => { ExecAction::Command(args.map(|arg| arg.to_owned()).collect()) } _ => ExecAction::Echo, }; let env = std::env::vars_os().collect(); let mut limiters = LimiterCollection::new(); match (options.max_args, options.max_lines) { (Some(max_args), Some(max_lines)) => { eprintln!( "WARNING: Both --{} and -L were given; last option will be used", options::MAX_ARGS, ); if matches.indices_of(options::MAX_LINES).unwrap().last() > matches.indices_of(options::MAX_ARGS).unwrap().last() { limiters.add(MaxLinesCommandSizeLimiter::new(max_lines)); } else { limiters.add(MaxArgsCommandSizeLimiter::new(max_args)); } } (Some(max_args), None) => limiters.add(MaxArgsCommandSizeLimiter::new(max_args)), (None, Some(max_lines)) => limiters.add(MaxLinesCommandSizeLimiter::new(max_lines)), (None, None) => (), } if let Some(max_chars) = options.size { limiters.add(MaxCharsCommandSizeLimiter::new(max_chars)); } limiters.add(MaxCharsCommandSizeLimiter::new_system(&env)); let mut builder_options = CommandBuilderOptions::new(action, env, limiters).map_err(|_| { "Base command and environment are too large to fit into one command execution" })?; builder_options.verbose = options.verbose; builder_options.close_stdin = options.arg_file.is_none(); let args_file: Box = if let Some(path) = &options.arg_file { Box::new(fs::File::open(path).map_err(|e| format!("Failed to open {}: {}", path, e))?) } else { Box::new(io::stdin()) }; let args: Box = if let Some(delimiter) = delimiter { Box::new(ByteDelimitedArgumentReader::new(args_file, delimiter)) } else { Box::new(WhitespaceDelimitedArgumentReader::new(args_file)) }; let result = process_input(builder_options, args, &options)?; Ok(result) } pub fn xargs_main(args: &[&str]) -> i32 { match do_xargs(args) { Ok(CommandResult::Success) => 0, Ok(CommandResult::Failure) => 123, Err(e) => { eprintln!("Error: {}", e); if let XargsError::CommandExecution(cx) = e { match cx { CommandExecutionError::UrgentlyFailed => 124, CommandExecutionError::Killed { .. } => 125, CommandExecutionError::CannotRun(_) => 126, CommandExecutionError::NotFound => 127, CommandExecutionError::Unknown => 1, } } else { 1 } } } } #[cfg(test)] mod tests { use super::*; fn make_arg_init(s: &str) -> Argument { Argument { arg: s.to_owned().into(), kind: ArgumentKind::Initial, } } fn make_arg_hard(s: &str) -> Argument { Argument { arg: s.to_owned().into(), kind: ArgumentKind::HardTerminated, } } fn make_arg_soft(s: &str) -> Argument { Argument { arg: s.to_owned().into(), kind: ArgumentKind::SoftTerminated, } } #[derive(Clone)] struct AlwaysRejectLimiter; impl CommandSizeLimiter for AlwaysRejectLimiter { fn try_arg( &mut self, arg: Argument, _cursor: LimiterCursor<'_>, ) -> Result { Err(ExhaustedCommandSpace { arg, out_of_chars: false, }) } fn dyn_clone(&self) -> Box { Box::new(self.clone()) } } fn empty_cursor() -> LimiterCursor<'static> { LimiterCursor { limiters: &mut [] } } enum Chunk { Data(&'static [u8]), Error(io::ErrorKind), } struct ChunkReader { chunks: Vec, current: usize, } impl ChunkReader { fn new(chunks: Vec) -> Self { Self { chunks, current: 0 } } } impl Read for ChunkReader { fn read(&mut self, buf: &mut [u8]) -> io::Result { if self.current >= self.chunks.len() { return Ok(0); } match &mut self.chunks[self.current] { Chunk::Data(data) => { let byte_count = std::cmp::min(data.len(), buf.len()); buf[..byte_count].copy_from_slice(&(*data)[..byte_count]); if byte_count == data.len() { self.current += 1; } else { *data = &(*data)[byte_count..]; } Ok(byte_count) } Chunk::Error(kind) => { self.current += 1; Err(io::Error::new(*kind, "Synthesized error")) } } } } #[test] fn test_chars_limiter() { let mut limiter = MaxCharsCommandSizeLimiter::new(6); assert!(limiter .try_arg(make_arg_hard("abc"), empty_cursor()) .is_ok()); assert!(limiter .try_arg(make_arg_hard("abcd"), empty_cursor()) .is_err()); assert!(limiter.try_arg(make_arg_hard("a"), empty_cursor()).is_ok()); } #[test] fn test_chars_limiter_asks_cursor() { let mut rejects: [Box; 1] = [Box::new(AlwaysRejectLimiter)]; let reject_cursor = LimiterCursor { limiters: &mut rejects, }; let mut limiter = MaxCharsCommandSizeLimiter::new(5); assert!(limiter .try_arg(make_arg_hard("abc"), reject_cursor) .is_err()); // Ensure the limiter didn't update before trying the cursor. assert!(limiter .try_arg(make_arg_hard("abc"), empty_cursor()) .is_ok()); } #[test] fn test_args_limiter() { let mut limiter = MaxArgsCommandSizeLimiter::new(2); // Should not count initial arguments. for _ in 1..3 { assert!(limiter .try_arg(make_arg_init("abc"), empty_cursor()) .is_ok()); } assert!(limiter .try_arg(make_arg_hard("abc"), empty_cursor()) .is_ok()); assert!(limiter .try_arg(make_arg_hard("abc"), empty_cursor()) .is_ok()); assert!(limiter .try_arg(make_arg_hard("abc"), empty_cursor()) .is_err()); } #[test] fn test_args_limiter_asks_cursor() { let mut rejects: [Box; 1] = [Box::new(AlwaysRejectLimiter)]; let reject_cursor = LimiterCursor { limiters: &mut rejects, }; let mut limiter = MaxArgsCommandSizeLimiter::new(1); assert!(limiter .try_arg(make_arg_hard("abc"), reject_cursor) .is_err()); // Ensure the limiter didn't update before trying the cursor. assert!(limiter .try_arg(make_arg_hard("abc"), empty_cursor()) .is_ok()); } #[test] fn test_lines_limiter() { let mut limiter = MaxLinesCommandSizeLimiter::new(2); assert!(limiter .try_arg(make_arg_soft("abc"), empty_cursor()) .is_ok()); assert!(limiter .try_arg(make_arg_soft("abc"), empty_cursor()) .is_ok()); assert!(limiter .try_arg(make_arg_soft("abc"), empty_cursor()) .is_ok()); assert!(limiter .try_arg(make_arg_hard("abc"), empty_cursor()) .is_ok()); assert!(limiter .try_arg(make_arg_soft("abc"), empty_cursor()) .is_ok()); assert!(limiter .try_arg(make_arg_hard("abc"), empty_cursor()) .is_ok()); assert!(limiter .try_arg(make_arg_soft("abc"), empty_cursor()) .is_err()); assert!(limiter .try_arg(make_arg_hard("abc"), empty_cursor()) .is_err()); } #[test] fn test_lines_limiter_asks_cursor() { let mut rejects: [Box; 1] = [Box::new(AlwaysRejectLimiter)]; let reject_cursor = LimiterCursor { limiters: &mut rejects, }; let mut limiter = MaxLinesCommandSizeLimiter::new(1); assert!(limiter .try_arg(make_arg_hard("abc"), reject_cursor) .is_err()); // Ensure the limiter didn't update before trying the cursor. assert!(limiter .try_arg(make_arg_hard("abc"), empty_cursor()) .is_ok()); } #[test] fn test_whitespace_delimited_reader() { let mut reader = WhitespaceDelimitedArgumentReader::new(ChunkReader::new(vec![ Chunk::Data(b"abc "), Chunk::Data(b" def"), Chunk::Data(b"\nghi\t\tj"), Chunk::Data(b"kl\n"), Chunk::Data(b"mn"), Chunk::Error(io::ErrorKind::Interrupted), Chunk::Data(b"\\\t\\ o 'ab"), Chunk::Data(b" \"' \"xy' z\""), ])); assert_eq!(reader.next().unwrap().unwrap(), make_arg_soft("abc")); assert_eq!(reader.next().unwrap().unwrap(), make_arg_hard("def")); assert_eq!(reader.next().unwrap().unwrap(), make_arg_soft("ghi")); assert_eq!(reader.next().unwrap().unwrap(), make_arg_hard("jkl")); assert_eq!(reader.next().unwrap().unwrap(), make_arg_soft("mn\t o")); assert_eq!(reader.next().unwrap().unwrap(), make_arg_soft("ab \"")); assert_eq!(reader.next().unwrap().unwrap(), make_arg_soft("xy' z")); assert_eq!(reader.next().unwrap(), None); } #[test] fn test_byte_delimited_reader() { let mut reader = ByteDelimitedArgumentReader::new( ChunkReader::new(vec![ Chunk::Data(b"ab"), Chunk::Error(io::ErrorKind::Interrupted), Chunk::Data(b"c!de!"), Chunk::Data(b"!ef!!gh"), Chunk::Data(b"!ij"), ]), b'!', ); assert_eq!(reader.next().unwrap().unwrap(), make_arg_hard("abc")); assert_eq!(reader.next().unwrap().unwrap(), make_arg_hard("de")); assert_eq!(reader.next().unwrap().unwrap(), make_arg_hard("ef")); assert_eq!(reader.next().unwrap().unwrap(), make_arg_hard("gh")); assert_eq!(reader.next().unwrap().unwrap(), make_arg_hard("ij")); assert_eq!(reader.next().unwrap(), None); } #[test] fn test_delimiter_parsing() { assert_eq!(parse_delimiter("a").unwrap(), b'a'); assert_eq!(parse_delimiter("\\x61").unwrap(), b'a'); assert_eq!(parse_delimiter("\\x00061").unwrap(), b'a'); assert_eq!(parse_delimiter("\\0141").unwrap(), b'a'); assert_eq!(parse_delimiter("\\0000141").unwrap(), b'a'); assert_eq!(parse_delimiter("\\n").unwrap(), b'\n'); assert!(parse_delimiter("\\0").is_err()); assert!(parse_delimiter("\\x").is_err()); assert!(parse_delimiter("\\").is_err()); assert!(parse_delimiter("abc").is_err()); } } findutils-0.4.2/test_data/depth/1/2/3/f3000064400000000000000000000000001046102023000155700ustar 00000000000000findutils-0.4.2/test_data/depth/1/2/f2000064400000000000000000000000001046102023000154250ustar 00000000000000findutils-0.4.2/test_data/depth/1/f1000064400000000000000000000000001046102023000152630ustar 00000000000000findutils-0.4.2/test_data/depth/f0000064400000000000000000000000001046102023000151220ustar 00000000000000findutils-0.4.2/test_data/links/abbbc000064400000000000000000000000001046102023000156620ustar 00000000000000findutils-0.4.2/test_data/links/subdir/test000064400000000000000000000000001046102023000171000ustar 00000000000000findutils-0.4.2/test_data/simple/abbbc000064400000000000000000000000001046102023000160330ustar 00000000000000findutils-0.4.2/test_data/simple/subdir/ABBBC000064400000000000000000000000001046102023000170630ustar 00000000000000findutils-0.4.2/test_data/size/512bytes000064400000000000000000000010001046102023000160420ustar 00000000000000findutils-0.4.2/tests/common/mod.rs000064400000000000000000000005551046102023000154140ustar 00000000000000// Copyright 2017 Google Inc. // // 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. // As this module is included by all the integration tests, any function used // in one test but not another can cause a dead code warning. #[allow(dead_code)] pub mod test_helpers; findutils-0.4.2/tests/common/test_helpers.rs000064400000000000000000000054671046102023000173450ustar 00000000000000// Copyright 2017 Google Inc. // // 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. use std::cell::RefCell; use std::env; use std::io::{Cursor, Read, Write}; use std::time::SystemTime; use std::vec::Vec; use walkdir::{DirEntry, WalkDir}; use findutils::find::matchers::MatcherIO; use findutils::find::Dependencies; /// A copy of `find::tests::FakeDependencies`. /// TODO: find out how to share #[cfg(test)] functions/structs between unit /// and integration tests. pub struct FakeDependencies { pub output: RefCell>>, now: SystemTime, } impl<'a> FakeDependencies { pub fn new() -> Self { Self { output: RefCell::new(Cursor::new(Vec::::new())), now: SystemTime::now(), } } pub fn new_matcher_io(&'a self) -> MatcherIO<'a> { MatcherIO::new(self) } pub fn get_output_as_string(&self) -> String { let mut cursor = self.output.borrow_mut(); cursor.set_position(0); let mut contents = String::new(); cursor.read_to_string(&mut contents).unwrap(); contents } } impl<'a> Dependencies<'a> for FakeDependencies { fn get_output(&'a self) -> &'a RefCell { &self.output } fn now(&'a self) -> SystemTime { self.now } } pub fn path_to_testing_commandline() -> String { let mut path_to_use = env::current_exe() // this will be something along the lines of /my/homedir/findutils/target/debug/deps/findutils-5532804878869ef1 .expect("can't find path of this executable") .parent() .expect("can't find parent directory of this executable") .to_path_buf(); // and we want /my/homedir/findutils/target/debug/testing-commandline if path_to_use.ends_with("deps") { path_to_use.pop(); } path_to_use = path_to_use.join("testing-commandline"); path_to_use.to_string_lossy().to_string() } #[cfg(windows)] /// A copy of find::tests::fix_up_slashes. /// TODO: find out how to share #[cfg(test)] functions/structs between unit /// and integration tests. pub fn fix_up_slashes(path: &str) -> String { path.replace("/", "\\") } #[cfg(not(windows))] pub fn fix_up_slashes(path: &str) -> String { path.to_string() } /// A copy of `find::tests::FakeDependencies`. /// TODO: find out how to share #[cfg(test)] functions/structs between unit /// and integration tests. pub fn get_dir_entry_for(directory: &str, filename: &str) -> DirEntry { for wrapped_dir_entry in WalkDir::new(fix_up_slashes(directory)) { let dir_entry = wrapped_dir_entry.unwrap(); if dir_entry.file_name().to_string_lossy() == filename { return dir_entry; } } panic!("Couldn't find {} in {}", directory, filename); } findutils-0.4.2/tests/exec_unit_tests.rs000064400000000000000000000166431046102023000165570ustar 00000000000000// Copyright 2017 Google Inc. // // 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. /// ! This file contains what would be normally be unit tests for `find::matchers::exec`. /// ! But as the tests require running an external executable, they need to be run /// ! as integration tests so we can ensure that our testing-commandline binary /// ! has been built. extern crate findutils; extern crate walkdir; use std::env; use std::fs::File; use std::io::Read; use tempfile::Builder; use walkdir::WalkDir; use common::test_helpers::{ fix_up_slashes, get_dir_entry_for, path_to_testing_commandline, FakeDependencies, }; use findutils::find::matchers::exec::SingleExecMatcher; use findutils::find::matchers::Matcher; mod common; #[test] fn matching_executes_code() { let temp_dir = Builder::new() .prefix("matching_executes_code") .tempdir() .unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = SingleExecMatcher::new( &path_to_testing_commandline(), &[temp_dir_path.as_ref(), "abc", "{}", "xyz"], false, ) .expect("Failed to create matcher"); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); let mut f = File::open(temp_dir.path().join("1.txt")).expect("Failed to open output file"); let mut s = String::new(); f.read_to_string(&mut s) .expect("failed to read output file"); assert_eq!( s, fix_up_slashes(&format!( "cwd={}\nargs=\nabc\ntest_data/simple/abbbc\nxyz\n", env::current_dir().unwrap().to_string_lossy() )) ); } #[test] fn matching_executes_code_in_files_directory() { let temp_dir = Builder::new() .prefix("matching_executes_code_in_files_directory") .tempdir() .unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = SingleExecMatcher::new( &path_to_testing_commandline(), &[temp_dir_path.as_ref(), "abc", "{}", "xyz"], true, ) .expect("Failed to create matcher"); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); let mut f = File::open(temp_dir.path().join("1.txt")).expect("Failed to open output file"); let mut s = String::new(); f.read_to_string(&mut s) .expect("failed to read output file"); assert_eq!( s, fix_up_slashes(&format!( "cwd={}/test_data/simple\nargs=\nabc\n./abbbc\nxyz\n", env::current_dir().unwrap().to_string_lossy() )) ); } #[test] fn matching_embedded_filename() { let temp_dir = Builder::new() .prefix("matching_embedded_filename") .tempdir() .unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = SingleExecMatcher::new( &path_to_testing_commandline(), &[temp_dir_path.as_ref(), "abc{}x{}yz"], false, ) .expect("Failed to create matcher"); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); let mut f = File::open(temp_dir.path().join("1.txt")).expect("Failed to open output file"); let mut s = String::new(); f.read_to_string(&mut s) .expect("failed to read output file"); assert_eq!( s, fix_up_slashes(&format!( "cwd={}\nargs=\nabctest_data/simple/abbbcxtest_data/simple/abbbcyz\n", env::current_dir().unwrap().to_string_lossy() )) ); } #[test] /// Running "find . -execdir whatever \;" failed with a No such file or directory error. /// It's now fixed, and this is a regression test to check that it stays fixed. fn execdir_in_current_directory() { let temp_dir = Builder::new() .prefix("execdir_in_current_directory") .tempdir() .unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); let current_dir_entry = WalkDir::new(".") .into_iter() .next() .expect("iterator was empty") .expect("result wasn't OK"); let matcher = SingleExecMatcher::new( &path_to_testing_commandline(), &[temp_dir_path.as_ref(), "abc", "{}", "xyz"], true, ) .expect("Failed to create matcher"); let deps = FakeDependencies::new(); assert!(matcher.matches(¤t_dir_entry, &mut deps.new_matcher_io())); let mut f = File::open(temp_dir.path().join("1.txt")).expect("Failed to open output file"); let mut s = String::new(); f.read_to_string(&mut s) .expect("failed to read output file"); assert_eq!( s, fix_up_slashes(&format!( "cwd={}\nargs=\nabc\n./.\nxyz\n", env::current_dir().unwrap().to_string_lossy() )) ); } #[test] /// Regression test for "find / -execdir whatever \;" fn execdir_in_root_directory() { let temp_dir = Builder::new() .prefix("execdir_in_root_directory") .tempdir() .unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); let cwd = env::current_dir().expect("no current directory"); let root_dir = cwd .ancestors() .last() .expect("current directory has no root"); let root_dir_entry = WalkDir::new(root_dir) .into_iter() .next() .expect("iterator was empty") .expect("result wasn't OK"); let matcher = SingleExecMatcher::new( &path_to_testing_commandline(), &[temp_dir_path.as_ref(), "abc", "{}", "xyz"], true, ) .expect("Failed to create matcher"); let deps = FakeDependencies::new(); assert!(matcher.matches(&root_dir_entry, &mut deps.new_matcher_io())); let mut f = File::open(temp_dir.path().join("1.txt")).expect("Failed to open output file"); let mut s = String::new(); f.read_to_string(&mut s) .expect("failed to read output file"); assert_eq!( s, fix_up_slashes(&format!( "cwd={}\nargs=\nabc\n{}\nxyz\n", root_dir.to_string_lossy(), root_dir.to_string_lossy(), )) ); } #[test] fn matching_fails_if_executable_fails() { let temp_dir = Builder::new() .prefix("matching_fails_if_executable_fails") .tempdir() .unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); let matcher = SingleExecMatcher::new( &path_to_testing_commandline(), &[ temp_dir_path.as_ref(), "--exit_with_failure", "abc", "{}", "xyz", ], true, ) .expect("Failed to create matcher"); let deps = FakeDependencies::new(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); let mut f = File::open(temp_dir.path().join("1.txt")).expect("Failed to open output file"); let mut s = String::new(); f.read_to_string(&mut s) .expect("failed to read output file"); assert_eq!( s, fix_up_slashes(&format!( "cwd={}/test_data/simple\nargs=\n--exit_with_failure\nabc\n.\ /abbbc\nxyz\n", env::current_dir().unwrap().to_string_lossy() )) ); } findutils-0.4.2/tests/find_cmd_tests.rs000064400000000000000000000353311046102023000163320ustar 00000000000000// Copyright 2021 Chad Williamson // // Use of this source code is governed by an MIT-style license that can be // found in the LICENSE file or at https://opensource.org/licenses/MIT. // This file contains integration tests for the find command. // // Note: the `serial` macro is used on tests that make assumptions about the // working directory, since we have at least one test that needs to change it. use assert_cmd::Command; use predicates::prelude::*; use serial_test::serial; use std::fs::File; use std::io::Write; use std::{env, io::ErrorKind}; use tempfile::Builder; #[cfg(unix)] use std::os::unix::fs::symlink; #[cfg(windows)] use std::os::windows::fs::{symlink_dir, symlink_file}; use common::test_helpers::fix_up_slashes; mod common; // Variants of fix_up_slashes that properly escape the forward slashes for being // in a regex. #[cfg(windows)] fn fix_up_regex_slashes(re: &str) -> String { re.replace("/", "\\\\") } #[cfg(not(windows))] fn fix_up_regex_slashes(re: &str) -> String { re.to_owned() } #[serial(working_dir)] #[test] fn no_args() { Command::cargo_bin("find") .expect("found binary") .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::contains("test_data")); } #[serial(working_dir)] #[test] fn two_matchers_both_match() { Command::cargo_bin("find") .expect("found binary") .args(["-type", "d", "-name", "test_data"]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::contains("test_data")); } #[serial(working_dir)] #[test] fn two_matchers_one_matches() { Command::cargo_bin("find") .expect("found binary") .args(["-type", "f", "-name", "test_data"]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::is_empty()); } #[test] fn matcher_with_side_effects_at_end() { let temp_dir = Builder::new().prefix("find_cmd_").tempdir().unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); let test_file = temp_dir.path().join("test"); File::create(&test_file).expect("created test file"); Command::cargo_bin("find") .expect("found binary") .args([&temp_dir_path, "-name", "test", "-delete"]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::is_empty()); assert!(!test_file.exists(), "test file should be deleted"); assert!(temp_dir.path().exists(), "temp dir should NOT be deleted"); } #[test] fn matcher_with_side_effects_in_front() { let temp_dir = Builder::new().prefix("find_cmd_").tempdir().unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); let test_file = temp_dir.path().join("test"); File::create(&test_file).expect("created test file"); Command::cargo_bin("find") .expect("found binary") .args([&temp_dir_path, "-delete", "-name", "test"]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::is_empty()); assert!(!test_file.exists(), "test file should be deleted"); assert!(!temp_dir.path().exists(), "temp dir should also be deleted"); } // This could be covered by a unit test in principle... in practice, changing // the working dir can't be done safely in unit tests unless `--test-threads=1` // or `serial` goes everywhere, and it doesn't seem possible to get an // appropriate `walkdir::DirEntry` for "." without actually changing dirs // (or risking deletion of the repo itself). #[serial(working_dir)] #[test] fn delete_on_dot_dir() { let temp_dir = Builder::new().prefix("example").tempdir().unwrap(); let original_dir = env::current_dir().unwrap(); env::set_current_dir(temp_dir.path()).expect("working dir changed"); // "." should be matched (confirmed by the print), but not deleted. Command::cargo_bin("find") .expect("found binary") .args([".", "-delete", "-print"]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::diff(".\n")); env::set_current_dir(original_dir).expect("restored original working dir"); assert!(temp_dir.path().exists(), "temp dir should still exist"); } #[test] fn regex_types() { let temp_dir = Builder::new().prefix("find_cmd_").tempdir().unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); let test_file = temp_dir.path().join("teeest"); File::create(test_file).expect("created test file"); Command::cargo_bin("find") .expect("found binary") .args([&temp_dir_path, "-regex", &fix_up_regex_slashes(".*/tE+st")]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::is_empty()); Command::cargo_bin("find") .expect("found binary") .args([&temp_dir_path, "-iregex", &fix_up_regex_slashes(".*/tE+st")]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::contains("teeest")); Command::cargo_bin("find") .expect("found binary") .args([ &temp_dir_path, "-regextype", "posix-basic", "-regex", &fix_up_regex_slashes(r".*/te\{1,3\}st"), ]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::contains("teeest")); Command::cargo_bin("find") .expect("found binary") .args([ &temp_dir_path, "-regextype", "posix-extended", "-regex", &fix_up_regex_slashes(".*/te{1,3}st"), ]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::contains("teeest")); Command::cargo_bin("find") .expect("found binary") .args([ &temp_dir_path, "-regextype", "ed", "-regex", &fix_up_regex_slashes(r".*/te\{1,3\}st"), ]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::contains("teeest")); Command::cargo_bin("find") .expect("found binary") .args([ &temp_dir_path, "-regextype", "sed", "-regex", &fix_up_regex_slashes(r".*/te\{1,3\}st"), ]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::contains("teeest")); } #[test] fn empty_files() { let temp_dir = Builder::new().prefix("find_cmd_").tempdir().unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); Command::cargo_bin("find") .expect("found binary") .args([&temp_dir_path, "-empty"]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(fix_up_slashes(&format!("{temp_dir_path}\n"))); let test_file_path = temp_dir.path().join("test"); let mut test_file = File::create(&test_file_path).unwrap(); Command::cargo_bin("find") .expect("found binary") .args([&temp_dir_path, "-empty"]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(fix_up_slashes(&format!( "{}\n", test_file_path.to_string_lossy() ))); let subdir_path = temp_dir.path().join("subdir"); std::fs::create_dir(&subdir_path).unwrap(); Command::cargo_bin("find") .expect("found binary") .args([&temp_dir_path, "-empty", "-sorted"]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(fix_up_slashes(&format!( "{}\n{}\n", subdir_path.to_string_lossy(), test_file_path.to_string_lossy() ))); write!(test_file, "x").unwrap(); test_file.sync_all().unwrap(); Command::cargo_bin("find") .expect("found binary") .args([&temp_dir_path, "-empty", "-sorted"]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(fix_up_slashes(&format!( "{}\n", subdir_path.to_string_lossy(), ))); } #[serial(working_dir)] #[test] fn find_printf() { #[cfg(unix)] { if let Err(e) = symlink("abbbc", "test_data/links/link-f") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } if let Err(e) = symlink("subdir", "test_data/links/link-d") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } if let Err(e) = symlink("missing", "test_data/links/link-missing") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } if let Err(e) = symlink("abbbc/x", "test_data/links/link-notdir") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } if let Err(e) = symlink("link-loop", "test_data/links/link-loop") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } } #[cfg(windows)] { if let Err(e) = symlink_file("abbbc", "test_data/links/link-f") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } if let Err(e) = symlink_dir("subdir", "test_data/links/link-d") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } if let Err(e) = symlink_file("missing", "test_data/links/link-missing") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } if let Err(e) = symlink_file("abbbc/x", "test_data/links/link-notdir") { assert!( e.kind() == ErrorKind::AlreadyExists, "Failed to create sym link: {:?}", e ); } } Command::cargo_bin("find") .expect("found binary") .args([ &fix_up_slashes("./test_data/simple"), "-sorted", "-printf", "%f %d %h %H %p %P %y\n", ]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::diff(fix_up_slashes( "simple 0 ./test_data ./test_data/simple \ ./test_data/simple d\n\ abbbc 1 ./test_data/simple ./test_data/simple \ ./test_data/simple/abbbc abbbc f\n\ subdir 1 ./test_data/simple ./test_data/simple \ ./test_data/simple/subdir subdir d\n\ ABBBC 2 ./test_data/simple/subdir ./test_data/simple \ ./test_data/simple/subdir/ABBBC subdir/ABBBC f\n", ))); Command::cargo_bin("find") .expect("found binary") .args([ &fix_up_slashes("./test_data/links"), "-sorted", "-type", "l", "-printf", "%f %l %y %Y\n", ]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::diff( [ "link-d subdir l d\n", "link-f abbbc l f\n", #[cfg(unix)] "link-loop link-loop l L\n", "link-missing missing l N\n", // We can't detect ENOTDIR on non-unix platforms yet. #[cfg(not(unix))] "link-notdir abbbc/x l ?\n", #[cfg(unix)] "link-notdir abbbc/x l N\n", ] .join(""), )); } #[cfg(unix)] #[serial(working_dir)] #[test] fn find_perm() { Command::cargo_bin("find") .expect("found binary") .args(["-perm", "+rwx"]) .assert() .success(); Command::cargo_bin("find") .expect("found binary") .args(["-perm", "u+rwX"]) .assert() .success(); Command::cargo_bin("find") .expect("found binary") .args(["-perm", "u=g"]) .assert() .success(); } #[cfg(unix)] #[serial(working_dir)] #[test] fn find_inum() { use std::fs::metadata; use std::os::unix::fs::MetadataExt; let inum = metadata("test_data/simple/abbbc") .expect("metadata for abbbc") .ino() .to_string(); Command::cargo_bin("find") .expect("found binary") .args(["test_data", "-inum", &inum]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::contains("abbbc")); } #[cfg(unix)] #[serial(working_dir)] #[test] fn find_links() { Command::cargo_bin("find") .expect("found binary") .args(["test_data", "-links", "1"]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::contains("abbbc")); } #[serial(working_dir)] #[test] fn find_mount_xdev() { // Make sure that -mount/-xdev doesn't prune unexpectedly. // TODO: Test with a mount point in the search. Command::cargo_bin("find") .expect("found binary") .args(["test_data", "-mount"]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::contains("abbbc")); Command::cargo_bin("find") .expect("found binary") .args(["test_data", "-xdev"]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::contains("abbbc")); } #[serial(working_dir)] #[test] fn find_accessible() { Command::cargo_bin("find") .expect("found binary") .args(["test_data", "-readable"]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::contains("abbbc")); Command::cargo_bin("find") .expect("found binary") .args(["test_data", "-writable"]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::contains("abbbc")); #[cfg(unix)] Command::cargo_bin("find") .expect("found binary") .args(["test_data", "-executable"]) .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::contains("abbbc").not()); } findutils-0.4.2/tests/find_exec_tests.rs000064400000000000000000000064271046102023000165170ustar 00000000000000// Copyright 2017 Google Inc. // // 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. /// ! This file contains what would be normally be unit tests for `find::find_main` /// ! related to -exec[dir] and ok[dir] clauses. /// ! But as the tests require running an external executable, they need to be run /// ! as integration tests so we can ensure that our testing-commandline binary /// ! has been built. extern crate findutils; extern crate tempfile; extern crate walkdir; use std::env; use std::fs::File; use std::io::Read; use tempfile::Builder; use common::test_helpers::{fix_up_slashes, path_to_testing_commandline, FakeDependencies}; use findutils::find::find_main; mod common; #[test] fn find_exec() { let temp_dir = tempfile::Builder::new() .prefix("find_exec") .tempdir() .unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); let deps = FakeDependencies::new(); let rc = find_main( &[ "find", &fix_up_slashes("./test_data/simple/subdir"), "-type", "f", "-exec", &path_to_testing_commandline(), temp_dir_path.as_ref(), "(", "{}", "-o", ";", ], &deps, ); assert_eq!(rc, 0); // exec has side effects, so we won't output anything unless -print is // explicitly passed in. assert_eq!(deps.get_output_as_string(), ""); // check the executable ran as expected let mut f = File::open(temp_dir.path().join("1.txt")).expect("Failed to open output file"); let mut s = String::new(); f.read_to_string(&mut s) .expect("failed to read output file"); assert_eq!( s, fix_up_slashes(&format!( "cwd={}\nargs=\n(\n./test_data/simple/subdir/ABBBC\n-o\n", env::current_dir().unwrap().to_string_lossy() )) ); } #[test] fn find_execdir() { let temp_dir = Builder::new().prefix("example").tempdir().unwrap(); let temp_dir_path = temp_dir.path().to_string_lossy(); let deps = FakeDependencies::new(); // only look at files because the "size" of a directory is a system (and filesystem) // dependent thing and we want these tests to be universal. let rc = find_main( &[ "find", &fix_up_slashes("./test_data/simple/subdir"), "-type", "f", "-execdir", &path_to_testing_commandline(), temp_dir_path.as_ref(), ")", "{}", ",", ";", ], &deps, ); assert_eq!(rc, 0); // exec has side effects, so we won't output anything unless -print is // explicitly passed in. assert_eq!(deps.get_output_as_string(), ""); // check the executable ran as expected let mut f = File::open(temp_dir.path().join("1.txt")).expect("Failed to open output file"); let mut s = String::new(); f.read_to_string(&mut s) .expect("failed to read output file"); assert_eq!( s, fix_up_slashes(&format!( "cwd={}/test_data/simple/subdir\nargs=\n)\n./ABBBC\n,\n", env::current_dir().unwrap().to_string_lossy() )) ); } findutils-0.4.2/tests/xargs_tests.rs000064400000000000000000000210371046102023000157110ustar 00000000000000// Copyright 2021 Collabora, Ltd. // // 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. /// ! This file contains integration tests for xargs, separate from the unit /// ! tests so that testing-commandline can be built first. extern crate findutils; extern crate tempfile; use std::io::{Seek, SeekFrom, Write}; use assert_cmd::Command; use predicates::prelude::*; use common::test_helpers::path_to_testing_commandline; mod common; #[test] fn xargs_basics() { Command::cargo_bin("xargs") .expect("found binary") .write_stdin("abc\ndef g\\hi 'i j \"k'") .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::diff("abc def ghi i j \"k\n")); } #[test] fn xargs_null() { Command::cargo_bin("xargs") .expect("found binary") .args(["-0n1"]) .write_stdin("ab c\0d\tef\0") .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::diff("ab c\nd\tef\n")); } #[test] fn xargs_delim() { Command::cargo_bin("xargs") .expect("found binary") .args(["-d1"]) .write_stdin("ab1cd1ef") .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::diff("ab cd ef\n")); Command::cargo_bin("xargs") .expect("found binary") .args(["-d\\t", "-n1"]) .write_stdin("a\nb\td e\tfg") .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::diff("a\nb\nd e\nfg\n")); Command::cargo_bin("xargs") .expect("found binary") .args(["-dabc"]) .assert() .failure() .code(1) .stderr(predicate::str::contains("Invalid")) .stdout(predicate::str::is_empty()); } #[test] fn xargs_null_conflict() { Command::cargo_bin("xargs") .expect("found binary") .args(["-d\t", "-0n1"]) .write_stdin("ab c\0d\tef\0") .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::diff("ab c\nd\tef\n")); } #[test] fn xargs_if_empty() { Command::cargo_bin("xargs") .expect("found binary") .assert() .success() .stderr(predicate::str::is_empty()) // Should echo at least once still. .stdout(predicate::eq("\n")); Command::cargo_bin("xargs") .expect("found binary") .args(["--no-run-if-empty"]) .assert() .success() .stderr(predicate::str::is_empty()) // Should never echo. .stdout(predicate::str::is_empty()); } #[test] fn xargs_max_args() { Command::cargo_bin("xargs") .expect("found binary") .args(["-n2"]) .write_stdin("ab cd ef\ngh i") .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::diff("ab cd\nef gh\ni\n")); } #[test] fn xargs_max_lines() { Command::cargo_bin("xargs") .expect("found binary") .args(["-L2"]) .write_stdin("ab cd\nef\ngh i\n\njkl\n") .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::diff("ab cd ef\ngh i jkl\n")); } #[test] fn xargs_max_args_lines_conflict() { Command::cargo_bin("xargs") .expect("found binary") // -n2 is last, so it should be given priority. .args(["-L2", "-n2"]) .write_stdin("ab cd ef\ngh i") .assert() .success() .stderr(predicate::str::contains("WARNING")) .stdout(predicate::str::diff("ab cd\nef gh\ni\n")); Command::cargo_bin("xargs") .expect("found binary") // -L2 is last, so it should be given priority. .args(["-n2", "-L2"]) .write_stdin("ab cd\nef\ngh i\n\njkl\n") .assert() .success() .stderr(predicate::str::contains("WARNING")) .stdout(predicate::str::diff("ab cd ef\ngh i jkl\n")); } #[test] fn xargs_max_chars() { Command::cargo_bin("xargs") .expect("found binary") .args(["-s11"]) .write_stdin("ab cd efg") .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::diff("ab cd\nefg\n")); // Behavior should be the same with -x, which only takes effect with -L or // -n. Command::cargo_bin("xargs") .expect("found binary") .args(["-xs11"]) .write_stdin("ab cd efg") .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::diff("ab cd\nefg\n")); Command::cargo_bin("xargs") .expect("found binary") .args(["-s10"]) .write_stdin("abcdefghijkl ab") .assert() .failure() .code(1) .stderr(predicate::str::contains("Error:")) .stdout(predicate::str::is_empty()); } #[test] fn xargs_exit_on_large() { Command::cargo_bin("xargs") .expect("found binary") .args(["-xs11", "-n2"]) .write_stdin("ab cd efg h i") .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::diff("ab cd\nefg h\ni\n")); Command::cargo_bin("xargs") .expect("found binary") .args(["-xs11", "-n2"]) .write_stdin("abcdefg hijklmn") .assert() .failure() .code(1) .stderr(predicate::str::contains("Error:")) .stdout(predicate::str::is_empty()); } #[test] fn xargs_exec() { Command::cargo_bin("xargs") .expect("found binary") .args([ "-n2", &path_to_testing_commandline(), "-", "--print_stdin", "--no_print_cwd", ]) .write_stdin("a b c\nd") .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::diff( "stdin=\nargs=\n--print_stdin\n--no_print_cwd\na\nb\n\ stdin=\nargs=\n--print_stdin\n--no_print_cwd\nc\nd\n", )); } #[test] fn xargs_exec_stdin_open() { let mut temp_file = tempfile::NamedTempFile::new().unwrap(); write!(temp_file, "a b c").unwrap(); temp_file.seek(SeekFrom::Start(0)).unwrap(); Command::cargo_bin("xargs") .expect("found binary") .args([ "-a", &temp_file.path().to_string_lossy(), &path_to_testing_commandline(), "-", "--print_stdin", "--no_print_cwd", ]) .write_stdin("test") .assert() .success() .stderr(predicate::str::is_empty()) .stdout(predicate::str::diff( "stdin=test\nargs=\n--print_stdin\n--no_print_cwd\na\nb\nc\n", )); } #[test] fn xargs_exec_failure() { Command::cargo_bin("xargs") .expect("found binary") .args([ "-n1", &path_to_testing_commandline(), "-", "--no_print_cwd", "--exit_with_failure", ]) .write_stdin("a b") .assert() .failure() .code(123) .stderr(predicate::str::is_empty()) .stdout( "args=\n--no_print_cwd\n--exit_with_failure\na\n\ args=\n--no_print_cwd\n--exit_with_failure\nb\n", ); } #[test] fn xargs_exec_urgent_failure() { Command::cargo_bin("xargs") .expect("found binary") .args([ "-n1", &path_to_testing_commandline(), "-", "--no_print_cwd", "--exit_with_urgent_failure", ]) .write_stdin("a b") .assert() .failure() .code(124) .stderr(predicate::str::contains("Error:")) .stdout("args=\n--no_print_cwd\n--exit_with_urgent_failure\na\n"); } #[test] #[cfg(unix)] fn xargs_exec_with_signal() { Command::cargo_bin("xargs") .expect("found binary") .args([ "-n1", &path_to_testing_commandline(), "-", "--no_print_cwd", "--exit_with_signal", ]) .write_stdin("a b") .assert() .failure() .code(125) .stderr(predicate::str::contains("Error:")) .stdout("args=\n--no_print_cwd\n--exit_with_signal\na\n"); } #[test] fn xargs_exec_not_found() { Command::cargo_bin("xargs") .expect("found binary") .args(["this-file-does-not-exist"]) .assert() .failure() .code(127) .stderr(predicate::str::contains("Error:")) .stdout(predicate::str::is_empty()); } findutils-0.4.2/util/build-bfs.sh000075500000000000000000000025661046102023000150240ustar 00000000000000#!/bin/bash set -eo pipefail if ! test -d ../bfs; then echo "Could not find ../bfs" echo "git clone https://github.com/tavianator/bfs.git" exit 1 fi # build the rust implementation cargo build --release FIND=$(readlink -f target/release/find) cd .. make -C bfs -j "$(nproc)" bin/tests/mksock WITH_ONIGURUMA= # Run the GNU find compatibility tests by default if test "$#" -eq 0; then set -- --verbose=tests --gnu --sudo fi LOG_FILE=bfs/tests.log ./bfs/tests/tests.sh --bfs="$FIND" "$@" | tee "$LOG_FILE" || : PASS=$(sed -n "s/^tests passed: \(.*\)/\1/p" "$LOG_FILE" | head -n1) SKIP=$(sed -n "s/^tests skipped: \(.*\)/\1/p" "$LOG_FILE" | head -n1) FAIL=$(sed -n "s/^tests failed: \(.*\)/\1/p" "$LOG_FILE" | head -n1) # Default any missing numbers to zero (e.g. no tests skipped) : ${PASS:=0} : ${SKIP:=0} : ${FAIL:=0} TOTAL=$((PASS + SKIP + FAIL)) if (( TOTAL <= 1 )); then echo "Error in the execution, failing early" exit 1 fi output="BFS tests summary = TOTAL: $TOTAL / PASS: $PASS / SKIP: $SKIP / FAIL: $FAIL" echo "${output}" if (( FAIL > 0 )); then echo "::warning ::${output}"; fi jq -n \ --arg date "$(date --rfc-email)" \ --arg sha "$GITHUB_SHA" \ --arg total "$TOTAL" \ --arg pass "$PASS" \ --arg skip "$SKIP" \ --arg fail "$FAIL" \ '{($date): { sha: $sha, total: $total, pass: $pass, skip: $skip, fail: $fail, }}' > bfs-result.json findutils-0.4.2/util/build-gnu.sh000075500000000000000000000050371046102023000150370ustar 00000000000000#!/bin/bash set -e if test ! -d ../findutils.gnu; then echo "Could not find ../findutils.gnu" echo "git clone https://git.savannah.gnu.org/git/findutils.git findutils.gnu" exit 1 fi # build the rust implementation cargo build --release cp target/release/find ../findutils.gnu/find.rust cp target/release/xargs ../findutils.gnu/xargs.rust # Clone and build upstream repo cd ../findutils.gnu if test ! -f configure; then ./bootstrap ./configure --quiet make -j "$(nproc)" fi # overwrite the GNU version with the rust impl cp find.rust find/find cp xargs.rust xargs/xargs if test -n "$1"; then # if set, run only the test passed export RUN_TEST="TESTS=$1" fi # Run the tests make check-TESTS $RUN_TEST || : make -C find/testsuite check || : make -C xargs/testsuite check || : PASS=0 SKIP=0 FAIL=0 XPASS=0 ERROR=0 LOG_FILE=./find/testsuite/find.log if test -f "$LOG_FILE"; then ((PASS += $(sed -En 's/# of expected passes\s*//p' "$LOG_FILE"))) || : ((FAIL += $(sed -En 's/# of unexpected failures\s*//p' "$LOG_FILE"))) || : fi LOG_FILE=./xargs/testsuite/xargs.log if test -f "$LOG_FILE"; then ((PASS += $(sed -En 's/# of expected passes\s*//p' "$LOG_FILE"))) || : ((FAIL += $(sed -En 's/# of unexpected failures\s*//p' "$LOG_FILE"))) || : fi ((TOTAL = PASS + FAIL)) || : LOG_FILE=./tests/test-suite.log if test -f "$LOG_FILE"; then ((TOTAL += $(sed -n "s/.*# TOTAL: \(.*\)/\1/p" "$LOG_FILE" | tr -d '\r' | head -n1))) || : ((PASS += $(sed -n "s/.*# PASS: \(.*\)/\1/p" "$LOG_FILE" | tr -d '\r' | head -n1))) || : ((SKIP += $(sed -n "s/.*# SKIP: \(.*\)/\1/p" "$LOG_FILE" | tr -d '\r' | head -n1))) || : ((FAIL += $(sed -n "s/.*# FAIL: \(.*\)/\1/p" "$LOG_FILE" | tr -d '\r' | head -n1))) || : ((XPASS += $(sed -n "s/.*# XPASS: \(.*\)/\1/p" "$LOG_FILE" | tr -d '\r' | head -n1))) || : ((ERROR += $(sed -n "s/.*# ERROR: \(.*\)/\1/p" "$LOG_FILE" | tr -d '\r' | head -n1))) || : fi if ((TOTAL <= 1)); then echo "Error in the execution, failing early" exit 1 fi output="GNU tests summary = TOTAL: $TOTAL / PASS: $PASS / FAIL: $FAIL / ERROR: $ERROR" echo "${output}" if [[ "$FAIL" -gt 0 || "$ERROR" -gt 0 ]]; then echo "::warning ::${output}" ; fi jq -n \ --arg date "$(date --rfc-email)" \ --arg sha "$GITHUB_SHA" \ --arg total "$TOTAL" \ --arg pass "$PASS" \ --arg skip "$SKIP" \ --arg fail "$FAIL" \ --arg xpass "$XPASS" \ --arg error "$ERROR" \ '{($date): { sha: $sha, total: $total, pass: $pass, skip: $skip, fail: $fail, xpass: $xpass, error: $error, }}' > ../gnu-result.json findutils-0.4.2/util/compare_bfs_result.py000064400000000000000000000013561046102023000170420ustar 00000000000000#!/usr/bin/python """ Compare the current results to the last results gathered from the main branch to highlight if a PR is making the results better/worse """ import json import sys NEW = json.load(open("bfs-result.json")) OLD = json.load(open("latest-bfs-result.json")) # Extract the specific results from the dicts [last] = OLD.values() [current] = NEW.values() pass_d = int(current["pass"]) - int(last["pass"]) skip_d = int(current["skip"]) - int(last.get("skip", 0)) fail_d = int(current["fail"]) - int(last["fail"]) # Get an annotation to highlight changes print(f"::warning ::Changes from main: PASS {pass_d:+d} / SKIP {skip_d:+d} / FAIL {fail_d:+d}") # If results are worse fail the job to draw attention if pass_d < 0: sys.exit(1) findutils-0.4.2/util/compare_gnu_result.py000064400000000000000000000015141046102023000170550ustar 00000000000000#!/usr/bin/python """ Compare the current results to the last results gathered from the main branch to highlight if a PR is making the results better/worse """ import json import sys NEW = json.load(open("gnu-result.json")) OLD = json.load(open("latest-gnu-result.json")) # Extract the specific results from the dicts last = OLD[list(OLD.keys())[0]] current = NEW[list(NEW.keys())[0]] pass_d = int(current["pass"]) - int(last["pass"]) fail_d = int(current["fail"]) - int(last["fail"]) error_d = int(current["error"]) - int(last["error"]) skip_d = int(current["skip"]) - int(last["skip"]) # Get an annotation to highlight changes print( f"::warning ::Changes from main: PASS {pass_d:+d} / FAIL {fail_d:+d} / ERROR {error_d:+d} / SKIP {skip_d:+d} " ) # If results are worse fail the job to draw attention if pass_d < 0: sys.exit(1) findutils-0.4.2/util/diff-bfs.sh000075500000000000000000000010021046102023000146150ustar 00000000000000#!/bin/bash set -eu export LC_COLLATE=C # Extract the failing test lines from log files failing_tests() { sed -n 's/^\([[:print:]]\+\) failed\!/\1/p' "$1" | sort } comm -3 <(failing_tests "$1") <(failing_tests "$2") | tr '\t' ',' | while IFS=, read old new; do if [ -n "$old" ]; then echo "::warning ::Congrats! The bfs test $old is now passing!" fi if [ -n "$new" ]; then echo "::error ::bfs test failed: $new. $new is passing on 'main'. Maybe you have to rebase?" fi done findutils-0.4.2/util/diff-gnu.sh000075500000000000000000000010451046102023000146430ustar 00000000000000#!/bin/bash set -eu export LC_COLLATE=C # Extract the failing test lines from log files failing_tests() { sed -En 's/FAIL: ([^,:]*)[,:].*/\1/p' "$1"/{tests,{find,xargs}/testsuite}/*.log | sort } comm -3 <(failing_tests "$1") <(failing_tests "$2") | tr '\t' ',' | while IFS=, read old new foo; do if [ -n "$old" ]; then echo "::warning ::Congrats! The GNU test $old is now passing!" fi if [ -n "$new" ]; then echo "::error ::GNU test failed: $new. $new is passing on 'main'. Maybe you have to rebase?" fi done