Skip to content

format: archives can compress what they hold - #34

Merged
donislawdev merged 1 commit into
mainfrom
format/archives-compress
Sep 1, 2026
Merged

format: archives can compress what they hold#34
donislawdev merged 1 commit into
mainfrom
format/archives-compress

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

One axis for both containers: compression with none (default), fast, default and best, 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 TestPlanningAContainerDoesNotGenerateWhatItHolds plans 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:

  • an entry's compressed bytes do not reach the writer until the entry closes, and Flush does not close one. The first version read zero and overshot by exactly the compressed length of the entries.
  • the filler must stay stored. It is random, so deflate grows it — measured 65195 → 65220 B — and a padding entry whose length cannot be aimed is useless.

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 fast and ~140 ms at default, against 8 ms stored, and .tar.gz pays it twice.

Two refusals, each naming both settings

  • compression + size-from-contents — the length would be knowable only by compressing.
  • compression + password — a locked entry goes through 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 sees deflate on the entries and store on the filler with content unchanged.

preflight --quick green on all 11 checks. zip.go was 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

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>
@donislawdev
donislawdev merged commit 78df1bd into main Sep 1, 2026
18 checks passed
@donislawdev
donislawdev deleted the format/archives-compress branch September 1, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant