format: archives can hold their files in directories - #33
Merged
Conversation
Two settings, shared by zip and tar.gz through internal/format/archive so the two containers cannot drift: depth says how many directories deep the files sit, and directory_entries says whether the archive also lists the directories themselves. Those are separate questions, because extractors differ - some create a directory when they meet a path that needs one, and some create only what the archive names. The default is flat, so no existing archive changes by a byte and no version needs bumping. Asking for directory_entries without a depth is refused naming both settings rather than quietly doing nothing. The ceiling of 50 is measured rather than picked. tar.gz pins USTAR, which carries a path in a 155 byte prefix and a 100 byte name split on a slash, so whether a path fits depends on where its slashes fall. Measured against archive/tar: depth 61 with a 12 byte name is taken at 256 bytes and depth 62 is refused at 260, which puts the real limit at 59 for the longest name this build makes. A guard asks archive/tar about every registered format rather than trusting that arithmetic. The size stays exact. zip counts by writing the container to a counting writer, so path lengths were already counted. tar.gz counts by formula, and a USTAR header is 512 bytes at every depth it accepts, so the formula needed no length term - only 512 per directory entry. Directories are not children. A child's seed is FileSeed(seed, index) over a running index, so a directory in that list would shift the seed of every file after it and rewrite its contents. 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.
Two settings, shared by
zipandtargzthroughinternal/format/archiveso the containers cannot drift:depth(0-50, default 0) - how many directories deep the files sitdirectory_entries(default false) - whether the archive also lists the directories themselvesThose are separate questions. Extractors differ: some create a directory when they meet a path that needs one, some create only what the archive names. An archive is the one format where you can test both.
No breaking change
The default is flat, which is what every archive this tool has written so far. No existing hash moves and no version needs bumping.
The ceiling is measured, not picked
targzpinstar.FormatUSTAR, which carries a path in a 155 byte prefix and a 100 byte name split on a slash - so whether a path fits depends on where the slashes fall, not on length alone.Measured against Go's
archive/tar: withd00/segments, depth 61 is taken at 256 bytes and depth 62 is refused at 260. That makes the ceiling depend on the entry name, which is not constant - the longest this build makes istargz_0001.tar.gzat 17 bytes, putting the real limit at 59. Fifty leaves room for a 56 byte name, and a guard asksarchive/tarabout every registered format rather than trusting the arithmetic.The size stays exact
zipcounts by writing the container through a counting writer, so path lengths were already counted - no arithmetic change.targzcounts by formula, and a USTAR header is 512 bytes at every depth it accepts, flat to the refusal with no hidden step. So the formula needed no length term, only+512per directory entry.Directories are not children
A child's seed is
core.FileSeed(seed, index)over a running index, so a directory in that list would shift the seed of every file after it and rewrite its contents. They are written outside that list and consume no index.Refusals
directory_entries: truewithdepth: 0is refused naming both settings - a flat archive has no directories, so the file would come out identical either way. It is reachable from the window, since a checkbox always sends its value.Guards
Six new, all proven by mutation except one:
directory_entriesis the firstPropertyBoolany format declares, so the path from a switch to the engine had never carried a valueTestADirectoryEntryCostsExactlyOneTarBlockis onnotProvenByMutation: it asserts whatarchive/tardoes, and there is no line of ours under it to break.Verification
Full suite green,
preflight --quickgreen on all 11 checks. 7-Zip independently reports3 files, 2 foldersagainst3 files. Byte stability re-confirmed after the refactor that splitzip.go.🤖 Generated with Claude Code