jdx-tar
Synchronous, streaming tar reading, writing, and extraction with support for all GNU sparse formats, including the PAX sparse formats that other pure-Rust tar crates cannot extract.
use ;
use File;
let archive = open?;
let summary = new.unpack?;
println!;
# Ok::
Input is anything that implements Read. Decompression is the caller's
responsibility: wrap the file in a gzip/xz/zstd decoder first.
Archives can also be written to any Write implementation:
use ;
let mut output = Vecnew;
let mut builder = new;
let mut header = new_gnu;
header.set_mode;
header.set_size;
builder.append_data?;
builder.finish?;
# Ok::
Why another tar crate?
mise kept hitting release tarballs that no
Rust tar crate could extract, and papered over it by shelling out to a system
tar. That fallback was unreliable: which tar you get depends on the
platform and PATH, and GNU tar itself fails on some of these archives with
Unexpected EOF (only bsdtar handles them all). This crate exists to delete
that fallback.
The gap is GNU sparse files in PAX format (GNU.sparse.* extended headers,
formats 0.0, 0.1, and 1.0), which is what modern GNU tar writes with
--sparse. These appear in real release artifacts, typically disk images
where a multi-gigabyte sparse file compresses to a few megabytes. The tar
crate extracts these as a mangled GNUSparseFile.0/<name> file whose
contents are the raw sparse map and packed data.
jdx-tar |
tar (tar-rs) |
astral-tokio-tar |
libarchive bindings | system tar subprocess |
|
|---|---|---|---|---|---|
| PAX GNU sparse extraction (0.0/0.1/1.0) | ✅ | ❌ | ❌ (validates, doesn't expand) | ✅ | depends which tar |
Old GNU sparse (type S) |
✅ | ✅ | ✅ | ✅ | ✅ |
Sync Read-based streaming |
✅ | ✅ | ❌ (async only) | ✅ | n/a |
strip_components built in (any depth) |
✅ | ❌ | ❌ | ❌ | ✅ |
| Progress + per-entry callbacks | ✅ | ❌ | ❌ | ❌ | ❌ |
| Secure extraction is the only mode | ✅ | partial (unchecked Entry::unpack exists) |
✅ | ❌ | ❌ |
Pure Rust, no C, unsafe_code = "forbid" |
✅ | ✅ | ✅ | ❌ (links C) | n/a |
| Writes archives | ✅ | ✅ | ✅ | ✅ | ✅ |
This crate's header parsing draws from the tar crate (see
acknowledgements); use astral-tokio-tar if you need an async API.
Features
- All four GNU sparse representations: old GNU (type
S, including extended headers) and PAX 0.0, 0.1, and 1.0. Sparse semantics follow Go'sarchive/tarand the GNU tar manual. - On Unix, sparse files are extracted with real filesystem holes, so a 20 GiB-logical disk image occupies only its data size on disk. On Windows, extracted contents are identical but holes are not preserved, so sparse files take their full logical size.
- Entries read as their logical contents (holes read as zeroes), and the
data-extent map is available via
sparse_map(). Paths are fully resolved across long names, PAX overrides, and sparse name un-mangling, so you never seeGNUSparseFile.0/.... EntryUnpackersecurely extracts selected entries, letting callers inspect or skip entries while retaining the same path, symlink, sparse-file, and deferred-directory-metadata handling as whole-archive extraction.- Deterministic, streaming GNU-format archive writing, including GNU long-name and long-link records.
strip_componentsat any depth, applied after name resolution, pluspreserve_mtime,preserve_permissions, andoverwrite.on_progressandon_entrycallbacks.unpackreturns anUnpackSummarywith counts and a typedSkipReasonfor every skipped entry.- One dependency (
filetime),unsafe_code = "forbid", fuzzed withcargo-fuzz.
Security
Extraction is secure by default and has no insecure mode. Rejected: absolute
paths, parent-directory traversal, writes through previously extracted
symlinks, invalid header checksums, oversized PAX records, excessive sparse
maps, and inconsistent or orphaned sparse metadata such as GNU.sparse.name
path-aliasing. Link targets may point outside the destination; archive
entries are never written outside it.
As with any path-based extraction API, callers should ensure no untrusted local process can modify the destination tree during extraction.
Progress bars
on_progress reports bytes consumed from the input reader, which is the
decompressed stream. Its total size usually isn't known up front, so it works
as an activity signal but not a bounded bar. For a real progress bar, count
bytes on the compressed file with a wrapping reader placed before the
decoder, and use the file's size as the total. Extraction is streaming, so
compressed bytes consumed track overall progress accurately. Use on_entry
for per-file status messages.
Non-goals
- Compression codecs: pass a decompressed
Read. - Async: wrap calls in
spawn_blocking. - Other formats: no zip, no 7z.
Minimum supported Rust version
Rust 1.85 (edition 2024).
Acknowledgements
Header layout and parsing details draw from the MIT/Apache-2.0 licensed
tar crate. Sparse-format semantics follow
Go's archive/tar implementation and the GNU tar manual's Storing Sparse
Files appendix. The PAX sparse metadata hardening checks were informed by
the security work in
astral-tokio-tar.
License
Licensed under either of MIT or Apache-2.0, at your option.