Quickstart
Install zipnative and cover the core workflows — reading with random access (in memory or over a byte-range source), secure extraction, deterministic creation, incremental modification, canonicalisation and one-call verification — in about five minutes.
Install#
npm install zipnative
Node ≥ 22, browsers, Deno, Bun and Workers share one API. Zero runtime dependencies — what you install is what runs.
Read with random access#
import { openZip } from 'zipnative';
const zip = openZip(bytes); // lazy: only the trailer is located
console.log(zip.entryCount);
for (const entry of zip.entries()) {
console.log(entry.name, entry.uncompressedSize);
}
const manifest = zip.readEntry('manifest.json'); // one entry, CRC-verified
for await (const chunk of zip.readEntryStream('video.bin')) {
// bounded memory for large entries
}
Read a remote or huge archive by ranges#
import { openZipRange } from 'zipnative';
const zip = await openZipRange({
size: contentLength, // known up front
read: (offset, length) => rangeFetch(url, offset, length), // HTTP Range, Blob.slice, FileHandle.read…
});
const manifest = await zip.readEntry('manifest.json'); // one tail window + the central directory + this entry
for await (const chunk of zip.readEntryStream('data.bin')) { /* 256 KiB ranged reads */ }
The engine never fetches: the source is yours, the limits and cross-checks
are the same as openZip() — the large and remote archives guide.
Extract securely#
import { extractZip } from 'zipnative';
const files = extractZip(bytes);
// Traversal, symlinks, duplicate names and decompression bombs are
// rejected BY DEFAULT — opting out is always explicit.
for (const file of files) {
console.log(file.path, file.data.length); // path is sanitized, relative
}
The engine never touches a filesystem; join file.path under your own
root (see the security guide). Directory entries are not
returned — recreate directories from the file paths. For large archives,
extractZipStream() yields { path, entry, stream() } entry by entry:
for await (const file of extractZipStream(bytes, { signal, onProgress })) {
for await (const chunk of file.stream()) { /* bounded memory; consume before advancing */ }
}
Create — reproducibly#
import { createZip } from 'zipnative';
const zip = createZip({ compression: { deterministic: true } });
zip.add('data.json', JSON.stringify(payload));
zip.add('raw.bin', bytes, { compression: { method: 'store' } });
const archive = zip.toBytes(); // sync
// or, with bounded memory and byte-identical output:
for await (const chunk of zip.stream()) { /* send */ }
Identical inputs give identical SHA-256 on every runtime — the determinism contract.
Make any archive reproducible#
import { analyzeDeterminism, canonicalizeZip } from 'zipnative';
analyzeDeterminism(foreignBytes).offenders; // what would drift: timestamps, order, extra fields…
const canonical = canonicalizeZip(foreignBytes); // sorted, epoch timestamps, extras dropped — no recompression
Idempotent, and the bytes are under the frozen contract — the reproducible builds guide.
Modify without recompressing#
import { createZipModifier, openZip } from 'zipnative';
const mod = createZipModifier(openZip(bytes));
mod.replaceEntry('config.json', '{"version":2}');
mod.removeEntry('obsolete.log');
const updated = mod.save(); // append-only: untouched entries untouched
const compact = mod.saveCompact(); // true deletion, still no recompression
Note: save() keeps every original byte — removed content remains
recoverable; saveCompact() is the deletion path, and
saveCompact({ canonical: true }) the canonical one.
To assemble an archive from entries of other archives without
recompressing them, createZip().addFromReader(reader, entry) transplants
the compressed bytes (verified first) and addRaw() takes a payload you
already compressed.
Verify in one call#
import { verifyZip } from 'zipnative';
const report = verifyZip(bytes); // never throws for archive problems
console.log(report.ok, report.entryCount);
// Structural refusals land in report.error ({ code, message });
// per-entry results carry crcMatch / sizeMatch / localHeaderMatch;
// encrypted and stream-only-codec entries are reported as skipped
// with a reason — never faked as corruption.
Going further#
- Parallel creation across worker threads:
import { createParallelZip } from 'zipnative/worker'. - Cancellation and progress on every asynchronous call:
signal(anAbortSignal— the rejection issignal.reason) andonProgress. - Explicit dates independent of the process time zone:
dosTimeMode: 'utc'. Legacy name encodings: anameDecodersuch as(b) => new TextDecoder('shift_jis').decode(b); extended timestamps and uid/gid:getExtendedTimestamps(entry),getUnixIds(entry). - Streamed entries that may exceed 4 GiB:
addStream(name, source, { zip64: true }). - Reading unseekable streams:
iterateZipEntries(source)— local headers only, so preferopenZip()whenever the whole archive is available. - Every thrown error carries a stable machine-readable
err.code(39 frozen codes) — branch on it, never on message text: see Errors and error codes. The message itself still starts withzipnative:and names the remedy. - Five production architectures, with diagrams and honest limits: Use cases. Try the engine live in your browser: Playgrounds.