export-tar: support sparse files (GNU sparse format 1.0) - #10128
Merged
ThomasWaldmann merged 1 commit intoAug 18, 2026
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #10128 +/- ##
==========================================
- Coverage 87.05% 86.94% -0.11%
==========================================
Files 101 101
Lines 17848 17984 +136
Branches 2705 2737 +32
==========================================
+ Hits 15537 15637 +100
- Misses 1609 1638 +29
- Partials 702 709 +7 ☔ View full report in Codecov by Harness. |
ThomasWaldmann
force-pushed
the
export-tar-sparse-2562
branch
from
August 18, 2026 11:33
40033ec to
3b04cd7
Compare
ThomasWaldmann
commented
Aug 18, 2026
Add a --sparse option to export-tar (BORG and PAX tar formats only): files whose content contains runs of all-zero chunks are written as sparse tar members in GNU sparse format 1.0 (what GNU tar creates in POSIX mode), storing only a hole map and the non-hole data. The hole map is computed from the item's chunk list metadata alone, before any data is fetched: an all-zero chunk is detected by comparing its id against the (memoized) id of an all-zero chunk of the same size, reusing the zeros shortcut heuristic of DownloadPipeline.fetch_many (now extracted into a shared zero_chunk_flags function). Additionally, the memo is warmed up for power-of-two chunk sizes and each file's last chunk size, so single (non-repeated) zero chunks - like the only chunk of an all-zero file or a trailing hole's tail - get detected, too. The zero chunks of a sparse member are never fetched from the repository at all. Notes: - python's tarfile module reads GNU sparse members (import-tar has always consumed such tarballs correctly), but cannot write them, so the writing side crafts the pax records and the map itself and streams map + data segments via tarfile.addfile(). - holes are shrunk to whole 512-byte tar blocks within a zero run (GNU tar's sparse reader processes data segments block-wise and desyncs on unaligned segments); the zero bytes shaved off the hole edges are emitted as literal zeros, still without fetching the all-zero chunks. Zero runs shorter than a block stay dense, as do members where the map would cost more than the holes save. - pax record order matters for in-order readers: path (mangled GNUSparseFile.0/<name>, like GNU tar) comes first, GNU.sparse.name / GNU.sparse.realsize come last, so the real name/size win. - no pax size record must be written for a sparse member (readers would desync recalculating the next-header offset from it), so the stored size always lives in the ustar size field: standard octal while it fits (< 8 GiB), else base-256 encoded (the GNU/star big-number encoding, which GNU tar, libarchive and python's tarfile all read in any tar format) - so stored data and logical file size (GNU.sparse.realsize) are effectively unlimited. - without --sparse, the output is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ThomasWaldmann
force-pushed
the
export-tar-sparse-2562
branch
from
August 18, 2026 11:36
3b04cd7 to
9547a53
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2562.
Add a
--sparseoption toexport-tar(BORG and PAX tar formats only): files whose content contains runs of all-zero chunks are written as sparse tar members in GNU sparse format 1.0 (what GNU tar creates in POSIX mode), storing only a hole map and the non-hole data.How
The issue's original blocker was that the sparse map must precede the file data in the tar stream. That is solved by computing the hole map from the item's chunk list metadata alone, before fetching any data: an all-zero chunk is detected by comparing its id against the (memoized) id of an all-zero chunk of the same size, reusing the zeros-shortcut heuristic of
DownloadPipeline.fetch_many(extracted into a sharedzero_chunk_flags()). The memo is additionally warmed up for power-of-two chunk sizes and each file's last chunk size, so single (non-repeated) zero chunks — the only chunk of an all-zero file, or a trailing hole's tail — are detected as well. The zero chunks of a sparse member are never fetched from the repository at all.Python's
tarfilereads GNU sparse members (import-tarhas always consumed such tarballs correctly) but cannot write them, so the writing side crafts the pax records and the map itself and streams map + data segments throughtarfile.addfile():path(mangledGNUSparseFile.0/<name>, like GNU tar) first,GNU.sparse.name/GNU.sparse.realsizelast, so the real name/size win.sizerecord may be written for a sparse member (readers would desync when recalculating the next-header offset from it); a stored (non-hole) size beyond the 12-digit octal ustar field limit (8 GiB) is base-256 encoded into that field instead (the GNU/star big-number encoding, accepted by GNU tar, libarchive and python's tarfile) — so both stored data and logical size (GNU.sparse.realsize) are effectively unlimited.--sparse, output is byte-identical to before.--sparsewith--tar-format=GNUerrors out (old-GNU sparse headers are not PAX-based and deprecated).Hole detection is chunk-granular (like
extract --sparse) and does not depend on the original file having been sparse on disk.Interop
Verified against GNU tar 1.35 (byte-level comparison of the member layout gtar produces, including the terminal
(realsize, 0)map entry convention), bsdtar/libarchive 3.7.4, pythontarfilein bothr:andr|modes, andborg import-tarround trips (PAX and BORG formats).Tests
Unit tests for the map builders and
zero_chunk_flags; integration tests parametrized over a fixed chunker and a small-target fastcdc chunker (16 KiB target / 64 KiB max, so the small test files' sparse ranges yield multiple pure repeated all-zero chunks, as with the default chunker on real files); round trips (PAX/BORG × both chunkers), GNU tar extraction interop (incl. on-disk sparseness check on Linux), hardlinks,--strip-components, all-hole/trailing-hole/empty/unaligned edge cases, not-worthwhile fallback, and the GNU-format error.🤖 Generated with Claude Code