Skip to content

Console Parity

YadeWira edited this page Sep 3, 2026 · 1 revision

Console parity and robustness

Moved here from the README (2026-09-03). The measured facts about the original's console and its defects are catalogued in the repository's docs/ORIGINAL_QUIRKS.md; this page keeps the narrative.

The console is the original's, byte for byte

Everything the decoder prints is matched against nz on a 36-case matrix (usage, help, info, list, test, extract, filters, -v, -sp, -o, missing and foreign files, truncated and corrupted archives, self-extracting .exe, the archive-name rule) with stdout, stderr and the created files compared byte for byte; the only differences left are the program name in the usage text, the thread-dependent order of the per-worker lines on a parallel container, and the encode commands. Measured behaviour that was not obvious: the exit status is always 0, even for a corrupt archive (reproduced; NZ_SAFE=1 or NZ_STRICT_EXIT=1 make damaged content return 2, see below); .nz is appended to the archive name unless it ends in .nz or .exe; a self-extracting .exe opens by seeking past the PE image; the [N MB] figure on the compressor line is the codec's memory-usage method transcribed (window and table sizes, to the byte); a checksum mismatch prints [stored computed] and continues; a failed decode is Archive corrupted. Error decoding (code 100), or 25600 when the archive is cut short. On a damaged archive the original writes whatever it decoded: it flushes its output per codec block (per 1 MB stream for -cd/-cD, per member for -cf/-cF), so the files of the blocks completed before the failure are on disk, the file the failing block starts with is created empty, a file whose checksum fails is written anyway with the Checksum mismatch line, and the status is still 0. This decoder does the same by default -- measured on 48 one-byte corruptions and truncations across eight codecs, 40 leave byte-identical trees; the rest differ in the garbage the two decoders produce or in a block-level check of the original not yet identified (see docs/ORIGINAL_QUIRKS.md, which lists every rough edge of the alpha and what is reproduced). Set NZ_SAFE=1 to write only entries whose checksum verifies, skip the rest with the mismatch line, and exit 2; the progress line shows the cumulative megabytes and re-prints the name only when the file changes; names over 40 columns are shown as ... plus their last 37 characters.

Round 3 (2026-09-03)

77 further cases on hand-built trees (empty files, links, odd names, modes, extreme mtimes, hostile paths, -o/-x/-sp corners, five time zones) plus a pty harness for the overwrite prompt. Fixed to match: directories created 0700; the mtime model (stored − writer offset, read back + the reader's current offset, 32-bit time_t, empty date field for 0); Cannot write: <path>; -forcemem, -continue, -pause; Unknown command: forms; the -fo column; the listing's 38-column name rule; prompt = one line, lowercase y/n/a. The remaining differences are progress-redraw timing.

Robustness against input that is not a valid archive

Fuzzed with 761 cases under AddressSanitizer + UBSan — truncations, single-bit flips weighted toward the header, corruption runs, and non-archives renamed .nz. That found an out-of-bounds write (a transform that ignored its output capacity), a 214-second denial of service ending in a segfault on a 191-byte mutated archive, an out-of-bounds read on a corrupt Huffman table, and two signed-overflow sites. All fixed; 761/761 clean. Worst corrupt case 3.7 s, a non-archive refused in ~10 ms.

The reusable invariant from that: bound decode work against the archive's DECLARED OUTPUT, per entry, not per call — a valid decode needs exactly 8 bit decodes per output byte no matter how the chunks are cut, and a per-call bound still lets a corrupt header multiply the work by inventing chunks.

Clone this wiki locally