Author SHA1 Message Date
codspeed-hq[bot]andGitHub c9f88f0fa2 Fix import ordering to match CI stable toolchain 2026-06-04 19:50:28 +00:00
codspeed-hq[bot]andGitHub 0faed0baeb Add CodSpeed performance benchmarks 2026-06-04 19:44:08 +00:00
ValentinandGitHub 4986a8460c Absolute Paths Support for Archives Creation (#291)
Added support for -P and --absolute-names flags to preserve absolute
paths when creating archives, matching GNU tar behavior.

* CLI: Added -P / --absolute-names options to uu_app.
* Operations: Configured tar::Builder to preserve absolute paths
  based on the new flag.
* Normalization: Updated normalize_path to skip stripping root
  and prefix components when absolute paths are allowed.
* Tests: Added integration tests for path preservation and trimming
  during archive creation and extraction.
2026-06-01 20:53:03 +01:00
Sylvestre Ledru 949437fe85 Add CONTRIBUTING.md pointing to the review guidelines 2026-05-30 10:07:54 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fac25e3b7d chore(deps): bump tar from 0.4.45 to 0.4.46 in /fuzz (#293)
Bumps [tar](https://github.com/composefs/tar-rs) from 0.4.45 to 0.4.46.
- [Release notes](https://github.com/composefs/tar-rs/releases)
- [Commits](https://github.com/composefs/tar-rs/compare/0.4.45...0.4.46)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 0.4.46
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-29 23:24:55 +01:00
renovate[bot]andDaniel Hofstetter 84c09e354f chore(deps): update rust crate ctor to v1.0.7 2026-05-29 08:26:19 +02:00
Sylvestre LedruandGitHub 82a4980a70 Merge pull request #287 from kevinburke/gnu-clone-retry
util: retry GNU tar setup downloads
2026-05-26 09:57:04 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3dd83a7b69 chore(deps): update rust crate similar to v3.1.1 (#290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-24 05:47:25 +00:00
renovate[bot]andDaniel Hofstetter 28ab4a8e2e chore(deps): update rust crate tar to v0.4.46 2026-05-19 07:13:57 +02:00
Kevin Burke b4865dab24 util: retry GNU tar setup downloads
The GNU tar CI job depends on git.savannah.gnu.org, which can fail
transiently during clone or submodule setup. Retry those network-dependent
setup steps with exponential backoff before failing the job.

Keep cleanup scoped to failed clone attempts, fail early if path_GNU already
points at a non-git directory, and tighten the helper script with pipefail and
shellcheck-clean quoting.
2026-05-18 08:46:15 -07:00
Sylvestre LedruandGitHub 2cc665e2d3 Merge pull request #286 from kaladron/codeowner
Add CODEOWNERS file to request review from @kaladron
2026-05-17 14:44:30 +02:00
Jeff BaileyandGitHub fc74817b9b feat: add CODEOWNERS file to request review from @kaladron 2026-05-17 12:37:36 +00:00
42f9d3722f remove 1 unsafe (#284)
Co-authored-by: Jeff Bailey <jbailey@raspberryginger.com>
2026-05-17 08:53:39 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
74abcb3428 chore(deps): update rust crate ctor to v1.0.6 (#285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-17 08:46:29 +01:00
Sylvestre LedruandGitHub 840b16bd8f Merge pull request #163 from kaladron/fast-t
perf: use BufWriter<StdoutLock> for all verbose/listing output
2026-05-13 12:19:46 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
cc2f4f1015 chore(deps): update rust crate ctor to v1.0.5 (#282)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 22:44:56 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3a7c125a1b chore(deps): update rust crate clap_complete to v4.6.5 (#283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 22:44:30 +01:00
renovate[bot]andDaniel Hofstetter c5cd4bba76 chore(deps): update rust crate clap_complete to v4.6.4 2026-05-10 10:10:16 +02:00
renovate[bot]andDaniel Hofstetter ff0bba6fd6 chore(deps): update rust crate ctor to v1.0.4 2026-05-09 07:10:01 +02:00
renovate[bot]andDaniel Hofstetter c39eb3aae5 chore(deps): update rust crate ctor to v1.0.3 2026-05-07 15:07:44 +02:00
renovate[bot]andDaniel Hofstetter a0b003c74c chore(deps): update rust crate ctor to v1.0.2 2026-05-07 09:46:13 +02:00
Daniel Hofstetter 60918b50b1 tests.rs: adapt to ctor change 2026-05-07 09:38:09 +02:00
Daniel Hofstetter e94d74614e Bump ctor from 0.12.0 to 1.0.1 2026-05-07 09:38:09 +02:00
renovate[bot]andDaniel Hofstetter 1afcb44928 chore(deps): update rust crate ctor to 0.12.0 2026-05-01 07:35:16 +02:00
renovate[bot]andDaniel Hofstetter 9ade0975c3 chore(deps): update rust crate clap_complete to v4.6.3 2026-04-30 07:18:12 +02:00
renovate[bot]andDaniel Hofstetter d7a6d46b31 chore(deps): update rust crate ctor to 0.11.0 2026-04-30 07:17:42 +02:00
renovate[bot]andDaniel Hofstetter e6e00783c5 chore(deps): update rust crate zip to v8.6.0 2026-04-30 07:14:09 +02:00
renovate[bot]andDaniel Hofstetter 1368c86eb9 chore(deps): update dawidd6/action-download-artifact action to v21 2026-04-30 07:13:01 +02:00
renovate[bot]andDaniel Hofstetter f6b4dc4f2f chore(deps): update mozilla-actions/sccache-action action to v0.0.10 2026-04-23 07:32:32 +02:00
renovate[bot]andDaniel Hofstetter f96a65de73 chore(deps): update rust crate ctor to v0.10.1 2026-04-23 07:17:43 +02:00
renovate[bot]andDaniel Hofstetter 3236dd7b87 chore(deps): update rust crate clap to v4.6.1 2026-04-16 07:15:12 +02:00
renovate[bot]andDaniel Hofstetter ee6b9f1e7f chore(deps): update rust crate clap_complete to v4.6.2 2026-04-16 07:13:35 +02:00
renovate[bot]andDaniel Hofstetter d5f100f38b chore(deps): update rust crate ctor to 0.10.0 2026-04-13 07:12:00 +02:00
xtqqczzeandGitHub b27553a5f2 actions: add security audit workflow (#258) 2026-04-12 11:28:43 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
23855c363b chore(deps): update softprops/action-gh-release action to v3 (#260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 08:41:08 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
44f2ebbe32 chore(deps): update rust crate similar to v3.1.0 (#257)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-11 22:27:59 +01:00
xtqqczzeandDaniel Hofstetter 2aa37f71d0 chore(deps): update rust crates 2026-04-11 16:18:33 +02:00
xtqqczzeandDaniel Hofstetter d24f17f067 fix: update clippy command to use workspace option 2026-04-11 16:12:10 +02:00
xtqqczzeandDaniel Hofstetter d281a396af clippy: fix io_other_error lint
https://rust-lang.github.io/rust-clippy/rust-1.94.0/index.html#io_other_error
2026-04-11 16:12:10 +02:00
renovate[bot]andDaniel Hofstetter 3ea2f51c91 chore(deps): update rust crate rand to v0.10.1 2026-04-11 15:22:30 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
0778f2c62b chore(deps): update rust crate clap_complete to v4.6.1 (#253)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-10 23:21:49 +01:00
Jeff BaileyandGitHub 78c666ac7d tar: fix CI coverage for spawned tar tests (#164)
Make the coverage job include the full workspace and collect
coverage from spawned CLI test runs. This prevents exercised
tar paths from showing up as uncovered.
2026-04-10 06:49:55 +01:00
renovate[bot]andDaniel Hofstetter 819b471288 chore(deps): update actions/github-script action to v9 2026-04-10 07:03:23 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
fbdef22a7e chore(deps): update rust crate ctor to v0.9.1 (#166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 23:37:45 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4eb3c53302 chore(deps): update rust crate ctor to 0.9.0 (#165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 19:05:22 +01:00
Jeff BaileyandGitHub b469a73940 perf: use BufWriter<StdoutLock> for all verbose/listing output
Replace println! in list, create, and extract operations with
writeln! on a BufWriter<StdoutLock>. This acquires stdout's mutex
once per operation instead of once per write call, and batches
writes to reduce write(2) syscalls.

Also propagates write errors (e.g. broken pipe) gracefully instead
of panicking.
2026-04-08 20:15:33 +00:00
renovate[bot]andDaniel Hofstetter 30537acf7d chore(deps): update rust crate zip to v8.5.1 2026-04-07 11:56:50 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
c54ceb6752 fix(deps): update rust crate uufuzz to 0.8.0 (#161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-06 23:56:09 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4ad5dd1a80 fix(deps): update rust crate uucore to 0.8.0 (#160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-06 23:53:42 +01:00
Jeff BaileyandDaniel Hofstetter b760338e9d ci: run coverage tests for full workspace 2026-04-05 16:03:35 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
7c00028cfb chore(deps): update rust crate thiserror to v2.0.18 (#155)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-04 23:53:36 +01:00
Jeff BaileyandGitHub f541b90a9c tar: replace all string-based error variants with structured types (#149)
* tar: replace all string-based error variants with structured types

Every TarError variant now carries PathBuf and/or io::Error data
instead of a pre-formatted String. Add thiserror derive macros so
Display implementations are generated from #[error(...)] attributes
in one central place. Eliminates the last generic TarOperation and
InvalidArchive string variants.

Add tar prefixes, update tests to match.
2026-04-04 23:47:25 +01:00
Jeff BaileyandGitHub b7c0578352 Merge pull request #153 from cakebaker/ci_rename_profile_file
ci: rename LLVM profile file
2026-04-04 13:35:57 +01:00
Daniel Hofstetter 2c7a5e62e2 ci: rename LLVM profile file 2026-04-04 13:57:05 +02:00
Jeff BaileyandDaniel Hofstetter a9e1228b5e tar: install llvm-tools before running grcov
Add a CI step to run `rustup component add llvm-tools` before
generating coverage data. grcov requires llvm-profdata (part of
llvm-tools) to process .profraw files; without it the coverage
job fails with "couldn't find llvm-profdata".
2026-04-04 13:48:36 +02:00
Jeff BaileyandDaniel Hofstetter fd44e1ef17 tar: use "tar (uutils)" as the display name in clap
Replace `uucore::util_name()` (argv[0]) with the literal string
"tar (uutils)" as the clap Command name.  This gives a consistent,
recognisable program name in --help output regardless of what the binary was invoked as.

Closes #150
2026-04-04 13:48:36 +02:00
Jeff BaileyandGitHub 65cd987c74 Merge pull request #137 from kevinburkesegment/fix-gnu-test-badversion
Fix GNU test suite reporting real pass/fail results
2026-04-03 20:29:54 +01:00
Jeff BaileyandGitHub 8da3aeecc9 Merge pull request #147 from oech3/patch-1
Override latest-commit's body with corresponding commit
2026-04-03 20:27:48 +01:00
Jeff BaileyandGitHub 29712f49cf Merge pull request #146 from cakebaker/ci_fix_coverage
ci: fix coverage
2026-04-03 20:15:48 +01:00
oech3 1fe8bc1d5a Override latest-commit's body with corresponding commit 2026-04-03 17:41:38 +09:00
8d1d0f56b7 README.md: update Installation section (#145)
* README.md: update Installation section

* README.md: fix description

Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>

---------

Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>
2026-04-03 10:39:29 +02:00
Daniel Hofstetter 18fa3ee18e ci: fix coverage 2026-04-03 10:36:55 +02:00
Jeff BaileyandDaniel Hofstetter 06042058f9 tar: add -t/--list operation
Implement the list operation. Non-verbose (-t) prints one entry path
per line. Verbose (-tv) prints permissions, owner/group, size,
modification time, and path — matching GNU tar's ls-l style output.

Adds conflict group so -c, -x, and -t are mutually exclusive.

Pulls in chrono for timestamp formatting in verbose output. chrono is
already in the workspace dependency set and will also be needed for
date parsing when --newer=DATE is implemented.

Point uucore/uutests at the coreutils git repo while we're iterating
quickly on basic functionality.
2026-04-03 10:13:30 +02:00
Kevin BurkeandClaude Opus 4.6 e077e08aa5 Fix GNU test suite reporting real pass/fail results
The GNU tar test suite's version.at (test 1) creates a .badversion
file when `tar --version` doesn't match the expected GNU tar output.
This causes AT_XFAIL_IF to mark every subsequent test as "expected
failure", hiding real results — all tests appear as XFAIL instead of
actual PASS or FAIL.

Fix this by:
- Generating the testsuite script first via `make -C tests testsuite`
- Patching it to redirect the .badversion write to /dev/null
- Removing any stale .badversion before running
- Touching src/tar after copy to prevent make from relinking it

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 10:05:46 -07:00
26 changed files with 1720 additions and 851 deletions
+2
View File
@@ -0,0 +1,2 @@
# Request review from the repository-wide code owner on pull requests.
* @kaladron
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
github.event.workflow_run.event == 'pull_request'
steps:
- name: 'Download artifact'
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
@@ -46,7 +46,7 @@ jobs:
run: unzip comment.zip
- name: 'Comment on PR'
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
+4 -2
View File
@@ -51,10 +51,12 @@ jobs:
strip -s target/release/tarapp
zstd -19 target/release/tarapp -o ../tar-x86_64-unknown-linux-gnu.zst
- name: Publish latest commit
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
tag_name: latest-commit
body: |
commit: ${{ github.sha }}
draft: false
prerelease: true
files: |
@@ -109,7 +111,7 @@ jobs:
persist-credentials: false
- name: Retrieve reference artifacts
uses: dawidd6/action-download-artifact@v20
uses: dawidd6/action-download-artifact@v21
continue-on-error: true
with:
workflow: GnuTests.yml
+15
View File
@@ -0,0 +1,15 @@
name: Security audit
# spell-checker:ignore (misc) rustsec
on:
schedule:
- cron: "0 0 * * *"
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
+11 -5
View File
@@ -53,12 +53,12 @@ jobs:
CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' )
outputs CODECOV_FLAGS
- name: Test
run: cargo test --no-fail-fast
run: cargo test --workspace --no-fail-fast
env:
RUSTC_WRAPPER: ""
RUSTFLAGS: "-Cinstrument-coverage -Zcoverage-options=branch -Ccodegen-units=1 -Copt-level=0 -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
RUSTDOCFLAGS: "-Cpanic=abort"
LLVM_PROFILE_FILE: "sed-%p-%m.profraw"
LLVM_PROFILE_FILE: "${{ github.workspace }}/tar-%p-%m.profraw"
# Use -Z
RUSTC_BOOTSTRAP: 1
- name: "`grcov` ~ install"
@@ -82,6 +82,11 @@ jobs:
# crate: grcov
# version: latest
# use-tool-cache: false
- name: "`llvm-tools` ~ install"
shell: bash
run: |
## Install the llvm-tools component to get access to `llvm-profdata`
rustup component add llvm-tools
- name: Generate coverage data (via `grcov`)
env:
# Use -Z
@@ -96,15 +101,16 @@ jobs:
# display coverage files
grcov . --binary-path="${COVERAGE_REPORT_DIR}" --output-type files --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" | sort --unique
# generate coverage report
grcov . --binary-path="${COVERAGE_REPORT_DIR}" --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()"
grcov . --binary-path="${COVERAGE_REPORT_DIR}" --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" --source-dir .
echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT
- name: Upload coverage results (to Codecov.io)
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ${{ steps.coverage.outputs.report }}
## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }}
flags: ${{ steps.vars.outputs.CODECOV_FLAGS }}
name: codecov-umbrella
fail_ci_if_error: false
verbose: true
directory: ./
root_dir: ./
+2 -2
View File
@@ -48,7 +48,7 @@ jobs:
- uses: actions/checkout@v6
- uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.9
uses: mozilla-actions/sccache-action@v0.0.10
- name: Initialize workflow variables
id: vars
shell: bash
@@ -70,5 +70,5 @@ jobs:
fault_type="${{ steps.vars.outputs.FAULT_TYPE }}"
fault_prefix=$(echo "$fault_type" | tr '[:lower:]' '[:upper:]')
# * convert any warnings to GHA UI annotations; ref: <https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message>
S=$(cargo clippy --all-targets -ptarapp -- ${CLIPPY_FLAGS} -D warnings 2>&1) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*$/::${fault_type} file=\2,line=\3,col=\4::${fault_prefix}: \`cargo clippy\`: \1 (file:'\2', line:\3)/p;" -e '}' ; fault=true ; }
S=$(cargo clippy --workspace --all-targets -ptarapp -- ${CLIPPY_FLAGS} -D warnings 2>&1) && printf "%s\n" "$S" || { printf "%s\n" "$S" ; printf "%s" "$S" | sed -E -n -e '/^error:/{' -e "N; s/^error:[[:space:]]+(.*)\\n[[:space:]]+-->[[:space:]]+(.*):([0-9]+):([0-9]+).*$/::${fault_type} file=\2,line=\3,col=\4::${fault_prefix}: \`cargo clippy\`: \1 (file:'\2', line:\3)/p;" -e '}' ; fault=true ; }
if [ -n "${{ steps.vars.outputs.FAIL_ON_FAULT }}" ] && [ -n "$fault" ]; then exit 1 ; fi
+42
View File
@@ -0,0 +1,42 @@
name: CodSpeed
on:
push:
branches:
- "main"
pull_request:
# `workflow_dispatch` allows CodSpeed to trigger backtest
# performance analysis in order to generate initial data.
workflow_dispatch:
permissions:
contents: read
id-token: write
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: "0"
jobs:
codspeed:
name: Run benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup rust toolchain
run: |
rustup toolchain install stable --profile minimal
rustup default stable
- name: Install cargo-codspeed
run: cargo install cargo-codspeed --locked
- name: Build the benchmark target(s)
run: cargo codspeed build
- name: Run the benchmarks
uses: CodSpeedHQ/action@v4
with:
mode: simulation
run: cargo codspeed run
+32
View File
@@ -0,0 +1,32 @@
# Contributing to tar
Hi! Welcome to uutils/tar, and thanks for wanting to contribute!
This project follows the shared conventions of the [uutils](https://github.com/uutils)
organization. Before opening a pull request, please read:
- Our **[Review Guidelines](https://uutils.github.io/reviews/)** — what we expect
from a pull request and how reviews are carried out.
- Our community's [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md), if present.
Finally, feel free to join our [Discord](https://discord.gg/wQVJbvJ)!
> [!WARNING]
> uutils is original code and cannot contain any code from GNU or other
> strongly-licensed (GPL/LGPL) implementations. We **cannot** accept changes
> based on the GNU source code, and you **must not link** to it either. You may
> look at permissively-licensed implementations (MIT/BSD) and read the GNU
> *manuals* — never the GNU *source*.
## In short
- Discuss non-trivial changes in an issue **before** writing the code.
- Keep pull requests **small, self-contained, and descriptively titled**
(e.g. `tar: fix ...`).
- Make sure CI passes: tests are green, `rustfmt` is satisfied, and there are
no `clippy` warnings.
- Add tests for new behavior; don't let coverage regress.
- Write small, atomic commits annotated with the component you touched.
See the [Review Guidelines](https://uutils.github.io/reviews/) for the full
details.
Generated
+614 -306
View File
File diff suppressed because it is too large Load Diff
+12 -5
View File
@@ -37,17 +37,19 @@ chrono = { version = "0.4.37", default-features = false, features = [
clap = { version = "4.4", features = ["wrap_help", "cargo"] }
clap_complete = "4.5"
clap_mangen = "0.3"
ctor = "0.8.0"
ctor = "1.0.0"
libc = "0.2.153"
phf = "0.13.0"
phf_codegen = "0.13.0"
rand = { version = "0.10.0" }
regex = "1.10.4"
uucore = "0.7.0"
uutests = "0.7.0"
rustix = "1.1.4"
tar = "0.4"
tempfile = "3.10.1"
textwrap = { version = "0.16.1", features = ["terminal_size"] }
thiserror = "2.0.3"
uucore = { git = "https://github.com/uutils/coreutils" }
uutests = { git = "https://github.com/uutils/coreutils" }
xattr = "1.3.1"
zip = "8.0"
@@ -58,18 +60,19 @@ clap_mangen = { workspace = true }
ctor = { workspace = true }
phf = { workspace = true }
textwrap = { workspace = true }
uutests = { workspace = true }
uucore = { workspace = true }
uutests = { workspace = true }
zip = { workspace = true, optional = true }
tar = { optional = true, version = "0.0.1", package = "uu_tar", path = "src/uu/tar" }
[dev-dependencies]
chrono = { workspace = true }
libc = { workspace = true }
divan = { version = "4.7.0", package = "codspeed-divan-compat" }
pretty_assertions = "1"
rand = { workspace = true }
regex = { workspace = true }
rustix = { workspace = true, features = ["process"] }
tar-rs-crate = { version = "0.4", package = "tar" }
tempfile = { workspace = true }
uucore = { workspace = true, features = ["entries", "process", "signals"] }
@@ -85,6 +88,10 @@ rlimit = "0.11.0"
phf_codegen = { workspace = true }
[[bench]]
name = "bench_tar"
harness = false
[[bin]]
name = "tarapp"
path = "src/bin/tarapp.rs"
+4 -1
View File
@@ -4,6 +4,7 @@
[![dependency status](https://deps.rs/repo/github/uutils/tar/status.svg)](https://deps.rs/repo/github/uutils/tar)
[![CodeCov](https://codecov.io/gh/uutils/tar/branch/master/graph/badge.svg)](https://codecov.io/gh/uutils/tar)
[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://app.codspeed.io/uutils/tar?utm_source=badge)
# tar
@@ -11,7 +12,9 @@ Rust reimplementation of the tar utility.
## Installation
Ensure you have Rust installed on your system. You can install Rust through [rustup](https://rustup.rs/).
We provide a [pre-built binary from main branch](https://github.com/uutils/tar/releases/tag/latest-commit).
To build tar from source, install Rust on your system. You can install Rust via [rustup](https://rustup.rs/).
Clone the repository and build the project using Cargo:
+155
View File
@@ -0,0 +1,155 @@
// Benchmarks for the uutils tar implementation.
//
// These benchmarks exercise the core archive operations (create, list, extract)
// at various scales to track performance over time.
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use tar::operations;
use tempfile::TempDir;
fn main() {
divan::main();
}
/// Create `count` files of `size` bytes each inside `dir`.
fn create_sample_files(dir: &Path, count: usize, size: usize) {
let content = vec![b'A'; size];
for i in 0..count {
let path = dir.join(format!("file_{i}.txt"));
let mut f = File::create(&path).unwrap();
f.write_all(&content).unwrap();
}
}
/// Collect all regular file paths inside `dir` (non-recursive).
fn collect_files(dir: &Path) -> Vec<PathBuf> {
let mut paths: Vec<PathBuf> = fs::read_dir(dir)
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.is_file())
.collect();
paths.sort();
paths
}
/// Build a tar archive at `archive_path` from all files in `source_dir`.
fn build_archive(archive_path: &Path, source_dir: &Path) {
let files = collect_files(source_dir);
let refs: Vec<&Path> = files.iter().map(|p| p.as_path()).collect();
operations::create::create_archive(archive_path, &refs, false, false).unwrap();
}
// ---------------------------------------------------------------------------
// Create benchmarks
// ---------------------------------------------------------------------------
#[divan::bench]
fn create_archive_10_files(bencher: divan::Bencher) {
let source = TempDir::new().unwrap();
create_sample_files(source.path(), 10, 4_096);
let files = collect_files(source.path());
let out = TempDir::new().unwrap();
let archive_path = out.path().join("bench.tar");
bencher.bench_local(|| {
let refs: Vec<&Path> = files.iter().map(|p| p.as_path()).collect();
operations::create::create_archive(&archive_path, &refs, false, false).unwrap();
});
}
#[divan::bench]
fn create_archive_100_files(bencher: divan::Bencher) {
let source = TempDir::new().unwrap();
create_sample_files(source.path(), 100, 4_096);
let files = collect_files(source.path());
let out = TempDir::new().unwrap();
let archive_path = out.path().join("bench.tar");
bencher.bench_local(|| {
let refs: Vec<&Path> = files.iter().map(|p| p.as_path()).collect();
operations::create::create_archive(&archive_path, &refs, false, false).unwrap();
});
}
#[divan::bench]
fn create_archive_directory(bencher: divan::Bencher) {
let source = TempDir::new().unwrap();
let sub = source.path().join("data");
fs::create_dir_all(&sub).unwrap();
create_sample_files(&sub, 30, 4_096);
let out = TempDir::new().unwrap();
let archive_path = out.path().join("bench.tar");
bencher.bench_local(|| {
operations::create::create_archive(&archive_path, &[sub.as_path()], false, false).unwrap();
});
}
// ---------------------------------------------------------------------------
// List benchmarks
// ---------------------------------------------------------------------------
#[divan::bench]
fn list_archive_50_files(bencher: divan::Bencher) {
let source = TempDir::new().unwrap();
create_sample_files(source.path(), 50, 4_096);
let archive_dir = TempDir::new().unwrap();
let archive_path = archive_dir.path().join("bench.tar");
build_archive(&archive_path, source.path());
bencher.bench_local(|| {
operations::list::list_archive(&archive_path, false).unwrap();
});
}
#[divan::bench]
fn list_archive_verbose_50_files(bencher: divan::Bencher) {
let source = TempDir::new().unwrap();
create_sample_files(source.path(), 50, 4_096);
let archive_dir = TempDir::new().unwrap();
let archive_path = archive_dir.path().join("bench.tar");
build_archive(&archive_path, source.path());
bencher.bench_local(|| {
operations::list::list_archive(&archive_path, true).unwrap();
});
}
// ---------------------------------------------------------------------------
// Extract benchmarks
// ---------------------------------------------------------------------------
#[divan::bench]
fn extract_archive_20_files(bencher: divan::Bencher) {
let source = TempDir::new().unwrap();
create_sample_files(source.path(), 20, 4_096);
let archive_dir = TempDir::new().unwrap();
let archive_path = archive_dir.path().join("bench.tar");
build_archive(&archive_path, source.path());
let original_dir = std::env::current_dir().unwrap();
bencher
.with_inputs(|| TempDir::new().unwrap())
.bench_local_values(|extract_dir| {
std::env::set_current_dir(extract_dir.path()).unwrap();
operations::extract::extract_archive(&archive_path, false).unwrap();
});
std::env::set_current_dir(original_dir).unwrap();
}
// ---------------------------------------------------------------------------
// CLI parsing benchmark
// ---------------------------------------------------------------------------
#[divan::bench]
fn build_cli_command(bencher: divan::Bencher) {
bencher.bench_local(|| {
divan::black_box(tar::uu_app());
});
}
+268 -375
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -14,8 +14,8 @@ libc = "0.2.153"
tempfile = "3.15.0"
rand = { version = "0.10.0" }
similar = "3.0.0"
uucore = { version = "0.7.0", features = ["libc"] }
uufuzz = "0.7.0"
uucore = { version = "0.8.0", features = ["libc"] }
uufuzz = "0.8.0"
uu_tar = { path = "../src/uu/tar/" }
+3 -1
View File
@@ -13,10 +13,12 @@ categories = ["command-line-utilities"]
[dependencies]
uucore = { workspace = true }
uucore = { workspace = true, features = ["fs"] }
clap = { workspace = true }
regex = { workspace = true }
tar = { workspace = true }
chrono = { workspace = true }
thiserror = { workspace = true }
[lib]
path = "src/tar.rs"
+53 -59
View File
@@ -3,62 +3,70 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::fmt;
use std::io;
use std::path::PathBuf;
use thiserror::Error;
use uucore::error::UError;
/// Error types for tar operations
#[derive(Debug)]
#[derive(Debug, Error)]
pub enum TarError {
/// I/O error occurred
IoError(io::Error),
/// Invalid archive format or corrupted archive
InvalidArchive(String),
/// File or directory not found
FileNotFound(String),
/// Permission denied
PermissionDenied(String),
/// General tar operation error
TarOperationError(String),
}
/// I/O error occurred while reading/writing
#[error("{0}")]
Io(#[from] io::Error),
/// Implements display formatting for TarError.
impl fmt::Display for TarError {
/// Formats the error for display to users
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
TarError::IoError(err) => write!(f, "{err}"),
TarError::InvalidArchive(msg) => write!(f, "{msg}"),
TarError::FileNotFound(path) => {
write!(f, "{path}: Cannot open: No such file or directory")
}
TarError::PermissionDenied(path) => {
write!(f, "{path}: Cannot open: Permission denied")
}
TarError::TarOperationError(msg) => write!(f, "{msg}"),
}
}
/// Cannot read entries from archive
#[error("tar: Cannot read archive entries: {0}")]
CannotReadEntries(io::Error),
/// Cannot read an individual archive entry
#[error("tar: Cannot read archive entry: {0}")]
CannotReadEntry(io::Error),
/// Cannot read the path of an archive entry
#[error("tar: Cannot read entry path: {0}")]
CannotReadEntryPath(io::Error),
/// File or directory not found
#[error("tar: {path}: Cannot open: No such file or directory")]
FileNotFound { path: PathBuf },
/// Permission denied when accessing file
#[error("tar: {path}: Cannot open: Permission denied")]
PermissionDenied { path: PathBuf },
/// Cannot create archive file
#[error("tar: Cannot create archive '{path}': {source}")]
CannotCreateArchive { path: PathBuf, source: io::Error },
/// Cannot add a directory to the archive
#[error("tar: Cannot add directory '{path}': {source}")]
CannotAddDirectory { path: PathBuf, source: io::Error },
/// Cannot add a file to the archive
#[error("tar: Cannot add file '{path}': {source}")]
CannotAddFile { path: PathBuf, source: io::Error },
/// Cannot extract an archive entry
#[error("tar: Cannot extract '{path}': {source}")]
CannotExtract { path: PathBuf, source: io::Error },
/// Cannot finalize the archive
#[error("tar: Cannot finalize archive: {0}")]
CannotFinalizeArchive(io::Error),
}
impl TarError {
/// Create a TarError from an io::Error with file path context
pub fn from_io_error(err: io::Error, path: &std::path::Path) -> Self {
match err.kind() {
io::ErrorKind::NotFound => TarError::FileNotFound(path.display().to_string()),
io::ErrorKind::PermissionDenied => {
TarError::PermissionDenied(path.display().to_string())
}
_ => TarError::IoError(err),
}
}
}
impl std::error::Error for TarError {
/// Returns the underlying error cause, if any
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
TarError::IoError(err) => Some(err),
_ => None,
io::ErrorKind::NotFound => TarError::FileNotFound {
path: path.to_path_buf(),
},
io::ErrorKind::PermissionDenied => TarError::PermissionDenied {
path: path.to_path_buf(),
},
_ => TarError::Io(err),
}
}
}
@@ -66,20 +74,6 @@ impl std::error::Error for TarError {
impl UError for TarError {
/// Returns the exit code for this error type
fn code(&self) -> i32 {
match self {
TarError::IoError(_) => 2,
TarError::InvalidArchive(_) => 2,
TarError::FileNotFound(_) => 2,
TarError::PermissionDenied(_) => 2,
TarError::TarOperationError(_) => 2,
}
}
}
impl From<io::Error> for TarError {
/// Converts io::Error into the appropriate TarError variant
fn from(err: io::Error) -> Self {
// For generic io::Error without context, just wrap it
TarError::IoError(err)
2 // TarError variants exit with code 2; argument/usage errors use code 64 (see tar.rs)
}
}
+37 -27
View File
@@ -6,6 +6,7 @@
use crate::errors::TarError;
use std::collections::VecDeque;
use std::fs::{self, File};
use std::io::{self, BufWriter, Write};
use std::path::Component::{self, ParentDir, Prefix, RootDir};
use std::path::{self, Path, PathBuf};
use tar::Builder;
@@ -17,6 +18,7 @@ use uucore::error::UResult;
///
/// * `archive_path` - Path where the tar archive should be created
/// * `files` - Slice of file paths to add to the archive
/// * `allow_absolute` - Allow absolute paths while creating archive
/// * `verbose` - Whether to print verbose output during creation
///
/// # Errors
@@ -25,24 +27,32 @@ use uucore::error::UResult;
/// - The archive file cannot be created
/// - Any input file cannot be read
/// - Files cannot be added due to I/O or permission errors
pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UResult<()> {
pub fn create_archive(
archive_path: &Path,
files: &[&Path],
allow_absolute: bool,
verbose: bool,
) -> UResult<()> {
// Create the output file
let file = File::create(archive_path).map_err(|e| {
TarError::TarOperationError(format!(
"Cannot create archive '{}': {}",
archive_path.display(),
e
))
let file = File::create(archive_path).map_err(|e| TarError::CannotCreateArchive {
path: archive_path.to_path_buf(),
source: e,
})?;
// Create Builder instance
let mut builder = Builder::new(file);
builder.preserve_absolute(allow_absolute);
let mut out = BufWriter::new(io::stdout().lock());
// Add each file or directory to the archive
for &path in files {
// Check if path exists
if !path.exists() {
return Err(TarError::FileNotFound(path.display().to_string()).into());
return Err(TarError::FileNotFound {
path: path.to_path_buf(),
}
.into());
}
if verbose {
@@ -58,11 +68,11 @@ pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UR
})
.collect::<Vec<_>>()
.join("\n");
println!("{to_print}");
writeln!(out, "{to_print}").map_err(TarError::Io)?;
}
// Normalize path if needed (so far, handles only absolute paths)
let normalized_name = if let Some(normalized) = normalize_path(path) {
let normalized_name = if let Some(normalized) = normalize_path(path, allow_absolute) {
let original_components: Vec<Component> = path.components().collect();
let normalized_components: Vec<Component> = normalized.components().collect();
if original_components.len() > normalized_components.len() {
@@ -70,7 +80,12 @@ pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UR
[..original_components.len() - normalized_components.len()]
.iter()
.collect();
println!("Removing leading `{}' from member names", removed.display());
writeln!(
out,
"Removing leading `{}' from member names",
removed.display()
)
.map_err(TarError::Io)?;
}
normalized
@@ -81,30 +96,25 @@ pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UR
// If it's a directory, recursively add all contents
if path.is_dir() {
builder.append_dir_all(normalized_name, path).map_err(|e| {
TarError::TarOperationError(format!(
"Failed to add directory '{}': {}",
path.display(),
e
))
TarError::CannotAddDirectory {
path: path.to_path_buf(),
source: e,
}
})?;
} else {
// For files, add them directly
builder
.append_path_with_name(path, normalized_name)
.map_err(|e| {
TarError::TarOperationError(format!(
"Failed to add file '{}': {}",
path.display(),
e
))
.map_err(|e| TarError::CannotAddFile {
path: path.to_path_buf(),
source: e,
})?;
}
}
// Finish writing the archive
builder
.finish()
.map_err(|e| TarError::TarOperationError(format!("Failed to finalize archive: {e}")))?;
out.flush().map_err(TarError::Io)?;
builder.finish().map_err(TarError::CannotFinalizeArchive)?;
Ok(())
}
@@ -127,8 +137,8 @@ fn get_tree(path: &Path) -> Result<Vec<PathBuf>, std::io::Error> {
Ok(paths)
}
fn normalize_path(path: &Path) -> Option<PathBuf> {
if path.is_absolute() {
fn normalize_path(path: &Path, allow_absolute: bool) -> Option<PathBuf> {
if path.is_absolute() && !allow_absolute {
Some(
path.components()
.filter(|c| !matches!(c, RootDir | ParentDir | Prefix(_)))
+11 -11
View File
@@ -5,6 +5,7 @@
use crate::errors::TarError;
use std::fs::File;
use std::io::{self, BufWriter, Write};
use std::path::Path;
use tar::Archive;
use uucore::error::UResult;
@@ -28,35 +29,34 @@ pub fn extract_archive(archive_path: &Path, verbose: bool) -> UResult<()> {
// Create Archive instance
let mut archive = Archive::new(file);
let mut out = BufWriter::new(io::stdout().lock());
// Extract to current directory
if verbose {
println!("Extracting archive: {}", archive_path.display());
writeln!(out, "Extracting archive: {}", archive_path.display()).map_err(TarError::Io)?;
}
// Iterate through entries for verbose output and error handling
for entry_result in archive
.entries()
.map_err(|e| TarError::InvalidArchive(format!("Failed to read archive entries: {e}")))?
{
let mut entry = entry_result
.map_err(|e| TarError::InvalidArchive(format!("Failed to read entry: {e}")))?;
for entry_result in archive.entries().map_err(TarError::CannotReadEntries)? {
let mut entry = entry_result.map_err(TarError::CannotReadEntry)?;
// Get the path before unpacking (clone it so we can use it after borrowing entry mutably)
let path = entry
.path()
.map_err(|e| TarError::InvalidArchive(format!("Failed to read entry path: {e}")))?
.map_err(TarError::CannotReadEntryPath)?
.to_path_buf();
if verbose {
println!("{}", path.display());
writeln!(out, "{}", path.display()).map_err(TarError::Io)?;
}
// Unpack the entry
entry.unpack_in(".").map_err(|e| {
TarError::TarOperationError(format!("Failed to extract '{}': {}", path.display(), e))
entry.unpack_in(".").map_err(|e| TarError::CannotExtract {
path: path.clone(),
source: e,
})?;
}
out.flush().map_err(TarError::Io)?;
Ok(())
}
+88
View File
@@ -0,0 +1,88 @@
// This file is part of the uutils tar package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use crate::errors::TarError;
use chrono::{TimeZone, Utc};
use std::fs::File;
use std::io::{self, BufWriter, Write};
use std::path::Path;
use tar::Archive;
use uucore::error::UResult;
use uucore::fs::display_permissions_unix;
/// List the contents of a tar archive, printing one entry per line.
pub fn list_archive(archive_path: &Path, verbose: bool) -> UResult<()> {
let file: File =
File::open(archive_path).map_err(|e| TarError::from_io_error(e, archive_path))?;
let mut archive = Archive::new(file);
let mut out = BufWriter::new(io::stdout().lock());
for entry_result in archive.entries().map_err(TarError::CannotReadEntries)? {
let entry = entry_result.map_err(TarError::CannotReadEntry)?;
if verbose {
// Collect all header fields into owned values before borrowing entry for the path,
// since both header() and path() require a borrow of entry.
let (mode, entry_type, owner, group, size, mtime) = {
let header = entry.header();
(
header.mode().unwrap_or(0),
header.entry_type(),
header
.username()
.ok()
.flatten()
.unwrap_or_default()
.to_owned(),
header
.groupname()
.ok()
.flatten()
.unwrap_or_default()
.to_owned(),
header.size().unwrap_or(0),
header.mtime().unwrap_or(0),
)
};
let path = entry.path().map_err(TarError::CannotReadEntryPath)?;
let type_char = match entry_type {
tar::EntryType::Directory => 'd',
tar::EntryType::Symlink => 'l',
tar::EntryType::Char => 'c',
tar::EntryType::Block => 'b',
tar::EntryType::Fifo => 'p',
_ => '-',
};
// Tar headers store the type separately from the mode bits, so we get the
// 9-character rwx string from uucore and prepend our own type character.
let perm_str = display_permissions_unix(mode, false);
let permissions = format!("{type_char}{perm_str}");
// TODO: GNU tar displays mtime in the user's local timezone; we
// currently format in UTC. Convert to local time for compatibility.
let dt: chrono::DateTime<Utc> = Utc
.timestamp_opt(mtime as i64, 0)
.single()
.unwrap_or_else(Utc::now);
let date_str = dt.format("%Y-%m-%d %H:%M");
writeln!(
out,
"{permissions} {owner}/{group} {size:>8} {date_str} {}",
path.display()
)
.map_err(TarError::Io)?;
} else {
let path = entry.path().map_err(TarError::CannotReadEntryPath)?;
writeln!(out, "{}", path.display()).map_err(TarError::Io)?;
}
}
out.flush().map_err(TarError::Io)?;
Ok(())
}
+1
View File
@@ -5,3 +5,4 @@
pub mod create;
pub mod extract;
pub mod list;

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