Skip to content

@absoluteui/helpers/zip

Streaming ZIP writer and reader with zip-slip and zip-bomb protection.

import { ... } from '@absoluteui/helpers/zip';

Creates and reads ZIP archives in the browser with the native CompressionStream, no library required. The writer streams, so memory stays flat for large archives; the reader verifies checksums and refuses path traversal (“zip slip”) and zip bombs. The Files and downloads guide shows the basic zipFiles and readZip round trip; the recipes below cover the rest.

How do I bundle many generated files into one download?

Section titled “How do I bundle many generated files into one download?”

Add entries to a createZipWriter and pass its stream to saveWithPicker, which writes to disk as the archive is produced. Store already-compressed files (JPEG, PDF, PNG) with compression: 'store' to save time.

import { saveWithPicker } from '@absoluteui/services/download';
import { createZipWriter } from '@absoluteui/helpers/zip';
declare const payslips: readonly { employeeId: number; pdf: Blob }[];
const zip = createZipWriter({ comment: 'Payslips September 2026' });
zip.addDirectory('2026-09');
for (const p of payslips) zip.add(`2026-09/payslip-${String(p.employeeId)}.pdf`, p.pdf, { compression: 'store' });
zip.add('2026-09/summary.csv', 'employee_id,net\n42,4180.00\n');
const status = await saveWithPicker(zip.stream(), { suggestedName: 'payslips-2026-09.zip' });

How do I read one file out of an uploaded archive?

Section titled “How do I read one file out of an uploaded archive?”

readZip lists entries without extracting them; entry.read() inflates one entry and checks its CRC-32. Entry names are already sanitized.

import { readZip, zipFiles } from '@absoluteui/helpers/zip';
const upload = await zipFiles([
{ name: 'import/employees.csv', data: 'id,name\n42,Ada Lovelace\n' },
{ name: 'import/readme.txt', data: 'Exported from the old HR system' },
]); // stands in for a File from a file input
const archive = await readZip(upload, { maxEntries: 1_000, maxTotalUncompressed: 200 * 1024 * 1024 });
const csv = archive.entries.find((e) => !e.isDirectory && e.name.endsWith('.csv'));
csv?.name; // => 'import/employees.csv'
new TextDecoder().decode(await csv?.read()); // => 'id,name\n42,Ada Lovelace\n'

toBlob() returns an application/zip Blob, for example to attach to an upload or pass to downloadBlob. A writer produces its archive once; add every entry first.

import { createZipWriter, readZip } from '@absoluteui/helpers/zip';
const zip = createZipWriter();
zip.add('policies/leave.md', '# Leave policy');
zip.add('policies/travel.md', '# Travel policy');
zip.size; // => 2
const blob = await zip.toBlob();
blob.type; // => 'application/zip'
(await readZip(blob)).entries.map((e) => e.name); // => ['policies/leave.md', 'policies/travel.md']
  • Unsafe or duplicate names throw when added: .. segments (HLP_ZIP_PATH_TRAVERSAL), absolute paths or drive letters (HLP_ZIP_ABSOLUTE_PATH), repeated names (HLP_ZIP_DUPLICATE_NAME). Adding after stream() or toBlob() throws HLP_ZIP_WRITER_CLOSED.
  • The reader rejects archives over its limits (HLP_ZIP_TOO_MANY_ENTRIES, HLP_ZIP_TOO_LARGE, HLP_ZIP_RATIO), corrupt data (HLP_ZIP_CRC_MISMATCH), and encrypted entries (HLP_ZIP_ENCRYPTED) instead of misreading them.
import { createZipWriter, readZip, zipFiles } from '@absoluteui/helpers/zip';
const zip = createZipWriter();
zip.add('../../etc/passwd', 'x'); // => throws HLP_ZIP_PATH_TRAVERSAL
zip.add('C:/Windows/win.ini', 'x'); // => throws HLP_ZIP_ABSOLUTE_PATH
const three = await zipFiles([{ name: 'a.txt', data: 'a' }, { name: 'b.txt', data: 'b' }, { name: 'c.txt', data: 'c' }]);
await readZip(three, { maxEntries: 2 }); // => throws HLP_ZIP_TOO_MANY_ENTRIES

readZip

Opens a ZIP archive (including ZIP64) by reading its central directory; entry data is read lazily. Guards against zip bombs (entry count, total size, compression ratio, actual output never exceeding the declared size), verifies CRC-32, rejects encrypted entries, and sanitizes names so they cannot escape the extraction root (“zip slip”).

function readZip(input: Blob | Uint8Array | ArrayBuffer, options?: ReadZipOptions): Promise<ZipArchive>;

Example:

const file = await zipFiles([{ name: 'reports/q1.csv', data: 'id,name\n1,Ada' }]); // or a File from a picker
const zip = await readZip(file);
zip.entries.map((e) => e.name); // => ['reports/q1.csv']
const first = zip.entries[0];
const text = first ? new TextDecoder().decode(await first.read()) : ''; // => 'id,name\n1,Ada'

ReadZipOptions

interface ReadZipOptions {
readonly maxEntries?: number;
readonly maxTotalUncompressed?: number;
readonly maxCompressionRatio?: number;
readonly rawNames?: boolean;
readonly signal?: AbortSignal;
}

ZipArchive

An opened archive.

interface ZipArchive {
readonly entries: readonly ZipEntry[];
readonly comment: string;
get(name: string): ZipEntry | undefined;
}

ZipEntry

One entry of an archive opened with readZip.

interface ZipEntry {
readonly name: string;
readonly rawName: string;
readonly size: number;
readonly compressedSize: number;
readonly lastModified: Date;
readonly isDirectory: boolean;
readonly encrypted: boolean;
readonly method: number;
readonly crc32: number;
readonly comment: string;
read(options?: {
readonly signal?: AbortSignal;
}): Promise<Uint8Array<ArrayBuffer>>;
stream(): ReadableStream<Uint8Array<ArrayBuffer>>;
}

createZipWriter

Creates a streaming ZIP writer. Entries stream through without knowing their sizes (local headers use data descriptors), names are UTF-8, and CRC-32 is computed incrementally.

function createZipWriter(options?: ZipWriterOptions): ZipWriter;

Example:

const zip = createZipWriter();
zip.add('reports/q1.csv', 'id,name\n1,Ada');
zip.add('photo.jpg', new File([new Uint8Array([0xff, 0xd8, 0xff])], 'photo.jpg'), { compression: 'store' });
const blob = await zip.toBlob();
(await readZip(blob)).entries.map((e) => e.name); // => ['reports/q1.csv', 'photo.jpg']

zipFiles

Zips files into a Blob in one call.

function zipFiles(files: readonly ZipFileInput[], options?: ZipWriterOptions): Promise<Blob>;

Example:

const pngBlob = new Blob([new Uint8Array([0x89, 0x50, 0x4e, 0x47])], { type: 'image/png' });
const blob = await zipFiles([{ name: 'a.txt', data: 'hello' }, { name: 'img/b.png', data: pngBlob, compression: 'store' }]);
blob.type; // => 'application/zip'

ZipAddOptions

interface ZipAddOptions {
readonly lastModified?: Date;
readonly compression?: ZipCompression;
readonly comment?: string;
}

ZipFileInput

A file for zipFiles.

interface ZipFileInput extends ZipAddOptions {
readonly name: string;
readonly data: ZipEntryData;
}

ZipWriter

Streaming ZIP archive builder.

interface ZipWriter {
add(name: string, data: ZipEntryData, options?: ZipAddOptions): void;
addDirectory(name: string, options?: Pick<ZipAddOptions, 'lastModified' | 'comment'>): void;
readonly size: number;
stream(): ReadableStream<Uint8Array<ArrayBuffer>>;
toBlob(): Promise<Blob>;
}

ZipWriterOptions

interface ZipWriterOptions {
readonly compression?: ZipCompression;
readonly comment?: string;
readonly zip64?: 'auto' | 'always';
readonly maxEntries?: number;
}

ZipCompression

deflate uses native CompressionStream('deflate-raw'); store writes bytes as they are.

type ZipCompression = 'deflate' | 'store';

ZipEntryData

Entry content. Streams and Blobs are read lazily while the archive is produced.

type ZipEntryData = string | Uint8Array | ArrayBuffer | Blob | ReadableStream<Uint8Array>;