Skip to content
Novus Examples
5 min readNovus ExamplesGuideIntermediatev2026.09

Test Archive Extraction With Real Compression Fixtures

Extractors fail on codecs they never linked, filenames they cannot decode, and nesting they never expected. Here is how to find out before your users do.

An extractor that opens your test zip has told you almost nothing

The zip you make to test with is the easiest zip that will ever reach your code. One directory, ASCII filenames, deflate, a few kilobytes. Every library opens it. The archives that break extractors in production are the ones nobody thinks to create, and they break in ways that do not look like archive bugs at all: a missing file, a mangled name, a silent truncation, a process that exits zero having written nothing.

The archive catalogue here is 131 fixtures precisely because the failures cluster in places a hand-made sample never reaches.

This is the failure that costs the most time, because the error message is usually about something else entirely.

Compression formats are not one capability. A libarchive build links a set of codecs, and which set depends on how it was compiled. Builds compiled for the browser are routinely smaller than the system build a developer tests against locally, and zstandard and lz4 are the two most commonly absent. The same call that extracts a .tar.gz on your laptop can return an empty result for a .tar.zst in a WebAssembly build, without raising anything you would recognise as "unsupported codec".

Two fixtures exist for exactly this: zstandard-single-file and lz4-single-file. They are single-member streams, small and boring on purpose, so that when extraction fails the only variable is the codec. If your pipeline handles gzip, bzip2 and xz and then returns nothing for these two, you have found a link-time gap and not a logic bug.

Test the older single-member streams alongside them, compressed-single-gz, compressed-single-bz2 and compressed-single-xz, so you can tell "this codec is missing" from "single-member streams are mishandled generally". Those are different repairs.

Filenames are not text until someone decides an encoding

zip-unicode-names exists because the zip format spent decades ambiguous about filename encoding. A zip entry carries a flag bit that says the name is UTF-8. When that bit is clear, the name is conventionally CP437, and readers that assume UTF-8 regardless will produce mojibake for anything outside ASCII. Readers that assume CP437 regardless will mangle correctly flagged UTF-8 names.

The user-visible symptom is not an error. It is a file on disk with a wrong name, which then fails a later lookup that had nothing to do with the archive. If your extractor writes to disk, this fixture is also the one that tells you whether your path sanitiser survives characters it did not expect.

Shape, not size

Three fixtures vary the structure rather than the bytes:

  • zip-flat is the control. One level, nothing surprising.
  • zip-deep-tree nests far enough to find path-length limits, which on Windows arrive earlier than most developers expect and surface as a permission error rather than a length error.
  • zip-many-files has enough entries to make per-entry overhead visible, and to cross the threshold where a reader must handle the zip64 central directory rather than the classic one.

zip-empty deserves its own mention. An archive with no entries is valid, and a surprising number of extractors treat "no entries" as "failed to read". If your code reports an error for this one, your users will see an error every time someone uploads an empty export.

Nesting, and knowing when to stop

nested-zip-sample and deep-nested-zip-sample are archives inside archives. Recursive extraction is a reasonable feature and an unbounded one is a denial-of-service surface, so the question these fixtures answer is not "can you recurse" but "where do you stop, and do you say so".

A good result is a stated depth limit and a clear message. A bad result is either silent truncation that looks like a successful extraction, or recursion that continues until memory runs out.

There is no zip bomb in this catalogue and there never will be. The nesting fixtures are ordinary archives at ordinary sizes; they test control flow, not resource exhaustion.

Formats that are not zip and not tar

An extractor that only handles zip and tar meets these eventually:

  • sevenzip-archive and sevenzip-password-protected. The second matters because an encrypted entry should be reported as needing a password, not as corrupt. Those are very different messages to show a user.
  • cpio-newc-archive and unix-ar-archive, which appear inside package formats and initramfs images far more often than people expect.
  • iso-9660-disk-image, where the archive is a filesystem image and the extraction question is whether you read the directory structure or hand back one large opaque blob.
  • warc-web-archive and warc-multi-record-sample, the web-crawl format, where one file holds many request and response records with their own headers.

The realistic case: a data export

The GDPR export fixtures are the closest thing here to a real archive a real user will hand you: gdpr-sample-export-zip, gdpr-json-only-export-zip, gdpr-csv-heavy-export-zip and gdpr-nested-export-zip, plus the loose members they contain as separate files (gdpr-messages-json, gdpr-activity-jsonl, gdpr-devices-csv, gdpr-preferences-toml).

They matter because a data export is an archive whose contents are heterogeneous by definition: JSON, JSONL, CSV and TOML in one bundle, at wildly different sizes. If your importer assumes one format per archive, or streams the whole thing into memory before looking at it, this is where you find out.

Having the members available individually is deliberate. When an import of the bundle fails, you want to know whether the archive layer or the parser broke, and testing the member on its own answers that in one step.

A short order of work

  1. Start with zip-flat and zip-empty. If either fails, nothing after it is meaningful.
  2. Add zstandard-single-file and lz4-single-file early. A codec gap explains failures you would otherwise spend hours attributing to your own code.
  3. Then zip-unicode-names, because a wrong filename is a silent failure and silent failures are the expensive ones.
  4. Then shape: zip-deep-tree, zip-many-files.
  5. Then the non-zip formats you actually expect to receive, rather than all of them.
  6. Finish with a GDPR export, because it is the only one shaped like the real thing.

Every fixture above is downloadable on its own page, with the specification it was generated from and the bytes pinned by checksum, so a failure you find today reproduces tomorrow.

Continue this workflow

Try the workflow

Documentation and troubleshooting

Was this article helpful?

Found an error? Send a correction.