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'
|
||||
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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' )
|
||||
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
|
||||
uses: codecov/codecov-action@v7
|
||||
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: ./
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_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,6 +4,7 @@
|
||||
[](https://deps.rs/repo/github/uutils/tar)
|
||||
|
||||
[](https://codecov.io/gh/uutils/tar)
|
||||
[](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:
|
||||
|
||||
|
||||
@@ -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"
|
||||
rand = { version = "0.10.0" }
|
||||
similar = "3.0.0"
|
||||
uucore = { version = "0.7.0", features = ["libc"] }
|
||||
uufuzz = "0.7.0"
|
||||
uucore = { version = "0.9.0", features = ["libc"] }
|
||||
uufuzz = "0.9.0"
|
||||
|
||||
uu_tar = { path = "../src/uu/tar/" }
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -24,3 +26,6 @@ path = "src/tar.rs"
|
||||
[[bin]]
|
||||
name = "tar"
|
||||
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
|
||||
// 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),
|
||||
|
||||
/// Refusing to write archive contents to terminal
|
||||
#[error("tar: Refusing to write archive contents to terminal (missing -f option?)")]
|
||||
RefuseWriteArchiveToTerminal,
|
||||
}
|
||||
|
||||
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 +78,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
use crate::errors::TarError;
|
||||
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::{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,28 @@ 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<()> {
|
||||
// Create the output file
|
||||
let file = File::create(archive_path).map_err(|e| {
|
||||
TarError::TarOperationError(format!(
|
||||
"Cannot create archive '{}': {}",
|
||||
archive_path.display(),
|
||||
e
|
||||
))
|
||||
})?;
|
||||
pub fn create_archive(
|
||||
output: impl Write,
|
||||
status_output: impl Write,
|
||||
files: &[&Path],
|
||||
allow_absolute: bool,
|
||||
verbose: bool,
|
||||
) -> UResult<()> {
|
||||
let mut output = BufWriter::new(output);
|
||||
let mut status_output = BufWriter::new(status_output);
|
||||
|
||||
// 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
|
||||
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 +64,11 @@ pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UR
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
println!("{to_print}");
|
||||
writeln!(status_output, "{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 +76,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!(
|
||||
std::io::stderr(),
|
||||
"tar: Removing leading `{}' from member names",
|
||||
removed.display()
|
||||
)
|
||||
.map_err(TarError::Io)?;
|
||||
}
|
||||
|
||||
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 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}")))?;
|
||||
builder.finish().map_err(TarError::CannotFinalizeArchive)?;
|
||||
drop(builder);
|
||||
|
||||
status_output.flush().map_err(TarError::Io)?;
|
||||
output.flush().map_err(TarError::Io)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -127,8 +135,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(_)))
|
||||
@@ -138,3 +146,32 @@ fn normalize_path(path: &Path) -> Option<PathBuf> {
|
||||
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.
|
||||
|
||||
use crate::errors::TarError;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufReader, BufWriter, Read, Write};
|
||||
use std::path::Path;
|
||||
use tar::Archive;
|
||||
use uucore::error::UResult;
|
||||
@@ -22,41 +22,37 @@ use uucore::error::UResult;
|
||||
/// - The archive file cannot be opened
|
||||
/// - The archive format is invalid
|
||||
/// - Files cannot be extracted due to I/O or permission errors
|
||||
pub fn extract_archive(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))?;
|
||||
|
||||
pub fn extract_archive(input: impl Read, archive_path: &Path, verbose: bool) -> UResult<()> {
|
||||
// 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
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -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 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