mirror of
https://github.com/uutils/tar.git
synced 2026-06-10 16:14:35 -07:00
Compare commits
75
Commits
latest-commit
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
815a1b3036 | ||
|
|
25f4bd7c61 | ||
|
|
26bc94342d | ||
|
|
cef2373196 | ||
|
|
8d4fef7aba | ||
|
|
3be98e9e5c | ||
|
|
3eb916a8c2 | ||
|
|
9161cb6e2d | ||
|
|
ea51e0cd4c | ||
|
|
31c9e09527 | ||
|
|
14185c5626 | ||
|
|
c9f88f0fa2 | ||
|
|
0faed0baeb | ||
|
|
4986a8460c | ||
|
|
949437fe85 | ||
|
|
fac25e3b7d | ||
|
|
84c09e354f | ||
|
|
82a4980a70 | ||
|
|
3dd83a7b69 | ||
|
|
28ab4a8e2e | ||
|
|
b4865dab24 | ||
|
|
2cc665e2d3 | ||
|
|
fc74817b9b | ||
|
|
42f9d3722f | ||
|
|
74abcb3428 | ||
|
|
840b16bd8f | ||
|
|
cc2f4f1015 | ||
|
|
3a7c125a1b | ||
|
|
c5cd4bba76 | ||
|
|
ff0bba6fd6 | ||
|
|
c39eb3aae5 | ||
|
|
a0b003c74c | ||
|
|
60918b50b1 | ||
|
|
e94d74614e | ||
|
|
1afcb44928 | ||
|
|
9ade0975c3 | ||
|
|
d7a6d46b31 | ||
|
|
e6e00783c5 | ||
|
|
1368c86eb9 | ||
|
|
f6b4dc4f2f | ||
|
|
f96a65de73 | ||
|
|
3236dd7b87 | ||
|
|
ee6b9f1e7f | ||
|
|
d5f100f38b | ||
|
|
b27553a5f2 | ||
|
|
23855c363b | ||
|
|
44f2ebbe32 | ||
|
|
2aa37f71d0 | ||
|
|
d24f17f067 | ||
|
|
d281a396af | ||
|
|
3ea2f51c91 | ||
|
|
0778f2c62b | ||
|
|
78c666ac7d | ||
|
|
819b471288 | ||
|
|
fbdef22a7e | ||
|
|
4eb3c53302 | ||
|
|
b469a73940 | ||
|
|
30537acf7d | ||
|
|
c54ceb6752 | ||
|
|
4ad5dd1a80 | ||
|
|
b760338e9d | ||
|
|
7c00028cfb | ||
|
|
f541b90a9c | ||
|
|
b7c0578352 | ||
|
|
2c7a5e62e2 | ||
|
|
a9e1228b5e | ||
|
|
fd44e1ef17 | ||
|
|
65cd987c74 | ||
|
|
8da3aeecc9 | ||
|
|
29712f49cf | ||
|
|
1fe8bc1d5a | ||
|
|
8d1d0f56b7 | ||
|
|
18fa3ee18e | ||
|
|
06042058f9 | ||
|
|
e077e08aa5 |
@@ -0,0 +1,2 @@
|
|||||||
|
# Request review from the repository-wide code owner on pull requests.
|
||||||
|
* @kaladron
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
github.event.workflow_run.event == 'pull_request'
|
github.event.workflow_run.event == 'pull_request'
|
||||||
steps:
|
steps:
|
||||||
- name: 'Download artifact'
|
- name: 'Download artifact'
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
run: unzip comment.zip
|
run: unzip comment.zip
|
||||||
|
|
||||||
- name: 'Comment on PR'
|
- name: 'Comment on PR'
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v9
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
@@ -51,10 +51,12 @@ jobs:
|
|||||||
strip -s target/release/tarapp
|
strip -s target/release/tarapp
|
||||||
zstd -19 target/release/tarapp -o ../tar-x86_64-unknown-linux-gnu.zst
|
zstd -19 target/release/tarapp -o ../tar-x86_64-unknown-linux-gnu.zst
|
||||||
- name: Publish latest commit
|
- 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'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
tag_name: latest-commit
|
tag_name: latest-commit
|
||||||
|
body: |
|
||||||
|
commit: ${{ github.sha }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: true
|
prerelease: true
|
||||||
files: |
|
files: |
|
||||||
@@ -109,7 +111,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Retrieve reference artifacts
|
- name: Retrieve reference artifacts
|
||||||
uses: dawidd6/action-download-artifact@v20
|
uses: dawidd6/action-download-artifact@v21
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
workflow: GnuTests.yml
|
workflow: GnuTests.yml
|
||||||
|
|||||||
@@ -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 }}
|
||||||
@@ -53,12 +53,12 @@ jobs:
|
|||||||
CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' )
|
CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' )
|
||||||
outputs CODECOV_FLAGS
|
outputs CODECOV_FLAGS
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --no-fail-fast
|
run: cargo test --workspace --no-fail-fast
|
||||||
env:
|
env:
|
||||||
RUSTC_WRAPPER: ""
|
RUSTC_WRAPPER: ""
|
||||||
RUSTFLAGS: "-Cinstrument-coverage -Zcoverage-options=branch -Ccodegen-units=1 -Copt-level=0 -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
|
RUSTFLAGS: "-Cinstrument-coverage -Zcoverage-options=branch -Ccodegen-units=1 -Copt-level=0 -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
|
||||||
RUSTDOCFLAGS: "-Cpanic=abort"
|
RUSTDOCFLAGS: "-Cpanic=abort"
|
||||||
LLVM_PROFILE_FILE: "sed-%p-%m.profraw"
|
LLVM_PROFILE_FILE: "${{ github.workspace }}/tar-%p-%m.profraw"
|
||||||
# Use -Z
|
# Use -Z
|
||||||
RUSTC_BOOTSTRAP: 1
|
RUSTC_BOOTSTRAP: 1
|
||||||
- name: "`grcov` ~ install"
|
- name: "`grcov` ~ install"
|
||||||
@@ -82,6 +82,11 @@ jobs:
|
|||||||
# crate: grcov
|
# crate: grcov
|
||||||
# version: latest
|
# version: latest
|
||||||
# use-tool-cache: false
|
# 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`)
|
- name: Generate coverage data (via `grcov`)
|
||||||
env:
|
env:
|
||||||
# Use -Z
|
# Use -Z
|
||||||
@@ -96,15 +101,16 @@ jobs:
|
|||||||
# display coverage files
|
# 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
|
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
|
# 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
|
echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT
|
||||||
- name: Upload coverage results (to Codecov.io)
|
- name: Upload coverage results (to Codecov.io)
|
||||||
uses: codecov/codecov-action@v6
|
uses: codecov/codecov-action@v7
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
files: ${{ steps.coverage.outputs.report }}
|
files: ${{ steps.coverage.outputs.report }}
|
||||||
## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }}
|
|
||||||
flags: ${{ steps.vars.outputs.CODECOV_FLAGS }}
|
flags: ${{ steps.vars.outputs.CODECOV_FLAGS }}
|
||||||
name: codecov-umbrella
|
name: codecov-umbrella
|
||||||
fail_ci_if_error: false
|
fail_ci_if_error: false
|
||||||
|
verbose: true
|
||||||
|
directory: ./
|
||||||
|
root_dir: ./
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- name: Run sccache-cache
|
- name: Run sccache-cache
|
||||||
uses: mozilla-actions/sccache-action@v0.0.9
|
uses: mozilla-actions/sccache-action@v0.0.10
|
||||||
- name: Initialize workflow variables
|
- name: Initialize workflow variables
|
||||||
id: vars
|
id: vars
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -70,5 +70,5 @@ jobs:
|
|||||||
fault_type="${{ steps.vars.outputs.FAULT_TYPE }}"
|
fault_type="${{ steps.vars.outputs.FAULT_TYPE }}"
|
||||||
fault_prefix=$(echo "$fault_type" | tr '[:lower:]' '[:upper:]')
|
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>
|
# * 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
|
if [ -n "${{ steps.vars.outputs.FAIL_ON_FAULT }}" ] && [ -n "$fault" ]; then exit 1 ; fi
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
+588
-318
File diff suppressed because it is too large
Load Diff
+12
-5
@@ -37,17 +37,19 @@ chrono = { version = "0.4.37", default-features = false, features = [
|
|||||||
clap = { version = "4.4", features = ["wrap_help", "cargo"] }
|
clap = { version = "4.4", features = ["wrap_help", "cargo"] }
|
||||||
clap_complete = "4.5"
|
clap_complete = "4.5"
|
||||||
clap_mangen = "0.3"
|
clap_mangen = "0.3"
|
||||||
ctor = "0.8.0"
|
ctor = "1.0.0"
|
||||||
libc = "0.2.153"
|
libc = "0.2.153"
|
||||||
phf = "0.13.0"
|
phf = "0.13.0"
|
||||||
phf_codegen = "0.13.0"
|
phf_codegen = "0.13.0"
|
||||||
rand = { version = "0.10.0" }
|
rand = { version = "0.10.0" }
|
||||||
regex = "1.10.4"
|
regex = "1.10.4"
|
||||||
uucore = "0.7.0"
|
rustix = "1.1.4"
|
||||||
uutests = "0.7.0"
|
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
tempfile = "3.10.1"
|
tempfile = "3.10.1"
|
||||||
textwrap = { version = "0.16.1", features = ["terminal_size"] }
|
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"
|
xattr = "1.3.1"
|
||||||
zip = "8.0"
|
zip = "8.0"
|
||||||
|
|
||||||
@@ -58,18 +60,19 @@ clap_mangen = { workspace = true }
|
|||||||
ctor = { workspace = true }
|
ctor = { workspace = true }
|
||||||
phf = { workspace = true }
|
phf = { workspace = true }
|
||||||
textwrap = { workspace = true }
|
textwrap = { workspace = true }
|
||||||
uutests = { workspace = true }
|
|
||||||
uucore = { workspace = true }
|
uucore = { workspace = true }
|
||||||
|
uutests = { workspace = true }
|
||||||
zip = { workspace = true, optional = true }
|
zip = { workspace = true, optional = true }
|
||||||
|
|
||||||
tar = { optional = true, version = "0.0.1", package = "uu_tar", path = "src/uu/tar" }
|
tar = { optional = true, version = "0.0.1", package = "uu_tar", path = "src/uu/tar" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
libc = { workspace = true }
|
divan = { version = "4.7.0", package = "codspeed-divan-compat" }
|
||||||
pretty_assertions = "1"
|
pretty_assertions = "1"
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
|
rustix = { workspace = true, features = ["process"] }
|
||||||
tar-rs-crate = { version = "0.4", package = "tar" }
|
tar-rs-crate = { version = "0.4", package = "tar" }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
uucore = { workspace = true, features = ["entries", "process", "signals"] }
|
uucore = { workspace = true, features = ["entries", "process", "signals"] }
|
||||||
@@ -85,6 +88,10 @@ rlimit = "0.11.0"
|
|||||||
phf_codegen = { workspace = true }
|
phf_codegen = { workspace = true }
|
||||||
|
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "bench_tar"
|
||||||
|
harness = false
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "tarapp"
|
name = "tarapp"
|
||||||
path = "src/bin/tarapp.rs"
|
path = "src/bin/tarapp.rs"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
[](https://deps.rs/repo/github/uutils/tar)
|
[](https://deps.rs/repo/github/uutils/tar)
|
||||||
|
|
||||||
[](https://codecov.io/gh/uutils/tar)
|
[](https://codecov.io/gh/uutils/tar)
|
||||||
|
[](https://app.codspeed.io/uutils/tar?utm_source=badge)
|
||||||
|
|
||||||
# tar
|
# tar
|
||||||
|
|
||||||
@@ -11,7 +12,9 @@ Rust reimplementation of the tar utility.
|
|||||||
|
|
||||||
## Installation
|
## 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:
|
Clone the repository and build the project using Cargo:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
// 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::{self, 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();
|
||||||
|
let output = File::create(archive_path).unwrap();
|
||||||
|
let status_output = io::sink();
|
||||||
|
operations::create::create_archive(output, status_output, &refs, true, 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();
|
||||||
|
let output = File::create(&archive_path).unwrap();
|
||||||
|
let status_output = io::sink();
|
||||||
|
operations::create::create_archive(output, status_output, &refs, true, 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();
|
||||||
|
let output = File::create(&archive_path).unwrap();
|
||||||
|
let status_output = io::sink();
|
||||||
|
operations::create::create_archive(output, status_output, &refs, true, 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(|| {
|
||||||
|
let output = File::create(&archive_path).unwrap();
|
||||||
|
let status_output = io::sink();
|
||||||
|
operations::create::create_archive(output, status_output, &[sub.as_path()], true, 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(|| {
|
||||||
|
let input = File::open(&archive_path).unwrap();
|
||||||
|
operations::list::list_archive(input, 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(|| {
|
||||||
|
let input = File::open(&archive_path).unwrap();
|
||||||
|
operations::list::list_archive(input, 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();
|
||||||
|
let input = File::open(&archive_path).unwrap();
|
||||||
|
operations::extract::extract_archive(input, &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());
|
||||||
|
});
|
||||||
|
}
|
||||||
Generated
+307
-458
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -14,8 +14,8 @@ libc = "0.2.153"
|
|||||||
tempfile = "3.15.0"
|
tempfile = "3.15.0"
|
||||||
rand = { version = "0.10.0" }
|
rand = { version = "0.10.0" }
|
||||||
similar = "3.0.0"
|
similar = "3.0.0"
|
||||||
uucore = { version = "0.7.0", features = ["libc"] }
|
uucore = { version = "0.9.0", features = ["libc"] }
|
||||||
uufuzz = "0.7.0"
|
uufuzz = "0.9.0"
|
||||||
|
|
||||||
uu_tar = { path = "../src/uu/tar/" }
|
uu_tar = { path = "../src/uu/tar/" }
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ categories = ["command-line-utilities"]
|
|||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
uucore = { workspace = true }
|
uucore = { workspace = true, features = ["fs"] }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
tar = { workspace = true }
|
tar = { workspace = true }
|
||||||
|
chrono = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
path = "src/tar.rs"
|
path = "src/tar.rs"
|
||||||
@@ -24,3 +26,6 @@ path = "src/tar.rs"
|
|||||||
[[bin]]
|
[[bin]]
|
||||||
name = "tar"
|
name = "tar"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = { workspace = true }
|
||||||
|
|||||||
+57
-59
@@ -3,62 +3,74 @@
|
|||||||
// For the full copyright and license information, please view the LICENSE
|
// For the full copyright and license information, please view the LICENSE
|
||||||
// file that was distributed with this source code.
|
// file that was distributed with this source code.
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use thiserror::Error;
|
||||||
use uucore::error::UError;
|
use uucore::error::UError;
|
||||||
|
|
||||||
/// Error types for tar operations
|
/// Error types for tar operations
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Error)]
|
||||||
pub enum TarError {
|
pub enum TarError {
|
||||||
/// I/O error occurred
|
/// I/O error occurred while reading/writing
|
||||||
IoError(io::Error),
|
#[error("{0}")]
|
||||||
/// Invalid archive format or corrupted archive
|
Io(#[from] io::Error),
|
||||||
InvalidArchive(String),
|
|
||||||
/// File or directory not found
|
|
||||||
FileNotFound(String),
|
|
||||||
/// Permission denied
|
|
||||||
PermissionDenied(String),
|
|
||||||
/// General tar operation error
|
|
||||||
TarOperationError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implements display formatting for TarError.
|
/// Cannot read entries from archive
|
||||||
impl fmt::Display for TarError {
|
#[error("tar: Cannot read archive entries: {0}")]
|
||||||
/// Formats the error for display to users
|
CannotReadEntries(io::Error),
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
/// Cannot read an individual archive entry
|
||||||
TarError::IoError(err) => write!(f, "{err}"),
|
#[error("tar: Cannot read archive entry: {0}")]
|
||||||
TarError::InvalidArchive(msg) => write!(f, "{msg}"),
|
CannotReadEntry(io::Error),
|
||||||
TarError::FileNotFound(path) => {
|
|
||||||
write!(f, "{path}: Cannot open: No such file or directory")
|
/// Cannot read the path of an archive entry
|
||||||
}
|
#[error("tar: Cannot read entry path: {0}")]
|
||||||
TarError::PermissionDenied(path) => {
|
CannotReadEntryPath(io::Error),
|
||||||
write!(f, "{path}: Cannot open: Permission denied")
|
|
||||||
}
|
/// File or directory not found
|
||||||
TarError::TarOperationError(msg) => write!(f, "{msg}"),
|
#[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),
|
||||||
|
|
||||||
|
/// Refusing to write archive contents to terminal
|
||||||
|
#[error("tar: Refusing to write archive contents to terminal (missing -f option?)")]
|
||||||
|
RefuseWriteArchiveToTerminal,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TarError {
|
impl TarError {
|
||||||
/// Create a TarError from an io::Error with file path context
|
/// Create a TarError from an io::Error with file path context
|
||||||
pub fn from_io_error(err: io::Error, path: &std::path::Path) -> Self {
|
pub fn from_io_error(err: io::Error, path: &std::path::Path) -> Self {
|
||||||
match err.kind() {
|
match err.kind() {
|
||||||
io::ErrorKind::NotFound => TarError::FileNotFound(path.display().to_string()),
|
io::ErrorKind::NotFound => TarError::FileNotFound {
|
||||||
io::ErrorKind::PermissionDenied => {
|
path: path.to_path_buf(),
|
||||||
TarError::PermissionDenied(path.display().to_string())
|
},
|
||||||
}
|
io::ErrorKind::PermissionDenied => TarError::PermissionDenied {
|
||||||
_ => TarError::IoError(err),
|
path: path.to_path_buf(),
|
||||||
}
|
},
|
||||||
}
|
_ => TarError::Io(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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,20 +78,6 @@ impl std::error::Error for TarError {
|
|||||||
impl UError for TarError {
|
impl UError for TarError {
|
||||||
/// Returns the exit code for this error type
|
/// Returns the exit code for this error type
|
||||||
fn code(&self) -> i32 {
|
fn code(&self) -> i32 {
|
||||||
match self {
|
2 // TarError variants exit with code 2; argument/usage errors use code 64 (see tar.rs)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
use crate::errors::TarError;
|
use crate::errors::TarError;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::fs::{self, File};
|
use std::fs;
|
||||||
|
use std::io::{BufWriter, Write};
|
||||||
use std::path::Component::{self, ParentDir, Prefix, RootDir};
|
use std::path::Component::{self, ParentDir, Prefix, RootDir};
|
||||||
use std::path::{self, Path, PathBuf};
|
use std::path::{self, Path, PathBuf};
|
||||||
use tar::Builder;
|
use tar::Builder;
|
||||||
@@ -17,6 +18,7 @@ use uucore::error::UResult;
|
|||||||
///
|
///
|
||||||
/// * `archive_path` - Path where the tar archive should be created
|
/// * `archive_path` - Path where the tar archive should be created
|
||||||
/// * `files` - Slice of file paths to add to the archive
|
/// * `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
|
/// * `verbose` - Whether to print verbose output during creation
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
@@ -25,24 +27,28 @@ use uucore::error::UResult;
|
|||||||
/// - The archive file cannot be created
|
/// - The archive file cannot be created
|
||||||
/// - Any input file cannot be read
|
/// - Any input file cannot be read
|
||||||
/// - Files cannot be added due to I/O or permission errors
|
/// - 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(
|
||||||
// Create the output file
|
output: impl Write,
|
||||||
let file = File::create(archive_path).map_err(|e| {
|
status_output: impl Write,
|
||||||
TarError::TarOperationError(format!(
|
files: &[&Path],
|
||||||
"Cannot create archive '{}': {}",
|
allow_absolute: bool,
|
||||||
archive_path.display(),
|
verbose: bool,
|
||||||
e
|
) -> UResult<()> {
|
||||||
))
|
let mut output = BufWriter::new(output);
|
||||||
})?;
|
let mut status_output = BufWriter::new(status_output);
|
||||||
|
|
||||||
// Create Builder instance
|
// Create Builder instance
|
||||||
let mut builder = Builder::new(file);
|
let mut builder = Builder::new(&mut output);
|
||||||
|
builder.preserve_absolute(allow_absolute);
|
||||||
|
|
||||||
// Add each file or directory to the archive
|
// Add each file or directory to the archive
|
||||||
for &path in files {
|
for &path in files {
|
||||||
// Check if path exists
|
// Check if path exists
|
||||||
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 {
|
if verbose {
|
||||||
@@ -58,11 +64,11 @@ pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UR
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n");
|
.join("\n");
|
||||||
println!("{to_print}");
|
writeln!(status_output, "{to_print}").map_err(TarError::Io)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize path if needed (so far, handles only absolute paths)
|
// 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 original_components: Vec<Component> = path.components().collect();
|
||||||
let normalized_components: Vec<Component> = normalized.components().collect();
|
let normalized_components: Vec<Component> = normalized.components().collect();
|
||||||
if original_components.len() > normalized_components.len() {
|
if original_components.len() > normalized_components.len() {
|
||||||
@@ -70,7 +76,12 @@ pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UR
|
|||||||
[..original_components.len() - normalized_components.len()]
|
[..original_components.len() - normalized_components.len()]
|
||||||
.iter()
|
.iter()
|
||||||
.collect();
|
.collect();
|
||||||
println!("Removing leading `{}' from member names", removed.display());
|
writeln!(
|
||||||
|
std::io::stderr(),
|
||||||
|
"tar: Removing leading `{}' from member names",
|
||||||
|
removed.display()
|
||||||
|
)
|
||||||
|
.map_err(TarError::Io)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
normalized
|
normalized
|
||||||
@@ -81,30 +92,27 @@ pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UR
|
|||||||
// If it's a directory, recursively add all contents
|
// If it's a directory, recursively add all contents
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
builder.append_dir_all(normalized_name, path).map_err(|e| {
|
builder.append_dir_all(normalized_name, path).map_err(|e| {
|
||||||
TarError::TarOperationError(format!(
|
TarError::CannotAddDirectory {
|
||||||
"Failed to add directory '{}': {}",
|
path: path.to_path_buf(),
|
||||||
path.display(),
|
source: e,
|
||||||
e
|
}
|
||||||
))
|
|
||||||
})?;
|
})?;
|
||||||
} else {
|
} else {
|
||||||
// For files, add them directly
|
// For files, add them directly
|
||||||
builder
|
builder
|
||||||
.append_path_with_name(path, normalized_name)
|
.append_path_with_name(path, normalized_name)
|
||||||
.map_err(|e| {
|
.map_err(|e| TarError::CannotAddFile {
|
||||||
TarError::TarOperationError(format!(
|
path: path.to_path_buf(),
|
||||||
"Failed to add file '{}': {}",
|
source: e,
|
||||||
path.display(),
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finish writing the archive
|
builder.finish().map_err(TarError::CannotFinalizeArchive)?;
|
||||||
builder
|
drop(builder);
|
||||||
.finish()
|
|
||||||
.map_err(|e| TarError::TarOperationError(format!("Failed to finalize archive: {e}")))?;
|
status_output.flush().map_err(TarError::Io)?;
|
||||||
|
output.flush().map_err(TarError::Io)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -127,8 +135,8 @@ fn get_tree(path: &Path) -> Result<Vec<PathBuf>, std::io::Error> {
|
|||||||
Ok(paths)
|
Ok(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_path(path: &Path) -> Option<PathBuf> {
|
fn normalize_path(path: &Path, allow_absolute: bool) -> Option<PathBuf> {
|
||||||
if path.is_absolute() {
|
if path.is_absolute() && !allow_absolute {
|
||||||
Some(
|
Some(
|
||||||
path.components()
|
path.components()
|
||||||
.filter(|c| !matches!(c, RootDir | ParentDir | Prefix(_)))
|
.filter(|c| !matches!(c, RootDir | ParentDir | Prefix(_)))
|
||||||
@@ -138,3 +146,32 @@ fn normalize_path(path: &Path) -> Option<PathBuf> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
struct FailFlushWriter;
|
||||||
|
impl Write for FailFlushWriter {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
Err(io::Error::other("flush failed"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_archive_flush_failed() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let file_path = dir.path().join("test.txt");
|
||||||
|
fs::write(&file_path, "hello").unwrap();
|
||||||
|
|
||||||
|
let output = FailFlushWriter;
|
||||||
|
let status_output = io::sink();
|
||||||
|
|
||||||
|
let res = create_archive(output, status_output, &[file_path.as_path()], false, false);
|
||||||
|
assert!(res.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// file that was distributed with this source code.
|
// file that was distributed with this source code.
|
||||||
|
|
||||||
use crate::errors::TarError;
|
use crate::errors::TarError;
|
||||||
use std::fs::File;
|
use std::io::{self, BufReader, BufWriter, Read, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tar::Archive;
|
use tar::Archive;
|
||||||
use uucore::error::UResult;
|
use uucore::error::UResult;
|
||||||
@@ -22,41 +22,37 @@ use uucore::error::UResult;
|
|||||||
/// - The archive file cannot be opened
|
/// - The archive file cannot be opened
|
||||||
/// - The archive format is invalid
|
/// - The archive format is invalid
|
||||||
/// - Files cannot be extracted due to I/O or permission errors
|
/// - Files cannot be extracted due to I/O or permission errors
|
||||||
pub fn extract_archive(archive_path: &Path, verbose: bool) -> UResult<()> {
|
pub fn extract_archive(input: impl Read, archive_path: &Path, verbose: bool) -> UResult<()> {
|
||||||
// Open the archive file
|
|
||||||
let file = File::open(archive_path).map_err(|e| TarError::from_io_error(e, archive_path))?;
|
|
||||||
|
|
||||||
// Create Archive instance
|
// Create Archive instance
|
||||||
let mut archive = Archive::new(file);
|
let mut archive = Archive::new(BufReader::new(input));
|
||||||
|
let mut out = BufWriter::new(io::stdout().lock());
|
||||||
|
|
||||||
// Extract to current directory
|
// Extract to current directory
|
||||||
if verbose {
|
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
|
// Iterate through entries for verbose output and error handling
|
||||||
for entry_result in archive
|
for entry_result in archive.entries().map_err(TarError::CannotReadEntries)? {
|
||||||
.entries()
|
let mut entry = entry_result.map_err(TarError::CannotReadEntry)?;
|
||||||
.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}")))?;
|
|
||||||
|
|
||||||
// Get the path before unpacking (clone it so we can use it after borrowing entry mutably)
|
// Get the path before unpacking (clone it so we can use it after borrowing entry mutably)
|
||||||
let path = entry
|
let path = entry
|
||||||
.path()
|
.path()
|
||||||
.map_err(|e| TarError::InvalidArchive(format!("Failed to read entry path: {e}")))?
|
.map_err(TarError::CannotReadEntryPath)?
|
||||||
.to_path_buf();
|
.to_path_buf();
|
||||||
|
|
||||||
if verbose {
|
if verbose {
|
||||||
println!("{}", path.display());
|
writeln!(out, "{}", path.display()).map_err(TarError::Io)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unpack the entry
|
// Unpack the entry
|
||||||
entry.unpack_in(".").map_err(|e| {
|
entry.unpack_in(".").map_err(|e| TarError::CannotExtract {
|
||||||
TarError::TarOperationError(format!("Failed to extract '{}': {}", path.display(), e))
|
path: path.clone(),
|
||||||
|
source: e,
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out.flush().map_err(TarError::Io)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// 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::io::{self, BufReader, BufWriter, Read, Write};
|
||||||
|
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(input: impl Read, verbose: bool) -> UResult<()> {
|
||||||
|
let mut archive = Archive::new(BufReader::new(input));
|
||||||
|
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(())
|
||||||
|
}
|
||||||
@@ -5,3 +5,4 @@
|
|||||||
|
|
||||||
pub mod create;
|
pub mod create;
|
||||||
pub mod extract;
|
pub mod extract;
|
||||||
|
pub mod list;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user