Home › Guides › Quickstart

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#