@absoluteui/helpers/zip
Streaming ZIP writer and reader with zip-slip and zip-bomb protection.
import { ... } from '@absoluteui/helpers/zip';How to use
Section titled “How to use”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 inputconst 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'How do I produce an archive in memory?
Section titled “How do I produce an archive in memory?”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; // => 2const blob = await zip.toBlob();blob.type; // => 'application/zip'(await readZip(blob)).entries.map((e) => e.name); // => ['policies/leave.md', 'policies/travel.md']What goes wrong?
Section titled “What goes wrong?”- 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 afterstream()ortoBlob()throwsHLP_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_TRAVERSALzip.add('C:/Windows/win.ini', 'x'); // => throws HLP_ZIP_ABSOLUTE_PATHconst 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_ENTRIESAPI reference
Section titled “API reference”Reader
Section titled “Reader”Functions
Section titled “Functions”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 pickerconst 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'Interfaces
Section titled “Interfaces”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>>;}Writer
Section titled “Writer”Functions
Section titled “Functions”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'Interfaces
Section titled “Interfaces”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>;