format: archives can compress what they hold - #34
Merged
Conversation
One axis for both containers - compression: none, fast, default, best, meaning levels 0, 1, 6 and 9. The words are the intent rather than the mechanism, because ZIP sets a method per entry and TAR.GZ compresses the whole stream, and those are the same choice said two ways. The archive still comes out exactly the size that was ordered. What changes is how much of it is content and how much is padding. The hard part is that a compressed length cannot be planned. Measured: our content compresses at about 50 MB/s, and the guard that keeps a preview cheap plans three gigabytes of declared contents in milliseconds against a twenty second ceiling, so compressing to learn the length would take about a minute. This project had already measured the same thing from the other side on TIFF, where deflate moves the length with the seed while uncompressed is flat. So the plan keeps its stored arithmetic and the writer settles the difference. ZIP needs one pass. Every structural field is fixed width, so a compressed archive differs from a stored one by the entry data and nothing else - the plan works the padding out for the stored archive and the writer gives back what the compressor freed, measured with a registered compressor rather than by counting headers. Two things had to be measured rather than reasoned about: an entry's compressed bytes do not reach the writer until the entry closes and Flush does not close one, and the filler has to stay stored because it is random, so deflate grows it from 65195 to 65220 bytes. TAR.GZ needs two, because gzip has no per-entry method. The filler carries the bulk and the gzip extra field closes the remainder exactly, since it sits in the uncompressed header and costs n+2. Measured across levels 1, 6 and 9 and targets from 64 KB to 10 MB: every one landed on the ordered size, with 567 to 3759 bytes left for the field against the 65531 it holds. Two combinations are refused, each naming both settings. Compression with a size from the contents cannot be planned. Compression with a password cannot be streamed, because a locked entry states its length before its data is written and a compressed one does not know it yet. The default is none, so no existing archive changes by a byte. Verified with independent readers rather than only Go: 7-Zip and GNU tar take all four levels in both containers, and Python sees deflate on the entries and store on the filler with the content coming back unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
One axis for both containers:
compressionwithnone(default),fast,defaultandbest, meaning levels 0, 1, 6 and 9. The words are the intent rather than the mechanism — ZIP sets a method per entry, TAR.GZ compresses the whole stream, and those are the same choice said two ways.The archive still comes out exactly the size that was ordered. What changes is how much of it is content and how much is padding.
Why the plan cannot do this
A compressed length is only knowable by compressing. Measured: our content compresses at ~50 MB/s, and
TestPlanningAContainerDoesNotGenerateWhatItHoldsplans 3 GB of declared contents in milliseconds against a 20 s ceiling — compressing to learn the length would take about a minute. The project had already measured this from the other side on TIFF, where deflate moves the length with the seed while uncompressed is flat.So the plan keeps its stored arithmetic and the writer settles the difference.
ZIP: one pass
Every structural field of a zip is fixed width, so a compressed archive differs from a stored one by the entry data and nothing else. The plan works the padding out for the stored archive; the writer gives back what the compressor freed, measured with a registered compressor rather than by counting headers.
Two things had to be measured rather than reasoned about:
Flushdoes not close one. The first version read zero and overshot by exactly the compressed length of the entries.TAR.GZ: two passes
gzip has no per-entry method. The filler carries the bulk (random, so it survives compression at ~+0.031%) and the gzip extra field closes the remainder exactly, since it sits in the uncompressed header and costs
n+2. Measured across levels 1, 6, 9 and targets 64 KB → 10 MB: every one landed exactly, with 567–3759 B left for the field against the 65531 it holds (O163).Cost, paid at write and never at planning: a 10 MB archive is ~25 ms at
fastand ~140 ms atdefault, against 8 ms stored, and.tar.gzpays it twice.Two refusals, each naming both settings
CreateRaw, which writes the length before the data, so a compressed one would have to be held in memory whole.No breaking change
The default is
none, so no existing archive changes by a byte and no version needs bumping.Verification
Five new guards, all proven by mutation (10 caught including repointed ones), plus one that exists specifically to catch a compression axis that quietly stores everything and only grows the padding — that would pass the size guard perfectly.
Independent readers rather than Go alone (
O163's lesson): 7-Zip and GNU tar accept all four levels in both containers, and Python seesdeflateon the entries andstoreon the filler with content unchanged.preflight --quickgreen on all 11 checks.zip.gowas split by subject to stay under the crowding cap, and the solve was written as a bounded recursion so the depth counter stayed at 52 rather than raising a ratchet.🤖 Generated with Claude Code