mirror of
https://github.com/uutils/findutils.git
synced 2026-06-10 15:48:30 -07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8cff2df69a | |||
| d35249ffab |
+36
-11
@@ -53,9 +53,9 @@ jobs:
|
||||
- run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV
|
||||
if: matrix.os == 'windows-latest'
|
||||
|
||||
- name: Test
|
||||
- name: Check
|
||||
run: |
|
||||
cargo test
|
||||
cargo check
|
||||
|
||||
fmt:
|
||||
name: cargo fmt --all -- --check
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
- run: rustup component add clippy
|
||||
- name: cargo clippy
|
||||
run: |
|
||||
cargo clippy --all-targets -- -D warnings
|
||||
cargo clippy -- -D warnings
|
||||
|
||||
grcov:
|
||||
name: Code coverage
|
||||
@@ -105,24 +105,49 @@ jobs:
|
||||
|
||||
- name: Install `rust` toolchain
|
||||
run: |
|
||||
## Install `rust` toolchain
|
||||
rustup toolchain install nightly --no-self-update -c rustfmt --profile minimal
|
||||
rustup default nightly
|
||||
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- name: Generate code coverage
|
||||
run: cargo llvm-cov --all-features --lcov --branch --output-path lcov.info
|
||||
- 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@v4
|
||||
with:
|
||||
name: lcov.info
|
||||
# path: ${{ steps.grcov.outputs.report }}
|
||||
path: lcov.info
|
||||
|
||||
- name: Upload coverage to codecov.io
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: lcov.info
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
# file: ${{ steps.grcov.outputs.report }}
|
||||
file: lcov.info
|
||||
fail_ci_if_error: true
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [External-testsuites]
|
||||
types: [completed]
|
||||
|
||||
name: Comment Test results on the PR
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
upload-pr-comment:
|
||||
if: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
|
||||
name: Upload PR comment
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: List Annotations
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
let artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }},
|
||||
});
|
||||
|
||||
// List all artifacts
|
||||
let matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "comment"
|
||||
})[0];
|
||||
|
||||
// Download the artifact to github.workspace
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
|
||||
let fs = require('fs');
|
||||
fs.writeFileSync('${{ github.workspace }}/comment.zip', Buffer.from(download.data));
|
||||
|
||||
- run: unzip comment.zip
|
||||
|
||||
- name: Comment on PR
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
let fs = require('fs');
|
||||
let annotations = JSON.parse(fs.readFileSync('./annotations.json', 'utf8'));
|
||||
|
||||
let annotationContent = annotations
|
||||
.data
|
||||
.map(annotation => `${annotation.run}: ${annotation.annotation.message}`)
|
||||
.join('\n');
|
||||
|
||||
// check if no changes
|
||||
let gnuTestReport = annotationContent.includes('Run GNU findutils tests: Gnu tests No changes');
|
||||
let bfsTestReport = annotationContent.includes('Run BFS tests: BFS tests No changes');
|
||||
|
||||
if (gnuTestReport && bfsTestReport) {
|
||||
console.log('No changes');
|
||||
return;
|
||||
}
|
||||
|
||||
// Comment on the PR
|
||||
github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: annotations.pull_request_number,
|
||||
body: 'Commit ${{ github.event.workflow_run.head_sha }} has GNU testsuite comparison:\n```\n' + annotationContent + '\n```\n'
|
||||
});
|
||||
+136
-240
@@ -1,253 +1,149 @@
|
||||
on: [push, pull_request]
|
||||
|
||||
name: External-testsuites
|
||||
name: External testsuites
|
||||
|
||||
jobs:
|
||||
gnu-tests:
|
||||
permissions:
|
||||
actions: read
|
||||
|
||||
name: Run GNU findutils tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout findutils
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: findutils
|
||||
- name: Checkout GNU findutils
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: gnu-mirror-unofficial/findutils
|
||||
path: findutils.gnu
|
||||
ref: 5768a03ddfb5e18b1682e339d6cdd24ff721c510
|
||||
submodules: false
|
||||
|
||||
- name: Override submodule URL and initialize submodules
|
||||
# Use github instead of upstream git server
|
||||
run: |
|
||||
git submodule sync --recursive
|
||||
git config submodule.gnulib.url https://github.com/coreutils/gnulib.git
|
||||
git submodule update --init --recursive --depth 1
|
||||
working-directory: findutils.gnu
|
||||
- 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 sed -i 's/^Types: deb$/Types: deb deb-src/' /etc/apt/sources.list.d/ubuntu.sources
|
||||
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: |
|
||||
|
||||
- name: Upload gnu-test-report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gnu-test-report
|
||||
path: |
|
||||
findutils.gnu/find/testsuite/*.log
|
||||
findutils.gnu/xargs/testsuite/*.log
|
||||
findutils.gnu/tests/**/*.log
|
||||
- name: Upload gnu-result
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gnu-result
|
||||
path: gnu-result.json
|
||||
- name: Download artifacts (gnu-result and gnu-test-report)
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
let fs = require('fs');
|
||||
fs.mkdirSync('${{ github.workspace }}/dl', { recursive: true });
|
||||
|
||||
async function downloadArtifact(artifactName) {
|
||||
// List all artifacts from the workflow run
|
||||
let artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.run_id }},
|
||||
});
|
||||
|
||||
// Find the specified artifact
|
||||
let matchArtifact = artifacts.data.artifacts.find((artifact) => artifact.name === artifactName);
|
||||
if (!matchArtifact) {
|
||||
throw new Error(`Artifact "${artifactName}" not found.`);
|
||||
}
|
||||
|
||||
// Download the artifact
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
|
||||
// Save the artifact to a file
|
||||
fs.writeFileSync(`${{ github.workspace }}/dl/${artifactName}.zip`, Buffer.from(download.data));
|
||||
}
|
||||
|
||||
// Download the required artifacts
|
||||
await downloadArtifact("gnu-result");
|
||||
await downloadArtifact("gnu-test-report");
|
||||
|
||||
- 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: |
|
||||
unzip dl/gnu-result.zip -d dl/
|
||||
unzip dl/gnu-test-report.zip -d dl/
|
||||
mv dl/gnu-result.json latest-gnu-result.json
|
||||
python findutils/util/compare_gnu_result.py
|
||||
- name: Checkout findutils
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: findutils
|
||||
- name: Checkout GNU findutils
|
||||
uses: actions/checkout@v4
|
||||
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: |
|
||||
- name: Upload gnu-test-report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gnu-test-report
|
||||
path: |
|
||||
findutils.gnu/find/testsuite/*.log
|
||||
findutils.gnu/xargs/testsuite/*.log
|
||||
findutils.gnu/tests/**/*.log
|
||||
- name: Upload gnu-result
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gnu-result
|
||||
path: gnu-result.json
|
||||
- name: Download the result
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
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@v3
|
||||
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@v4
|
||||
with:
|
||||
path: findutils
|
||||
- name: Checkout BFS
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: tavianator/bfs
|
||||
path: bfs
|
||||
ref: "4.0"
|
||||
- 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 sed -i 's/^Types: deb$/Types: deb deb-src/' /etc/apt/sources.list.d/ubuntu.sources
|
||||
sudo apt-get update
|
||||
sudo apt-get build-dep bfs
|
||||
- name: Run BFS tests
|
||||
shell: bash
|
||||
run: |
|
||||
cd findutils
|
||||
bash util/build-bfs.sh ||:
|
||||
- name: Upload bfs-test-report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bfs-test-report
|
||||
path: bfs/tests.log
|
||||
- name: Upload bfs-result
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bfs-result
|
||||
path: bfs-result.json
|
||||
- name: Download artifacts (gnu-result and bfs-test-report)
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
let fs = require('fs');
|
||||
fs.mkdirSync('${{ github.workspace }}/dl', { recursive: true });
|
||||
|
||||
async function downloadArtifact(artifactName) {
|
||||
// List all artifacts from the workflow run
|
||||
let artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.run_id }},
|
||||
});
|
||||
|
||||
// Find the specified artifact
|
||||
let matchArtifact = artifacts.data.artifacts.find((artifact) => artifact.name === artifactName);
|
||||
if (!matchArtifact) {
|
||||
throw new Error(`Artifact "${artifactName}" not found.`);
|
||||
}
|
||||
|
||||
// Download the artifact
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
|
||||
// Save the artifact to a file
|
||||
fs.writeFileSync(`${{ github.workspace }}/dl/${artifactName}.zip`, Buffer.from(download.data));
|
||||
}
|
||||
|
||||
// Download the required artifacts
|
||||
await downloadArtifact("bfs-result");
|
||||
await downloadArtifact("bfs-test-report");
|
||||
- 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: |
|
||||
unzip dl/bfs-result.zip -d dl/
|
||||
unzip dl/bfs-test-report.zip -d dl/
|
||||
mv dl/bfs-result.json latest-bfs-result.json
|
||||
python findutils/util/compare_bfs_result.py
|
||||
|
||||
upload-annotations:
|
||||
name: Upload annotations
|
||||
runs-on: ubuntu-latest
|
||||
needs: [gnu-tests, bfs-tests]
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
steps:
|
||||
- name: List Annotations
|
||||
uses: actions/github-script@v7
|
||||
|
||||
with:
|
||||
script: |
|
||||
let runs = await github.rest.checks.listForRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: '${{ github.event.pull_request.head.sha }}'
|
||||
});
|
||||
|
||||
let names = ['Run GNU findutils tests', 'Run BFS tests'];
|
||||
let results = [];
|
||||
runs.data.check_runs.filter(check => names.includes(check.name)).forEach(run => results.push(run));
|
||||
|
||||
let annotations = { data: [], pull_request_number: '${{ github.event.number }}' };
|
||||
for (let result of results) {
|
||||
let run = await github.rest.checks.listAnnotations({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
check_run_id: result.id
|
||||
});
|
||||
|
||||
run.data.forEach(data => {
|
||||
annotations.data.push({
|
||||
run: result.name,
|
||||
annotation: data
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Remove duplicate items.
|
||||
annotations.data = annotations.data.filter((value, index, self) =>
|
||||
self.findIndex(v => v.annotation.message === value.annotation.message) === index);
|
||||
|
||||
let fs = require('fs');
|
||||
fs.writeFileSync('${{ github.workspace }}/annotations.json', JSON.stringify(annotations));
|
||||
|
||||
- name: Upload annotations
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: comment
|
||||
path: annotations.json
|
||||
- name: Checkout findutils
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: findutils
|
||||
- name: Checkout BFS
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: tavianator/bfs
|
||||
path: bfs
|
||||
ref: '3.1.3'
|
||||
- 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 ||:
|
||||
- name: Upload bfs-test-report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bfs-test-report
|
||||
path: bfs/tests.log
|
||||
- name: Upload bfs-result
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bfs-result
|
||||
path: bfs-result.json
|
||||
- name: Download the result
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
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@v3
|
||||
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
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/
|
||||
#
|
||||
# Copyright 2022-2024, axodotdev
|
||||
# Copyright 2022-2023, axodotdev
|
||||
# SPDX-License-Identifier: MIT or Apache-2.0
|
||||
#
|
||||
# CI that:
|
||||
#
|
||||
# * checks for a Git Tag that looks like a release
|
||||
# * builds artifacts with dist (archives, installers, hashes)
|
||||
# * builds artifacts with cargo-dist (archives, installers, hashes)
|
||||
# * uploads those artifacts to temporary workflow zip
|
||||
# * on success, uploads the artifacts to a GitHub Release
|
||||
# * on success, uploads the artifacts to a Github Release
|
||||
#
|
||||
# Note that the GitHub Release will be created with a generated
|
||||
# Note that the Github Release will be created with a generated
|
||||
# title/body based on your changelogs.
|
||||
|
||||
name: Release
|
||||
|
||||
permissions:
|
||||
"contents": "write"
|
||||
contents: write
|
||||
|
||||
# This task will run whenever you push a git tag that looks like a version
|
||||
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
|
||||
@@ -24,30 +23,30 @@ permissions:
|
||||
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
|
||||
#
|
||||
# If PACKAGE_NAME is specified, then the announcement will be for that
|
||||
# package (erroring out if it doesn't have the given version or isn't dist-able).
|
||||
# package (erroring out if it doesn't have the given version or isn't cargo-dist-able).
|
||||
#
|
||||
# If PACKAGE_NAME isn't specified, then the announcement will be for all
|
||||
# (dist-able) packages in the workspace with that version (this mode is
|
||||
# (cargo-dist-able) packages in the workspace with that version (this mode is
|
||||
# intended for workspaces with only one dist-able package, or with all dist-able
|
||||
# packages versioned/released in lockstep).
|
||||
#
|
||||
# If you push multiple tags at once, separate instances of this workflow will
|
||||
# spin up, creating an independent announcement for each one. However, GitHub
|
||||
# spin up, creating an independent announcement for each one. However Github
|
||||
# will hard limit this to 3 tags per commit, as it will assume more tags is a
|
||||
# mistake.
|
||||
#
|
||||
# If there's a prerelease-style suffix to the version, then the release(s)
|
||||
# will be marked as a prerelease.
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- '**[0-9]+.[0-9]+.[0-9]+*'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
# Run 'dist plan' (or host) to determine what tasks we need to do
|
||||
# Run 'cargo dist plan' (or host) to determine what tasks we need to do
|
||||
plan:
|
||||
runs-on: "ubuntu-20.04"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
val: ${{ steps.plan.outputs.manifest }}
|
||||
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
|
||||
@@ -59,16 +58,11 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install dist
|
||||
- name: Install cargo-dist
|
||||
# we specify bash to get pipefail; it guards against the `curl` command
|
||||
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.28.0/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/dist
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh"
|
||||
# sure would be cool if github gave us proper conditionals...
|
||||
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
|
||||
# functionality based on whether this is a pull_request, and whether it's from a fork.
|
||||
@@ -76,8 +70,8 @@ jobs:
|
||||
# but also really annoying to build CI around when it needs secrets to work right.)
|
||||
- id: plan
|
||||
run: |
|
||||
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||
echo "cargo dist ran successfully"
|
||||
cat plan-dist-manifest.json
|
||||
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
@@ -95,38 +89,28 @@ jobs:
|
||||
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# Target platforms/runners are computed by dist in create-release.
|
||||
# Target platforms/runners are computed by cargo-dist in create-release.
|
||||
# Each member of the matrix has the following arguments:
|
||||
#
|
||||
# - runner: the github runner
|
||||
# - dist-args: cli flags to pass to dist
|
||||
# - install-dist: expression to run to install dist on the runner
|
||||
# - dist-args: cli flags to pass to cargo dist
|
||||
# - install-dist: expression to run to install cargo-dist on the runner
|
||||
#
|
||||
# Typically there will be:
|
||||
# - 1 "global" task that builds universal installers
|
||||
# - N "local" tasks that build each platform's binaries and platform-specific installers
|
||||
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
container: ${{ matrix.container && matrix.container.image || null }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
|
||||
steps:
|
||||
- name: enable windows longpaths
|
||||
run: |
|
||||
git config --global core.longpaths true
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install Rust non-interactively if not already installed
|
||||
if: ${{ matrix.container }}
|
||||
run: |
|
||||
if ! command -v cargo > /dev/null 2>&1; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
fi
|
||||
- name: Install dist
|
||||
run: ${{ matrix.install_dist.run }}
|
||||
- uses: swatinem/rust-cache@v2
|
||||
- name: Install cargo-dist
|
||||
run: ${{ matrix.install_dist }}
|
||||
# Get the dist-manifest
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -140,8 +124,8 @@ jobs:
|
||||
- name: Build artifacts
|
||||
run: |
|
||||
# Actually do builds and make zips and whatnot
|
||||
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
|
||||
echo "cargo dist ran successfully"
|
||||
- id: cargo-dist
|
||||
name: Post-build
|
||||
# We force bash here just because github makes it really hard to get values up
|
||||
@@ -151,7 +135,7 @@ jobs:
|
||||
run: |
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
@@ -176,12 +160,9 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
- name: Install cargo-dist
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh"
|
||||
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -192,12 +173,12 @@ jobs:
|
||||
- id: cargo-dist
|
||||
shell: bash
|
||||
run: |
|
||||
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||
echo "cargo dist ran successfully"
|
||||
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
@@ -225,12 +206,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
- name: Install cargo-dist
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh"
|
||||
# Fetch artifacts from scratch-storage
|
||||
- name: Fetch artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -238,10 +215,11 @@ jobs:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
# This is a harmless no-op for Github Releases, hosting for that happens in "announce"
|
||||
- id: host
|
||||
shell: bash
|
||||
run: |
|
||||
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||
cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||
echo "artifacts uploaded and released successfully"
|
||||
cat dist-manifest.json
|
||||
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
@@ -251,29 +229,8 @@ jobs:
|
||||
# Overwrite the previous copy
|
||||
name: artifacts-dist-manifest
|
||||
path: dist-manifest.json
|
||||
# Create a GitHub Release while uploading all files to it
|
||||
- name: "Download GitHub Artifacts"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
- name: Cleanup
|
||||
run: |
|
||||
# Remove the granular manifests
|
||||
rm -f artifacts/*-dist-manifest.json
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
|
||||
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
|
||||
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
|
||||
RELEASE_COMMIT: "${{ github.sha }}"
|
||||
run: |
|
||||
# Write and read notes from a file to avoid quoting breaking things
|
||||
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
|
||||
|
||||
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
|
||||
|
||||
# Create a Github Release while uploading all files to it
|
||||
announce:
|
||||
needs:
|
||||
- plan
|
||||
@@ -289,3 +246,21 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: "Download Github Artifacts"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
- name: Cleanup
|
||||
run: |
|
||||
# Remove the granular manifests
|
||||
rm -f artifacts/*-dist-manifest.json
|
||||
- name: Create Github Release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: ${{ needs.plan.outputs.tag }}
|
||||
name: ${{ fromJson(needs.host.outputs.val).announcement_title }}
|
||||
body: ${{ fromJson(needs.host.outputs.val).announcement_github_body }}
|
||||
prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }}
|
||||
artifacts: "artifacts/*"
|
||||
|
||||
Generated
+228
-386
File diff suppressed because it is too large
Load Diff
+20
-25
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "findutils"
|
||||
version = "0.8.0"
|
||||
version = "0.5.0"
|
||||
homepage = "https://github.com/uutils/findutils"
|
||||
repository = "https://github.com/uutils/findutils"
|
||||
edition = "2021"
|
||||
@@ -10,24 +10,23 @@ description = "Rust implementation of GNU findutils"
|
||||
authors = ["uutils developers"]
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.40"
|
||||
chrono = "0.4.37"
|
||||
clap = "4.5"
|
||||
faccess = "0.2.4"
|
||||
walkdir = "2.5"
|
||||
regex = "1.11"
|
||||
regex = "1.7"
|
||||
once_cell = "1.19"
|
||||
onig = { version = "6.4", default-features = false }
|
||||
uucore = { version = "0.0.30", features = ["entries", "fs", "fsext", "mode"] }
|
||||
nix = { version = "0.29", features = ["fs", "user"] }
|
||||
argmax = "0.3.1"
|
||||
uucore = { version = "0.0.25", features = ["entries", "fs", "fsext", "mode"] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2"
|
||||
filetime = "0.2"
|
||||
nix = { version = "0.29", features = ["fs"] }
|
||||
nix = { version = "0.28", features = ["fs"] }
|
||||
predicates = "3"
|
||||
serial_test = "3.2"
|
||||
serial_test = "3.0"
|
||||
tempfile = "3"
|
||||
pretty_assertions = "1.4.1"
|
||||
pretty_assertions = "1.4.0"
|
||||
|
||||
[[bin]]
|
||||
name = "find"
|
||||
@@ -46,19 +45,15 @@ path = "src/testing/commandline/main.rs"
|
||||
inherits = "release"
|
||||
lto = "thin"
|
||||
|
||||
|
||||
[lints.clippy]
|
||||
multiple_crate_versions = "allow"
|
||||
cargo_common_metadata = "allow"
|
||||
uninlined_format_args = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
|
||||
use_self = "warn"
|
||||
needless_pass_by_value = "warn"
|
||||
semicolon_if_nothing_returned = "warn"
|
||||
single_char_pattern = "warn"
|
||||
explicit_iter_loop = "warn"
|
||||
if_not_else = "warn"
|
||||
manual_let_else = "warn"
|
||||
# Disable for now, we have a few occurrences
|
||||
# panic = "warn"
|
||||
# Config for 'cargo dist'
|
||||
[workspace.metadata.dist]
|
||||
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.12.0"
|
||||
# CI backends to support
|
||||
ci = ["github"]
|
||||
# The installers to generate for each app
|
||||
installers = []
|
||||
# Target platforms to build apps for (Rust target-triple syntax)
|
||||
targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]
|
||||
# Publish jobs to run in CI
|
||||
pr-run-mode = "plan"
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
[workspace]
|
||||
members = ["cargo:."]
|
||||
|
||||
# Config for 'dist'
|
||||
[dist]
|
||||
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.28.0"
|
||||
# CI backends to support
|
||||
ci = "github"
|
||||
# The installers to generate for each app
|
||||
installers = ["shell"]
|
||||
# Target platforms to build apps for (Rust target-triple syntax)
|
||||
targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]
|
||||
# Which actions to run on pull requests
|
||||
pr-run-mode = "plan"
|
||||
# Path that installers should place binaries in
|
||||
install-path = "CARGO_HOME"
|
||||
# Whether to install an updater program
|
||||
install-updater = false
|
||||
@@ -5,11 +5,6 @@
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
fn main() {
|
||||
// Ignores the SIGPIPE signal.
|
||||
// This is to solve the problem that when find is used with a pipe character,
|
||||
// the downstream software of the standard output stream closes the pipe and triggers a panic.
|
||||
uucore::panic::mute_sigpipe_panic();
|
||||
|
||||
let args = std::env::args().collect::<Vec<String>>();
|
||||
let strs: Vec<&str> = args.iter().map(std::convert::AsRef::as_ref).collect();
|
||||
let deps = findutils::find::StandardDependencies::new();
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use faccess::PathExt;
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
use super::{Matcher, MatcherIO};
|
||||
|
||||
/// Matcher for -{read,writ,execut}able.
|
||||
pub enum AccessMatcher {
|
||||
@@ -16,7 +17,7 @@ pub enum AccessMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for AccessMatcher {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
let path = file_info.path();
|
||||
|
||||
match self {
|
||||
|
||||
+12
-10
@@ -7,29 +7,32 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use std::fs;
|
||||
use std::fs::{self, FileType};
|
||||
use std::io::{self, stderr, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::{Matcher, MatcherIO};
|
||||
|
||||
pub struct DeleteMatcher;
|
||||
|
||||
impl DeleteMatcher {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
DeleteMatcher
|
||||
}
|
||||
|
||||
fn delete(&self, entry: &WalkEntry) -> io::Result<()> {
|
||||
if entry.file_type().is_dir() && !entry.path_is_symlink() {
|
||||
fs::remove_dir(entry.path())
|
||||
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(entry.path())
|
||||
fs::remove_file(file_path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher for DeleteMatcher {
|
||||
fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
let path = file_info.path();
|
||||
let path_str = path.to_string_lossy();
|
||||
|
||||
@@ -40,10 +43,9 @@ impl Matcher for DeleteMatcher {
|
||||
return true;
|
||||
}
|
||||
|
||||
match self.delete(file_info) {
|
||||
match self.delete(path, file_info.file_type()) {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
matcher_io.set_exit_code(1);
|
||||
writeln!(&mut stderr(), "Failed to delete {path_str}: {e}").unwrap();
|
||||
false
|
||||
}
|
||||
|
||||
@@ -9,18 +9,18 @@ use std::{
|
||||
io::{stderr, Write},
|
||||
};
|
||||
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
use super::Matcher;
|
||||
|
||||
pub struct EmptyMatcher;
|
||||
|
||||
impl EmptyMatcher {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
pub fn new() -> EmptyMatcher {
|
||||
EmptyMatcher
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher for EmptyMatcher {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
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,
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
//! Paths encountered during a walk.
|
||||
|
||||
use std::cell::OnceCell;
|
||||
use std::error::Error;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::fs::{self, Metadata};
|
||||
use std::io::{self, ErrorKind};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::Follow;
|
||||
|
||||
/// Wrapper for a directory entry.
|
||||
#[derive(Debug)]
|
||||
enum Entry {
|
||||
/// Wraps an explicit path and depth.
|
||||
Explicit(PathBuf, usize),
|
||||
/// Wraps a WalkDir entry.
|
||||
WalkDir(DirEntry),
|
||||
}
|
||||
|
||||
/// File types.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum FileType {
|
||||
Unknown,
|
||||
Fifo,
|
||||
CharDevice,
|
||||
Directory,
|
||||
BlockDevice,
|
||||
Regular,
|
||||
Symlink,
|
||||
Socket,
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
pub fn is_dir(self) -> bool {
|
||||
self == Self::Directory
|
||||
}
|
||||
|
||||
pub fn is_file(self) -> bool {
|
||||
self == Self::Regular
|
||||
}
|
||||
|
||||
pub fn is_symlink(self) -> bool {
|
||||
self == Self::Symlink
|
||||
}
|
||||
}
|
||||
|
||||
impl From<fs::FileType> for FileType {
|
||||
fn from(t: fs::FileType) -> Self {
|
||||
if t.is_dir() {
|
||||
return Self::Directory;
|
||||
}
|
||||
if t.is_file() {
|
||||
return Self::Regular;
|
||||
}
|
||||
if t.is_symlink() {
|
||||
return Self::Symlink;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if t.is_fifo() {
|
||||
return Self::Fifo;
|
||||
}
|
||||
if t.is_char_device() {
|
||||
return Self::CharDevice;
|
||||
}
|
||||
if t.is_block_device() {
|
||||
return Self::BlockDevice;
|
||||
}
|
||||
if t.is_socket() {
|
||||
return Self::Socket;
|
||||
}
|
||||
}
|
||||
|
||||
Self::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
/// An error encountered while walking a file system.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WalkError {
|
||||
/// The path that caused the error, if known.
|
||||
path: Option<PathBuf>,
|
||||
/// The depth below the root path, if known.
|
||||
depth: Option<usize>,
|
||||
/// The io::Error::raw_os_error(), if known.
|
||||
raw: Option<i32>,
|
||||
}
|
||||
|
||||
impl WalkError {
|
||||
/// Get the path this error occurred on, if known.
|
||||
pub fn path(&self) -> Option<&Path> {
|
||||
self.path.as_deref()
|
||||
}
|
||||
|
||||
/// Get the traversal depth when this error occurred, if known.
|
||||
pub fn depth(&self) -> Option<usize> {
|
||||
self.depth
|
||||
}
|
||||
|
||||
/// Get the kind of I/O error.
|
||||
pub fn kind(&self) -> ErrorKind {
|
||||
io::Error::from(self).kind()
|
||||
}
|
||||
|
||||
/// Check for ErrorKind::{NotFound,NotADirectory}.
|
||||
pub fn is_not_found(&self) -> bool {
|
||||
if self.kind() == ErrorKind::NotFound {
|
||||
return true;
|
||||
}
|
||||
|
||||
// NotADirectory is nightly-only
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if self.raw == Some(uucore::libc::ENOTDIR) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Check for ErrorKind::FilesystemLoop.
|
||||
pub fn is_loop(&self) -> bool {
|
||||
#[cfg(unix)]
|
||||
return self.raw == Some(uucore::libc::ELOOP);
|
||||
|
||||
#[cfg(not(unix))]
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for WalkError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
let ioe = io::Error::from(self);
|
||||
if let Some(path) = &self.path {
|
||||
write!(f, "{}: {}", path.display(), ioe)
|
||||
} else {
|
||||
write!(f, "{}", ioe)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for WalkError {}
|
||||
|
||||
impl From<io::Error> for WalkError {
|
||||
fn from(e: io::Error) -> Self {
|
||||
Self::from(&e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&io::Error> for WalkError {
|
||||
fn from(e: &io::Error) -> Self {
|
||||
Self {
|
||||
path: None,
|
||||
depth: None,
|
||||
raw: e.raw_os_error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<walkdir::Error> for WalkError {
|
||||
fn from(e: walkdir::Error) -> Self {
|
||||
Self::from(&e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&walkdir::Error> for WalkError {
|
||||
fn from(e: &walkdir::Error) -> Self {
|
||||
Self {
|
||||
path: e.path().map(|p| p.to_owned()),
|
||||
depth: Some(e.depth()),
|
||||
raw: e.io_error().and_then(|e| e.raw_os_error()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WalkError> for io::Error {
|
||||
fn from(e: WalkError) -> Self {
|
||||
Self::from(&e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&WalkError> for io::Error {
|
||||
fn from(e: &WalkError) -> Self {
|
||||
e.raw
|
||||
.map(Self::from_raw_os_error)
|
||||
.unwrap_or_else(|| ErrorKind::Other.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A path encountered while walking a file system.
|
||||
#[derive(Debug)]
|
||||
pub struct WalkEntry {
|
||||
/// The wrapped path/dirent.
|
||||
inner: Entry,
|
||||
/// Whether to follow symlinks.
|
||||
follow: Follow,
|
||||
/// Cached metadata.
|
||||
meta: OnceCell<Result<Metadata, WalkError>>,
|
||||
}
|
||||
|
||||
impl WalkEntry {
|
||||
/// Create a new WalkEntry for a specific file.
|
||||
pub fn new(path: impl Into<PathBuf>, depth: usize, follow: Follow) -> Self {
|
||||
Self {
|
||||
inner: Entry::Explicit(path.into(), depth),
|
||||
follow,
|
||||
meta: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [walkdir::DirEntry] to a [WalkEntry]. Errors due to broken symbolic links will be
|
||||
/// converted to valid entries, but other errors will be propagated.
|
||||
pub fn from_walkdir(
|
||||
result: walkdir::Result<DirEntry>,
|
||||
follow: Follow,
|
||||
) -> Result<Self, WalkError> {
|
||||
let result = result.map_err(WalkError::from);
|
||||
|
||||
match result {
|
||||
Ok(entry) => {
|
||||
let ret = if entry.depth() == 0 && follow != Follow::Never {
|
||||
// DirEntry::file_type() is wrong for root symlinks when follow_root_links is set
|
||||
Self::new(entry.path(), 0, follow)
|
||||
} else {
|
||||
Self {
|
||||
inner: Entry::WalkDir(entry),
|
||||
follow,
|
||||
meta: OnceCell::new(),
|
||||
}
|
||||
};
|
||||
Ok(ret)
|
||||
}
|
||||
Err(e) if e.is_not_found() => {
|
||||
// Detect broken symlinks and replace them with explicit entries
|
||||
if let (Some(path), Some(depth)) = (e.path(), e.depth()) {
|
||||
if let Ok(meta) = path.symlink_metadata() {
|
||||
return Ok(Self {
|
||||
inner: Entry::Explicit(path.into(), depth),
|
||||
follow: Follow::Never,
|
||||
meta: Ok(meta).into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(e)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the path to this entry.
|
||||
pub fn path(&self) -> &Path {
|
||||
match &self.inner {
|
||||
Entry::Explicit(path, _) => path.as_path(),
|
||||
Entry::WalkDir(ent) => ent.path(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the path to this entry.
|
||||
pub fn into_path(self) -> PathBuf {
|
||||
match self.inner {
|
||||
Entry::Explicit(path, _) => path,
|
||||
Entry::WalkDir(ent) => ent.into_path(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the name of this entry.
|
||||
pub fn file_name(&self) -> &OsStr {
|
||||
match &self.inner {
|
||||
Entry::Explicit(path, _) => {
|
||||
// Path::file_name() only works if the last component is normal
|
||||
path.components()
|
||||
.next_back()
|
||||
.map(|c| c.as_os_str())
|
||||
.unwrap_or_else(|| path.as_os_str())
|
||||
}
|
||||
Entry::WalkDir(ent) => ent.file_name(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the depth of this entry below the root.
|
||||
pub fn depth(&self) -> usize {
|
||||
match &self.inner {
|
||||
Entry::Explicit(_, depth) => *depth,
|
||||
Entry::WalkDir(ent) => ent.depth(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get whether symbolic links are followed for this entry.
|
||||
pub fn follow(&self) -> bool {
|
||||
self.follow.follow_at_depth(self.depth())
|
||||
}
|
||||
|
||||
/// Get the metadata on a cache miss.
|
||||
fn get_metadata(&self) -> Result<Metadata, WalkError> {
|
||||
self.follow.metadata_at_depth(self.path(), self.depth())
|
||||
}
|
||||
|
||||
/// Get the [Metadata] for this entry, following symbolic links if appropriate.
|
||||
/// Multiple calls to this function will cache and re-use the same [Metadata].
|
||||
pub fn metadata(&self) -> Result<&Metadata, WalkError> {
|
||||
let result = self.meta.get_or_init(|| match &self.inner {
|
||||
Entry::Explicit(_, _) => Ok(self.get_metadata()?),
|
||||
Entry::WalkDir(ent) => Ok(ent.metadata()?),
|
||||
});
|
||||
result.as_ref().map_err(|e| e.clone())
|
||||
}
|
||||
|
||||
/// Get the file type of this entry.
|
||||
pub fn file_type(&self) -> FileType {
|
||||
match &self.inner {
|
||||
Entry::Explicit(_, _) => self
|
||||
.metadata()
|
||||
.map(|m| m.file_type().into())
|
||||
.unwrap_or(FileType::Unknown),
|
||||
Entry::WalkDir(ent) => ent.file_type().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether this entry is a symbolic link, regardless of whether links
|
||||
/// are being followed.
|
||||
pub fn path_is_symlink(&self) -> bool {
|
||||
match &self.inner {
|
||||
Entry::Explicit(path, _) => {
|
||||
if self.follow() {
|
||||
path.symlink_metadata()
|
||||
.is_ok_and(|m| m.file_type().is_symlink())
|
||||
} else {
|
||||
self.file_type().is_symlink()
|
||||
}
|
||||
}
|
||||
Entry::WalkDir(ent) => ent.path_is_symlink(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-123
@@ -4,14 +4,14 @@
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use std::cell::RefCell;
|
||||
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, WalkEntry};
|
||||
use super::{Matcher, MatcherIO};
|
||||
|
||||
enum Arg {
|
||||
FileArg(Vec<OsString>),
|
||||
@@ -52,7 +52,7 @@ impl SingleExecMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for SingleExecMatcher {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
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() {
|
||||
@@ -67,7 +67,7 @@ impl Matcher for SingleExecMatcher {
|
||||
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())),
|
||||
Arg::FileArg(ref parts) => command.arg(&parts.join(path_to_file.as_os_str())),
|
||||
};
|
||||
}
|
||||
if self.exec_in_parent_dir {
|
||||
@@ -98,125 +98,6 @@ impl Matcher for SingleExecMatcher {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MultiExecMatcher {
|
||||
executable: String,
|
||||
args: Vec<OsString>,
|
||||
exec_in_parent_dir: bool,
|
||||
/// Command to build while matching.
|
||||
command: RefCell<Option<argmax::Command>>,
|
||||
}
|
||||
|
||||
impl MultiExecMatcher {
|
||||
pub fn new(
|
||||
executable: &str,
|
||||
args: &[&str],
|
||||
exec_in_parent_dir: bool,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
let transformed_args = args.iter().map(OsString::from).collect();
|
||||
|
||||
Ok(Self {
|
||||
executable: executable.to_string(),
|
||||
args: transformed_args,
|
||||
exec_in_parent_dir,
|
||||
command: RefCell::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
fn new_command(&self) -> argmax::Command {
|
||||
let mut command = argmax::Command::new(&self.executable);
|
||||
command.try_args(&self.args).unwrap();
|
||||
command
|
||||
}
|
||||
|
||||
fn run_command(&self, command: &mut argmax::Command, matcher_io: &mut MatcherIO) {
|
||||
match command.status() {
|
||||
Ok(status) => {
|
||||
if !status.success() {
|
||||
matcher_io.set_exit_code(1);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
writeln!(&mut stderr(), "Failed to run {}: {}", self.executable, e).unwrap();
|
||||
matcher_io.set_exit_code(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher for MultiExecMatcher {
|
||||
fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
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()
|
||||
};
|
||||
let mut command = self.command.borrow_mut();
|
||||
let command = command.get_or_insert_with(|| self.new_command());
|
||||
|
||||
// Build command, or dispatch it before when it is long enough.
|
||||
if command.try_arg(&path_to_file).is_err() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.run_command(command, matcher_io);
|
||||
|
||||
// Reset command status.
|
||||
*command = self.new_command();
|
||||
if let Err(e) = command.try_arg(&path_to_file) {
|
||||
writeln!(
|
||||
&mut stderr(),
|
||||
"Cannot fit a single argument {}: {}",
|
||||
&path_to_file.to_string_lossy(),
|
||||
e
|
||||
)
|
||||
.unwrap();
|
||||
matcher_io.set_exit_code(1);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn finished_dir(&self, dir: &Path, matcher_io: &mut MatcherIO) {
|
||||
// Dispatch command for -execdir.
|
||||
if self.exec_in_parent_dir {
|
||||
let mut command = self.command.borrow_mut();
|
||||
if let Some(mut command) = command.take() {
|
||||
command.current_dir(Path::new(".").join(dir));
|
||||
self.run_command(&mut command, matcher_io);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finished(&self, matcher_io: &mut MatcherIO) {
|
||||
// Dispatch command for -exec.
|
||||
if !self.exec_in_parent_dir {
|
||||
let mut command = self.command.borrow_mut();
|
||||
if let Some(mut command) = command.take() {
|
||||
self.run_command(&mut command, matcher_io);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
// This file is part of the uutils findutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
#[cfg(unix)]
|
||||
use uucore::error::UResult;
|
||||
|
||||
/// The latest mapping from dev_id to fs_type, used for saving mount info reads
|
||||
#[cfg(unix)]
|
||||
pub struct Cache {
|
||||
dev_id: String,
|
||||
fs_type: String,
|
||||
}
|
||||
|
||||
/// Get the filesystem type of a file.
|
||||
/// 1. get the metadata of the file
|
||||
/// 2. get the device ID of the metadata
|
||||
/// 3. search the cache, then the filesystem list
|
||||
///
|
||||
/// Returns an empty string when no file system list matches.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the metadata could not be read.
|
||||
/// Returns an error if the filesystem list could not be read.
|
||||
///
|
||||
/// This is only supported on Unix.
|
||||
#[cfg(unix)]
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
io::{stderr, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn get_file_system_type(path: &Path, cache: &RefCell<Option<Cache>>) -> UResult<String> {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
// use symlink_metadata (lstat under the hood) instead of metadata (stat) to make sure that it
|
||||
// does not return an error when there is a (broken) symlink; this is aligned with GNU find.
|
||||
let metadata = match path.symlink_metadata() {
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) => Err(err)?,
|
||||
};
|
||||
let dev_id = metadata.dev().to_string();
|
||||
|
||||
if let Some(cache) = cache.borrow().as_ref() {
|
||||
if cache.dev_id == dev_id {
|
||||
return Ok(cache.fs_type.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let fs_list = uucore::fsext::read_fs_list()?;
|
||||
let result = fs_list
|
||||
.into_iter()
|
||||
.find(|fs| fs.dev_id == dev_id)
|
||||
.map_or_else(String::new, |fs| fs.fs_type);
|
||||
|
||||
// cache the latest query if not a match before
|
||||
cache.replace(Some(Cache {
|
||||
dev_id,
|
||||
fs_type: result.clone(),
|
||||
}));
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// This matcher handles the -fstype argument.
|
||||
/// It matches the filesystem type of the file.
|
||||
///
|
||||
/// This is only supported on Unix.
|
||||
pub struct FileSystemMatcher {
|
||||
#[cfg(unix)]
|
||||
fs_text: String,
|
||||
#[cfg(unix)]
|
||||
cache: RefCell<Option<Cache>>,
|
||||
}
|
||||
|
||||
impl FileSystemMatcher {
|
||||
#[cfg(unix)]
|
||||
pub fn new(fs_text: String) -> Self {
|
||||
Self {
|
||||
fs_text,
|
||||
cache: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub fn new(_fs_text: String) -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher for FileSystemMatcher {
|
||||
#[cfg(unix)]
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
match get_file_system_type(file_info.path(), &self.cache) {
|
||||
Ok(result) => result == self.fs_text,
|
||||
Err(_) => {
|
||||
writeln!(
|
||||
&mut stderr(),
|
||||
"Error getting filesystem type for {}",
|
||||
file_info.path().to_string_lossy()
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_fs_matcher() {
|
||||
use crate::find::{
|
||||
matchers::{
|
||||
fs::{get_file_system_type, Cache},
|
||||
tests::get_dir_entry_for,
|
||||
Matcher,
|
||||
},
|
||||
tests::FakeDependencies,
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
use std::fs::File;
|
||||
use tempfile::Builder;
|
||||
|
||||
let deps = FakeDependencies::new();
|
||||
let mut matcher_io = deps.new_matcher_io();
|
||||
|
||||
// create temp file and get its fs type
|
||||
// We pass this file and the corresponding file system type into the Matcher for comparison.
|
||||
let temp_dir = Builder::new().prefix("fs_matcher").tempdir().unwrap();
|
||||
let foo_path = temp_dir.path().join("foo");
|
||||
let _ = File::create(foo_path).expect("create temp file");
|
||||
let file_info = get_dir_entry_for(&temp_dir.path().to_string_lossy(), "foo");
|
||||
|
||||
// create an empty cache for initial fs type lookup
|
||||
let empty_cache = RefCell::new(None);
|
||||
let target_fs_type = get_file_system_type(file_info.path(), &empty_cache).unwrap();
|
||||
|
||||
// should work with unmatched cache, and the cache should be set to the last query result
|
||||
let unmatched_cache = RefCell::new(Some(Cache {
|
||||
dev_id: "foo".to_string(),
|
||||
fs_type: "bar".to_string(),
|
||||
}));
|
||||
let target_fs_type_unmatched_cache =
|
||||
get_file_system_type(file_info.path(), &unmatched_cache).unwrap();
|
||||
assert_eq!(
|
||||
target_fs_type, target_fs_type_unmatched_cache,
|
||||
"get_file_system_type should return correct result with unmatched cache"
|
||||
);
|
||||
assert_eq!(
|
||||
unmatched_cache.borrow().as_ref().unwrap().fs_type,
|
||||
target_fs_type,
|
||||
"get_file_system_type should set the cache to the last query result"
|
||||
);
|
||||
|
||||
// should match fs type
|
||||
let matcher = super::FileSystemMatcher::new(target_fs_type.clone());
|
||||
assert!(
|
||||
matcher.matches(&file_info, &mut matcher_io),
|
||||
"{} should match {}",
|
||||
file_info.path().to_string_lossy(),
|
||||
target_fs_type
|
||||
);
|
||||
|
||||
// should not match fs type
|
||||
let matcher = super::FileSystemMatcher::new(target_fs_type.clone() + "foo");
|
||||
assert!(
|
||||
!matcher.matches(&file_info, &mut matcher_io),
|
||||
"{} should not match {}",
|
||||
file_info.path().to_string_lossy(),
|
||||
target_fs_type
|
||||
);
|
||||
}
|
||||
}
|
||||
+21
-31
@@ -112,7 +112,7 @@ fn extract_bracket_expr(pattern: &str) -> Option<(String, &str)> {
|
||||
}
|
||||
|
||||
/// Converts a POSIX glob into a POSIX Basic Regular Expression
|
||||
fn glob_to_regex(pattern: &str) -> Option<String> {
|
||||
fn glob_to_regex(pattern: &str) -> String {
|
||||
let mut regex = String::new();
|
||||
|
||||
let mut chars = pattern.chars();
|
||||
@@ -130,9 +130,9 @@ fn glob_to_regex(pattern: &str) -> Option<String> {
|
||||
// If pattern ends with an unescaped <backslash>, fnmatch() shall return a
|
||||
// non-zero value (indicating either no match or an error).
|
||||
//
|
||||
// Most implementations return FNM_NOMATCH in this case, so create a pattern that
|
||||
// Most implementations return FNM_NOMATCH in this case, so return a regex that
|
||||
// never matches.
|
||||
return None;
|
||||
return "$.".to_string();
|
||||
}
|
||||
}
|
||||
'[' => {
|
||||
@@ -147,12 +147,12 @@ fn glob_to_regex(pattern: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
Some(regex)
|
||||
regex
|
||||
}
|
||||
|
||||
/// An fnmatch()-style glob matcher.
|
||||
pub struct Pattern {
|
||||
regex: Option<Regex>,
|
||||
regex: Regex,
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
@@ -165,13 +165,13 @@ impl Pattern {
|
||||
};
|
||||
|
||||
// As long as glob_to_regex() is correct, this should never fail
|
||||
let regex = glob_to_regex(pattern).map(|r| parse_bre(&r, options).unwrap());
|
||||
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.as_ref().is_some_and(|r| r.is_match(string))
|
||||
self.regex.is_match(string)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,52 +179,47 @@ impl Pattern {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[track_caller]
|
||||
fn assert_glob_regex(glob: &str, regex: &str) {
|
||||
assert_eq!(glob_to_regex(glob).as_deref(), Some(regex));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literals() {
|
||||
assert_glob_regex(r"foo.bar", r"foo\.bar");
|
||||
assert_eq!(glob_to_regex(r"foo.bar"), r"foo\.bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regex_special() {
|
||||
assert_glob_regex(r"^foo.bar$", r"\^foo\.bar\$");
|
||||
assert_eq!(glob_to_regex(r"^foo.bar$"), r"\^foo\.bar\$");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wildcards() {
|
||||
assert_glob_regex(r"foo?bar*baz", r"foo.bar.*baz");
|
||||
assert_eq!(glob_to_regex(r"foo?bar*baz"), r"foo.bar.*baz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escapes() {
|
||||
assert_glob_regex(r"fo\o\?bar\*baz\\", r"foo?bar\*baz\\");
|
||||
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_glob_regex(r"foo[bar][!baz]", r"foo[bar][^baz]");
|
||||
assert_eq!(glob_to_regex(r"foo[bar][!baz]"), r"foo[bar][^baz]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_brackets() {
|
||||
assert_glob_regex(
|
||||
r"[!]!.*[\[.].][=]=][:space:]-]",
|
||||
r"[^]!.*[\[.].][=]=][:space:]-]",
|
||||
assert_eq!(
|
||||
glob_to_regex(r"[!]!.*[\[.].][=]=][:space:]-]"),
|
||||
r"[^]!.*[\[.].][=]=][:space:]-]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_brackets() {
|
||||
assert_glob_regex(r"foo[bar[!baz", r"foo\[bar\[!baz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_escape() {
|
||||
assert_eq!(glob_to_regex(r"foo\"), None);
|
||||
assert_eq!(glob_to_regex(r"foo[bar[!baz"), r"foo\[bar\[!baz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -240,9 +235,4 @@ mod tests {
|
||||
|
||||
assert!(!Pattern::new(r"foo*BAR", true).matches("BAR--foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_escape_matches() {
|
||||
assert!(!Pattern::new(r"foo\", false).matches("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
// This file is part of the uutils findutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use super::{ComparableValue, Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
#[cfg(unix)]
|
||||
use nix::unistd::Group;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
pub struct GroupMatcher {
|
||||
gid: ComparableValue,
|
||||
}
|
||||
|
||||
impl GroupMatcher {
|
||||
#[cfg(unix)]
|
||||
pub fn from_group_name(group: &str) -> Option<Self> {
|
||||
// get gid from group name
|
||||
let group = Group::from_name(group).ok()??;
|
||||
let gid = group.gid.as_raw();
|
||||
Some(Self::from_gid(gid))
|
||||
}
|
||||
|
||||
pub fn from_gid(gid: u32) -> Self {
|
||||
Self::from_comparable(ComparableValue::EqualTo(gid as u64))
|
||||
}
|
||||
|
||||
pub fn from_comparable(gid: ComparableValue) -> Self {
|
||||
Self { gid }
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn from_group_name(_group: &str) -> Option<Self> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher for GroupMatcher {
|
||||
#[cfg(unix)]
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
match file_info.metadata() {
|
||||
Ok(metadata) => self.gid.matches(metadata.gid().into()),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
// The user group acquisition function for Windows systems is not implemented in MetadataExt,
|
||||
// so it is somewhat difficult to implement it. :(
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NoGroupMatcher {}
|
||||
|
||||
impl Matcher for NoGroupMatcher {
|
||||
#[cfg(unix)]
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
use nix::unistd::Gid;
|
||||
|
||||
if file_info.path().is_symlink() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Ok(metadata) = file_info.metadata() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let Ok(gid) = Group::from_gid(Gid::from_raw(metadata.gid())) else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let Some(_group) = gid else {
|
||||
return true;
|
||||
};
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_group_matcher() {
|
||||
use crate::find::matchers::{group::GroupMatcher, tests::get_dir_entry_for, Matcher};
|
||||
use crate::find::tests::FakeDependencies;
|
||||
use chrono::Local;
|
||||
use nix::unistd::{Gid, Group};
|
||||
use std::fs::File;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use tempfile::Builder;
|
||||
|
||||
let deps = FakeDependencies::new();
|
||||
let mut matcher_io = deps.new_matcher_io();
|
||||
|
||||
let temp_dir = Builder::new().prefix("group_matcher").tempdir().unwrap();
|
||||
let foo_path = temp_dir.path().join("foo");
|
||||
let _ = File::create(foo_path).expect("create temp file");
|
||||
let file_info = get_dir_entry_for(&temp_dir.path().to_string_lossy(), "foo");
|
||||
let file_gid = file_info.metadata().unwrap().gid();
|
||||
let file_group = Group::from_gid(Gid::from_raw(file_gid))
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.name;
|
||||
|
||||
let matcher =
|
||||
super::GroupMatcher::from_group_name(file_group.as_str()).expect("group should exist");
|
||||
assert!(
|
||||
matcher.matches(&file_info, &mut matcher_io),
|
||||
"group should match"
|
||||
);
|
||||
|
||||
// Testing a non-existent group name
|
||||
let time_string = Local::now().format("%Y%m%d%H%M%S").to_string();
|
||||
let matcher = GroupMatcher::from_group_name(time_string.as_str());
|
||||
assert!(
|
||||
matcher.is_none(),
|
||||
"group name {} should not exist",
|
||||
time_string
|
||||
);
|
||||
|
||||
// Testing group id
|
||||
let matcher = GroupMatcher::from_gid(file_gid);
|
||||
assert!(
|
||||
matcher.matches(&file_info, &mut matcher_io),
|
||||
"group id should match"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,12 @@
|
||||
use std::io::{stderr, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::glob::Pattern;
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
use walkdir::DirEntry;
|
||||
|
||||
fn read_link_target(file_info: &WalkEntry) -> Option<PathBuf> {
|
||||
use super::glob::Pattern;
|
||||
use super::{Matcher, MatcherIO};
|
||||
|
||||
fn read_link_target(file_info: &DirEntry) -> Option<PathBuf> {
|
||||
match file_info.path().read_link() {
|
||||
Ok(target) => Some(target),
|
||||
Err(err) => {
|
||||
@@ -38,14 +40,14 @@ pub struct LinkNameMatcher {
|
||||
}
|
||||
|
||||
impl LinkNameMatcher {
|
||||
pub fn new(pattern_string: &str, caseless: bool) -> Self {
|
||||
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: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
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 {
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
//! to "-foo -o ( -bar -baz )", not "( -foo -o -bar ) -baz").
|
||||
use std::error::Error;
|
||||
use std::path::Path;
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
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
|
||||
@@ -32,7 +33,7 @@ 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: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
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;
|
||||
@@ -51,15 +52,15 @@ impl Matcher for AndMatcher {
|
||||
.any(super::Matcher::has_side_effects)
|
||||
}
|
||||
|
||||
fn finished_dir(&self, dir: &Path, matcher_io: &mut MatcherIO) {
|
||||
fn finished_dir(&self, dir: &Path) {
|
||||
for m in &self.submatchers {
|
||||
m.finished_dir(dir, matcher_io);
|
||||
m.finished_dir(dir);
|
||||
}
|
||||
}
|
||||
|
||||
fn finished(&self, matcher_io: &mut MatcherIO) {
|
||||
fn finished(&self) {
|
||||
for m in &self.submatchers {
|
||||
m.finished(matcher_io);
|
||||
m.finished();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,7 +109,7 @@ 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: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
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;
|
||||
@@ -127,15 +128,15 @@ impl Matcher for OrMatcher {
|
||||
.any(super::Matcher::has_side_effects)
|
||||
}
|
||||
|
||||
fn finished_dir(&self, dir: &Path, matcher_io: &mut MatcherIO) {
|
||||
fn finished_dir(&self, dir: &Path) {
|
||||
for m in &self.submatchers {
|
||||
m.finished_dir(dir, matcher_io);
|
||||
m.finished_dir(dir);
|
||||
}
|
||||
}
|
||||
|
||||
fn finished(&self, matcher_io: &mut MatcherIO) {
|
||||
fn finished(&self) {
|
||||
for m in &self.submatchers {
|
||||
m.finished(matcher_io);
|
||||
m.finished();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,7 +206,7 @@ impl ListMatcher {
|
||||
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: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
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);
|
||||
@@ -222,15 +223,15 @@ impl Matcher for ListMatcher {
|
||||
.any(super::Matcher::has_side_effects)
|
||||
}
|
||||
|
||||
fn finished_dir(&self, dir: &Path, matcher_io: &mut MatcherIO) {
|
||||
fn finished_dir(&self, dir: &Path) {
|
||||
for m in &self.submatchers {
|
||||
m.finished_dir(dir, matcher_io);
|
||||
m.finished_dir(dir);
|
||||
}
|
||||
}
|
||||
|
||||
fn finished(&self, matcher_io: &mut MatcherIO) {
|
||||
fn finished(&self) {
|
||||
for m in &self.submatchers {
|
||||
m.finished(matcher_io);
|
||||
m.finished();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,7 +311,7 @@ impl ListMatcherBuilder {
|
||||
pub struct TrueMatcher;
|
||||
|
||||
impl Matcher for TrueMatcher {
|
||||
fn matches(&self, _dir_entry: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, _dir_entry: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -319,7 +320,7 @@ impl Matcher for TrueMatcher {
|
||||
pub struct FalseMatcher;
|
||||
|
||||
impl Matcher for FalseMatcher {
|
||||
fn matches(&self, _dir_entry: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, _dir_entry: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -338,7 +339,7 @@ impl NotMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for NotMatcher {
|
||||
fn matches(&self, dir_entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
!self.submatcher.matches(dir_entry, matcher_io)
|
||||
}
|
||||
|
||||
@@ -346,16 +347,17 @@ impl Matcher for NotMatcher {
|
||||
self.submatcher.has_side_effects()
|
||||
}
|
||||
|
||||
fn finished_dir(&self, dir: &Path, matcher_io: &mut MatcherIO) {
|
||||
self.submatcher.finished_dir(dir, matcher_io);
|
||||
fn finished_dir(&self, dir: &Path) {
|
||||
self.submatcher.finished_dir(dir);
|
||||
}
|
||||
|
||||
fn finished(&self, matcher_io: &mut MatcherIO) {
|
||||
self.submatcher.finished(matcher_io);
|
||||
fn finished(&self) {
|
||||
self.submatcher.finished();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::find::matchers::quit::QuitMatcher;
|
||||
@@ -368,7 +370,7 @@ mod tests {
|
||||
pub struct HasSideEffects;
|
||||
|
||||
impl Matcher for HasSideEffects {
|
||||
fn matches(&self, _: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -381,7 +383,7 @@ mod tests {
|
||||
struct Counter(Rc<RefCell<u32>>);
|
||||
|
||||
impl Matcher for Counter {
|
||||
fn matches(&self, _: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
*self.0.borrow_mut() += 1;
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
// This file is part of the uutils findutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use chrono::DateTime;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{stderr, Write},
|
||||
};
|
||||
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
#[cfg(unix)]
|
||||
fn format_permissions(mode: uucore::libc::mode_t) -> String {
|
||||
let file_type = match mode & (uucore::libc::S_IFMT as uucore::libc::mode_t) {
|
||||
uucore::libc::S_IFDIR => "d",
|
||||
uucore::libc::S_IFREG => "-",
|
||||
_ => "?",
|
||||
};
|
||||
|
||||
// S_$$USR means "user permissions"
|
||||
let user_perms = format!(
|
||||
"{}{}{}",
|
||||
if mode & uucore::libc::S_IRUSR != 0 {
|
||||
"r"
|
||||
} else {
|
||||
"-"
|
||||
},
|
||||
if mode & uucore::libc::S_IWUSR != 0 {
|
||||
"w"
|
||||
} else {
|
||||
"-"
|
||||
},
|
||||
if mode & uucore::libc::S_IXUSR != 0 {
|
||||
"x"
|
||||
} else {
|
||||
"-"
|
||||
}
|
||||
);
|
||||
|
||||
// S_$$GRP means "group permissions"
|
||||
let group_perms = format!(
|
||||
"{}{}{}",
|
||||
if mode & uucore::libc::S_IRGRP != 0 {
|
||||
"r"
|
||||
} else {
|
||||
"-"
|
||||
},
|
||||
if mode & uucore::libc::S_IWGRP != 0 {
|
||||
"w"
|
||||
} else {
|
||||
"-"
|
||||
},
|
||||
if mode & uucore::libc::S_IXGRP != 0 {
|
||||
"x"
|
||||
} else {
|
||||
"-"
|
||||
}
|
||||
);
|
||||
|
||||
// S_$$OTH means "other permissions"
|
||||
let other_perms = format!(
|
||||
"{}{}{}",
|
||||
if mode & uucore::libc::S_IROTH != 0 {
|
||||
"r"
|
||||
} else {
|
||||
"-"
|
||||
},
|
||||
if mode & uucore::libc::S_IWOTH != 0 {
|
||||
"w"
|
||||
} else {
|
||||
"-"
|
||||
},
|
||||
if mode & uucore::libc::S_IXOTH != 0 {
|
||||
"x"
|
||||
} else {
|
||||
"-"
|
||||
}
|
||||
);
|
||||
|
||||
format!("{}{}{}{}", file_type, user_perms, group_perms, other_perms)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn format_permissions(file_attributes: u32) -> String {
|
||||
let mut attributes = Vec::new();
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
|
||||
if file_attributes & 0x0001 != 0 {
|
||||
attributes.push("read-only");
|
||||
}
|
||||
if file_attributes & 0x0002 != 0 {
|
||||
attributes.push("hidden");
|
||||
}
|
||||
if file_attributes & 0x0004 != 0 {
|
||||
attributes.push("system");
|
||||
}
|
||||
if file_attributes & 0x0020 != 0 {
|
||||
attributes.push("archive");
|
||||
}
|
||||
if file_attributes & 0x0040 != 0 {
|
||||
attributes.push("compressed");
|
||||
}
|
||||
if file_attributes & 0x0080 != 0 {
|
||||
attributes.push("offline");
|
||||
}
|
||||
|
||||
attributes.join(", ")
|
||||
}
|
||||
|
||||
pub struct Ls {
|
||||
output_file: Option<File>,
|
||||
}
|
||||
|
||||
impl Ls {
|
||||
pub fn new(output_file: Option<File>) -> Self {
|
||||
Self { output_file }
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn print(
|
||||
&self,
|
||||
file_info: &WalkEntry,
|
||||
matcher_io: &mut MatcherIO,
|
||||
mut out: impl Write,
|
||||
print_error_message: bool,
|
||||
) {
|
||||
use nix::unistd::{Gid, Group, Uid, User};
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||
|
||||
let metadata = file_info.metadata().unwrap();
|
||||
|
||||
let inode_number = metadata.ino();
|
||||
let number_of_blocks = {
|
||||
let size = metadata.size();
|
||||
let number_of_blocks = size / 1024;
|
||||
let remainder = number_of_blocks % 4;
|
||||
|
||||
if remainder == 0 {
|
||||
if number_of_blocks == 0 {
|
||||
4
|
||||
} else {
|
||||
number_of_blocks
|
||||
}
|
||||
} else {
|
||||
number_of_blocks + (4 - (remainder))
|
||||
}
|
||||
};
|
||||
let permission =
|
||||
{ format_permissions(metadata.permissions().mode() as uucore::libc::mode_t) };
|
||||
let hard_links = metadata.nlink();
|
||||
let user = {
|
||||
let uid = metadata.uid();
|
||||
User::from_uid(Uid::from_raw(uid)).unwrap().unwrap().name
|
||||
};
|
||||
let group = {
|
||||
let gid = metadata.gid();
|
||||
Group::from_gid(Gid::from_raw(gid)).unwrap().unwrap().name
|
||||
};
|
||||
let size = metadata.size();
|
||||
let last_modified = {
|
||||
let system_time = metadata.modified().unwrap();
|
||||
let now_utc: DateTime<chrono::Utc> = system_time.into();
|
||||
now_utc.format("%b %e %H:%M")
|
||||
};
|
||||
let path = file_info.path().to_string_lossy();
|
||||
|
||||
match writeln!(
|
||||
out,
|
||||
" {:<4} {:>6} {:<10} {:>3} {:<8} {:<8} {:>8} {} {}",
|
||||
inode_number,
|
||||
number_of_blocks,
|
||||
permission,
|
||||
hard_links,
|
||||
user,
|
||||
group,
|
||||
size,
|
||||
last_modified,
|
||||
path,
|
||||
) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
if print_error_message {
|
||||
writeln!(
|
||||
&mut stderr(),
|
||||
"Error writing {:?} for {}",
|
||||
file_info.path().to_string_lossy(),
|
||||
e
|
||||
)
|
||||
.unwrap();
|
||||
matcher_io.set_exit_code(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn print(
|
||||
&self,
|
||||
file_info: &WalkEntry,
|
||||
matcher_io: &mut MatcherIO,
|
||||
mut out: impl Write,
|
||||
print_error_message: bool,
|
||||
) {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
|
||||
let metadata = file_info.metadata().unwrap();
|
||||
|
||||
let inode_number = 0;
|
||||
let number_of_blocks = {
|
||||
let size = metadata.file_size();
|
||||
let number_of_blocks = size / 1024;
|
||||
let remainder = number_of_blocks % 4;
|
||||
|
||||
if remainder == 0 {
|
||||
if number_of_blocks == 0 {
|
||||
4
|
||||
} else {
|
||||
number_of_blocks
|
||||
}
|
||||
} else {
|
||||
number_of_blocks + (4 - (remainder))
|
||||
}
|
||||
};
|
||||
let permission = { format_permissions(metadata.file_attributes()) };
|
||||
let hard_links = 0;
|
||||
let user = 0;
|
||||
let group = 0;
|
||||
let size = metadata.file_size();
|
||||
let last_modified = {
|
||||
let system_time = metadata.modified().unwrap();
|
||||
let now_utc: DateTime<chrono::Utc> = system_time.into();
|
||||
now_utc.format("%b %e %H:%M")
|
||||
};
|
||||
let path = file_info.path().to_string_lossy();
|
||||
|
||||
match write!(
|
||||
out,
|
||||
" {:<4} {:>6} {:<10} {:>3} {:<8} {:<8} {:>8} {} {}\n",
|
||||
inode_number,
|
||||
number_of_blocks,
|
||||
permission,
|
||||
hard_links,
|
||||
user,
|
||||
group,
|
||||
size,
|
||||
last_modified,
|
||||
path,
|
||||
) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
if print_error_message {
|
||||
writeln!(
|
||||
&mut stderr(),
|
||||
"Error writing {:?} for {}",
|
||||
file_info.path().to_string_lossy(),
|
||||
e
|
||||
)
|
||||
.unwrap();
|
||||
matcher_io.set_exit_code(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher for Ls {
|
||||
fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
if let Some(file) = &self.output_file {
|
||||
self.print(file_info, matcher_io, file, true);
|
||||
} else {
|
||||
self.print(
|
||||
file_info,
|
||||
matcher_io,
|
||||
&mut *matcher_io.deps.get_output().borrow_mut(),
|
||||
false,
|
||||
);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn has_side_effects(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_format_permissions() {
|
||||
use super::format_permissions;
|
||||
|
||||
let mode: uucore::libc::mode_t = 0o100644;
|
||||
let expected = "-rw-r--r--";
|
||||
assert_eq!(format_permissions(mode), expected);
|
||||
|
||||
let mode: uucore::libc::mode_t = 0o040755;
|
||||
let expected = "drwxr-xr-x";
|
||||
assert_eq!(format_permissions(mode), expected);
|
||||
|
||||
let mode: uucore::libc::mode_t = 0o100777;
|
||||
let expected = "-rwxrwxrwx";
|
||||
assert_eq!(format_permissions(mode), expected);
|
||||
}
|
||||
}
|
||||
+127
-676
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user