Skip to content

format: archives can hold their files in directories - #33

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

format: archives can hold their files in directories#33
donislawdev merged 1 commit into
mainfrom
format/archives-hold-directories

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

Two settings, shared by zip and targz through internal/format/archive so the containers cannot drift:

  • depth (0-50, default 0) - how many directories deep the files sit
  • directory_entries (default false) - whether the archive also lists the directories themselves

Those 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

targz pins tar.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: with d00/ 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 is targz_0001.tar.gz at 17 bytes, putting the real limit at 59. Fifty leaves room for a 56 byte name, and a guard asks archive/tar about every registered format rather than trusting the arithmetic.

The size stays exact

  • zip counts by writing the container through a counting writer, so path lengths were already counted - no arithmetic change.
  • targz counts 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 +512 per 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: true with depth: 0 is 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:

  • the deepest path every format can make still fits a USTAR header
  • a path one step past the ceiling is refused by tar
  • an archive nobody asked to nest is still flat
  • asking for directory entries in a flat archive names both settings
  • the directories come outermost first
  • a switch on the window reaches the file it describes - drives the real window and reads the archive off disk. directory_entries is the first PropertyBool any format declares, so the path from a switch to the engine had never carried a value

TestADirectoryEntryCostsExactlyOneTarBlock is on notProvenByMutation: it asserts what archive/tar does, and there is no line of ours under it to break.

Verification

Full suite green, preflight --quick green on all 11 checks. 7-Zip independently reports 3 files, 2 folders against 3 files. Byte stability re-confirmed after the refactor that split zip.go.

🤖 Generated with Claude Code

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>
@donislawdev
donislawdev merged commit a00ee9d into main Sep 1, 2026
30 of 31 checks passed
@donislawdev
donislawdev deleted the format/archives-hold-directories branch September 1, 2026 16:26
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