-
Notifications
You must be signed in to change notification settings - Fork 1
Decode Coverage
Moved here from the README (2026-09-03) so the README stays a short overview. Figures are the ones measured at the time each paragraph was written; the current headline numbers are in the README and the day-by-day history in the Changelog.
The honest metric is how much decodes byte-exact with no original binary at runtime.
tests/native_only_v2.sh runs extraction and diffs against the legacy oracle (the
NZ_NO_BRIDGE=1 it still exports dates from when a fallback to the original existed;
there is none any more). On a mixed
corpus (random, text, source, repeats, zeros, audio, a mixed audio/text/high-entropy
file, and a 1.1 MB mixed-entropy file; 12 fixtures × 8 methods):
| method | native byte-exact | method | native byte-exact |
|---|---|---|---|
-cn (store) |
12/12 |
-cd (lzhd) |
12/12 |
-cf (lzpf A) |
12/12 |
-cD (lzhd strong) |
12/12 |
-cF (lzpf B) |
12/12 |
-co (optimum1) |
12/12 |
-cc (cm) |
12/12 |
-cO (optimum2) |
12/12 |
96/96 (100%) byte-exact native on this fixture set — see the wide real-world sweep
below for the numbers that actually characterise the decode. The whole post-filter chain is native —
param2, param1, all seven text-transform bits the encoder emits (including 0x40, the PGN/chess
transform, which the community reference never implemented either),
and the dece x86 exe-filter — and so is every block/chunk kind the four 0x2b-family
codecs emit, including the prefilter sub-chunk and decr_param==2 audio blocks. -co/-cO decode
single-container and parallel-container LZ/CM content plus decr_param==0 (BWT) blocks in both shapes (raw-stored
output, the 256-bucket MTF/arithmetic entropy layer, and buckets the encoder stored verbatim) with the BWT-only
param14/param15 follow-ons.
On a 61-file real-world corpus (tests/real_corpus_sweep.sh, same corpus for every codec):
488/488, every codec 61/61. That corpus is now saturated, so it cannot detect anything on its own —
the honest figure comes from the wider sweep below.
tests/multifile_v2.sh covers what neither of those can: they build one-file archives and compare one
extracted file. It runs all twelve compressor selectors the binary's own usage lists — -cdp/-cdP/-cDp/-cDP
are encoder-parallelism variants of -cd/-cD, and testing only eight of them hid a real bug — across nine
archive shapes, comparing whole extracted trees (contents, permissions and timestamps), plus extraction under
the metadata switches and 72 listings. Each shape forces a different branch: distinct versus repeated permissions,
70 equal modes, setuid/sticky, an all-0600 input (whose permission record the encoder omits entirely), a
multi-block mix, a -r recursive tree, a -p4 single-file container and a -p4 multi-file one.
108/108 extract · 36/36 switches · 12/12 in a bare user environment · 72/72 listings.
That suite is also where a flaky case turned out to be a real defect. NanoZip is multi-threaded by default, and each worker writes its own self-describing record run into the container — in thread-scheduling order, not stream order. About one archive in twenty came out with the runs reordered, which this decoder rejected outright as a corrupt header. A flaky test is a defect report: re-running until green would have buried a layout that a user hits 5% of the time.
There is no fallback to the original binary in this program: nothing searches for an nz, nothing
shells out, and a stream no native decoder accepts is reported as corrupt. Earlier versions carried an
"extract bridge" that could silently run an original found near the working directory or on $PATH;
it was removed once every corpus decoded natively, after an audit under strace (zero foreign
execve, zero probes of an original binary, with one reachable and the bridge still enabled) and a
run with the originals made unreadable (95/95). tests/multifile_v2.sh also runs a copy of the
binary under env -i, with no variables set, and compares against the oracle.
A 61-file corpus at 488/488 proves nothing by itself; an earlier release quoted one at 479/480 while
-cO was in fact failing about 7.5% of real files, because that corpus happened to contain exactly
one of them. The figure below therefore comes from a fresh 155-file real-world corpus (63 MB across
eight format categories), swept with all eight codecs: 1240/1240 byte-exact.
| method | pass | fail | method | pass | fail | |
|---|---|---|---|---|---|---|
-cn |
155 | 0 | -cd |
155 | 0 | |
-cf |
155 | 0 | -cD |
155 | 0 | |
-cF |
155 | 0 | -co |
155 | 0 | |
-cc |
155 | 0 | -cO |
155 | 0 |
The last cause standing on this corpus, the image model, is ported (NzImageModel, nz_audio.cpp).
NanoZip's encoder runs four image detectors (BMP, and the same model is reached by PGM/PPM/TGA and
uncompressed TIFF) and puts a recognised block on decr_param = 3; every codec then decodes it with
the same function — the CM family's mode 3, -cd/-cD's 0xf sub-chunk, -cf/-cF's prefilter-slot
block with bit 3 set. The community reference treats that value as ordinary CM without reset, which
is why every uncompressed bitmap failed its checksum in every codec. The model is the audio
decoder's two-dimensional sibling: per-channel LMS planes and the same residual coder, a four-stage
sign-sign cascade fed by the four rows above, and an eight-mode pixel predictor over the left,
above, above-left and above-right neighbours. It decodes byte-exact in all seven codecs on 37 real
BMPs, on 8/16/24/32-bit and PGM/PPM/TGA/TIFF images, and in mixed archives with audio and text.
The BMP sweep also surfaced two -co/-cO declines outside this corpus that never entered the image
model, and both were one cause, now fixed: a BWT block's param15 pass names its match sources as
absolute offsets into everything decoded so far — the pre-post-filter stream the LZ window holds,
not the final output. The port sourced them from the final output, which the param1 delta filter had
already rewritten almost byte for byte, so the copied bytes were wrong and so was the window the next
LZ block read. Found by dumping the original's window at the failing block's entry and asking where
its bytes came from: its own window, where ours came from the file.
The -cd/-cD cluster that stood here earlier the same day (12 failures) was three causes, all now
closed: the prefilter state was not reset after a pure-literal LZ chunk (the original resets on every
LZ chunk); the LZ ring was sized round(total / 64 KB) where the real rule is bytefloat(p1 + 1) —
the same mantissa/exponent byte the -cc window and the lzpf dictionary use — for single-container
and per-stream parallel rings alike; and a match reaching back across the ring end is copied
linearly into the zeroed slack past the ring, not modulo the ring.
-cO's own literal model — the long-standing failure this project quoted for months — is closed. It
was two ring-lifetime bugs: the wrap's LZP-table sweep started 64 bytes too low, leaving its last 16
entries uncleared for the life of the archive, and the window-feed path collapsed four cases into one, so
a feed crossing the ring end left the cursor in the wrong place and skipped the reset. Because that table
feeds exactly one of eight mixer inputs, a stale entry moved a single probability by about 1% and flipped
a bit only where the coder already sat on a decision boundary — one wrong byte with tens of thousands of
byte-exact bytes on either side.
See the wiki's Component Status page for the full per-codec breakdown, known gaps, and roadmap to 100%.
tests/corpus_select.sh samples ~3000 files (4 KB–20 MB, every category of a 36 000-file format
collection, all PGM/PPM/TGA/TIFF/BMP images and executables included) and tests/sweep_run.sh runs
real_corpus_sweep.sh as resumable background shards over all eight codecs; NZ_TRACE_CONSTRUCTS=1
records which format constructs each decode met. First run: 24 201 of 24 296 pairs byte-exact, 71
failures in 21 files, all six causes fixed the same day (-cd/-cD exe filter + block-RLE chunks; -cc
stored blocks skipping their post-filters; the -cD literal-model reset rule (pure-literal chunks > 255
bytes); the chess transform's literal [ consuming its two hashed bytes; the exe filter restarting at an
embedded MZ/ELF executable; the chess resync digit class). Final: 24 272/24 272 (24 pairs skipped: three
files the original itself refuses). Constructs observed: every image detector (BMP24, TGA24, PGM 8/16, PPM),
predictor mode 2 only, -cd chunk kinds 0x0–0x9, 0xc, 0xf (never 0xd/0xe), text params 0x01/0x08/0x09/0x20/
0x21/0x28/0x29/0x40/0x41/0x88/0x89/0xc8/0xc9.