The zipnative determinism contract
Reproducible output is a product feature with a written contract: what is guaranteed always, what is guaranteed per environment, and what
compression: { deterministic: true }pins bit-for-bit on every runtime.
This document is the contract; it was written down before anyone depended on it, and the golden tests (tests/core/zip-determinism.test.ts, tests/codecs/deflate-pure.test.ts) enforce it byte-for-byte.
The three levels of guarantee#
- Structurally deterministic — always. Entry order, header layout,
flags, attributes, timestamps and Zip64 decisions never depend on the
clock, randomness, locale or environment. Two
createZipruns with identical inputs produce structurally identical archives everywhere. - Bitwise deterministic per environment — the default. Deflate
compresses through the best available tier (
node:zlib, or the pure-TS encoder elsewhere), so bytes are stable for a given runtime + zlib build, but may differ across environments. - Bitwise deterministic everywhere —
compression: { deterministic: true }. The pure-TS encoder is pinned:SHA256(A) === SHA256(B)for identical inputs on every runtime (Node, browsers, Deno, Bun, Workers). This is the mode for content addressing, reproducible builds, caching and signatures.
Canonicalization rules (level 1, always)#
| Aspect | Rule |
|---|---|
| Entry order | sorted by raw UTF-8 name bytes, unsigned bytewise (order: 'insertion' preserves call order — still deterministic given identical calls) |
| Timestamps | DOS epoch 1980-01-01 00:00:00 unless a Date is given; 'now' emits ZIP_TIMESTAMP_NOT_PINNED |
| Name encoding | always UTF-8 with flag bit 11, including pure-ASCII names |
| version-made-by | constant 0x032D (Unix, spec 4.5) |
| versions-needed | 20, or 45 exactly when the entry uses Zip64 |
| External attributes | files 0o100644 << 16, directories (0o40755 << 16) | 0x10 |
| Internal attributes | 0 |
| Extra fields | none, except Zip64 (0x0001) exactly when a field overflows, carrying exactly the overflowed fields in spec order; caller-supplied extraFields are embedded verbatim (their determinism is the caller's) and bounded — maxExtraFieldBytes plus a hard 65535 structural cap (the u16 header field), enforced at plan time since 0.8.1 |
| Method selection | empty content is stored; deflate falls back to store when it does not shrink the payload (a pure function of the content) |
| Zip64 records | emitted exactly when a classic field overflows; classic EOCD sentinels only the overflowed fields |
| Data descriptors | never on buffered entries; always on addStream entries |
The frozen encoder contract (level 3)#
Under deterministic: true, deflate output bytes are produced by
src/codecs/deflate-pure.ts and every
constant in that file is frozen public API:
- hash function
imul(3-byte window, 0x9E3779B1) >>> 17over a 32 KiB window with head/prev chains; - zlib's level configuration table (good/lazy/nice/chain for levels 1–9);
- unified one-step-lazy matching, deferred match wins ties,
TOO_FAR = 4096; - 65 534-symbol blocks, hash history never reset across blocks;
- Huffman construction: two-queue merge over leaves sorted (frequency ascending, symbol ascending), leaf preferred on equal cost, zlib overflow fix at 15 bits (7 for the code-length tree), reassignment in the same sorted order;
- block choice at exact bit cost with the tie order stored ≤ fixed ≤ dynamic.
Changing any of these changes emitted bytes and is a semver-major release. The golden SHA-256 tables in the test suite are the tripwire.
Documented determinism losses#
| Situation | Effect | Signal |
|---|---|---|
Default codec tier (no deterministic: true) |
bytes vary across zlib builds | ZIP_NONDETERMINISTIC_CODEC (info; emitted when an explicit date is pinned but the codec is not) |
defaultDate: 'now' |
bytes vary per run | ZIP_TIMESTAMP_NOT_PINNED (info) |
addStream vs add of identical content |
different layout (data descriptor) | documented here; use add() when bytes must match |
Injected codec (setDeflateImpl) |
caller-defined bytes | never used for deterministic: true |
Verifying reproducibility#
import { createZip } from 'zipnative';
const build = () => {
const zip = createZip({ compression: { deterministic: true } });
zip.add('data.json', payload); // identical inputs...
return zip.toBytes();
};
// ...identical bytes, on any runtime:
sha256(build()) === sha256(build());