runtime:archive

Read and write tar, tar.gz and zip (DECISIONS D174). Bun.Archive's shape: new Archive(input) builds from a plain object of entries or opens bytes, a Blob or a runtime:fs file(); bytes()/blob() return the archive, entries() exposes metadata with lazy entry reads, files() returns Map<string, File>, and extract() writes it out.

Capability: none for bytes

Bytes in and bytes out need nothing. A runtime:fs file() source needs FileRead; extract() and Archive.write() need FileWrite, with the destination as the capability target. A file source keeps its open handle, but it is not a snapshot: in-place writes before or during extraction are visible. Status: Available.

Import

JavaScript
import { Archive, ArchiveEntry } from "runtime:archive";
import { file } from "runtime:fs";

new Archive(input, options?)

input is one of:

  • a plain object whose keys are entry paths and whose values are a string (UTF-8), a Uint8Array/ArrayBuffer/view, a Blob or File, null for a directory (the key must end with /), or { data, lastModified } where data is any of the former and lastModified is epoch milliseconds or a Date;

  • a Uint8Array, ArrayBuffer, view or Blob holding an archive;

  • a runtime:fs file().

A path string is a TypeError: pass file(path). A SharedArrayBuffer is refused. Bytes — a source, or an entry's data — are copied when the Archive is constructed, so changing the buffer afterwards does not change the archive. Opening detects the format from the leading bytes — gzip, zip or tar — and never from a name or extension.

For build entries, Blob and File mean the web APIs. A runtime:fs file() handle is not a build entry value; use await file(path).bytes() to add its contents. This reads the whole input file into memory.

OptionDefaultEffect
format"tar""tar" or "zip" when building. Ignored when opening.
compress"none""gzip" for tar, "deflate" for zip, or "none". A combination the format cannot carry is a TypeError.
level6Compression level with "gzip" or "deflate": 1 is fastest, 9 smallest. Outside 1–9 is a RangeError; with compress: "none" it is a TypeError.
maxEntries100000Entry-count cap, on build and read. For zip the count is read from the end record before the central directory is parsed. Over it is RangeError with ERR_ARCHIVE_TOO_LARGE.
maxBytes1073741824 (1 GiB)Cap on uncompressed bytes, on build and read, checked against declared sizes before allocation and against bytes actually decompressed. Over it is RangeError with ERR_ARCHIVE_TOO_LARGE.

Builds are deterministic: entries are sorted by path, ownership is 0:0, modes are 0644 for files and 0755 for directories, and the timestamp is 0 unless an entry supplied lastModified (zip clamps to 1980–2107). The same input produces the same bytes.

JavaScript
import { Archive } from "runtime:archive";
import { file } from "runtime:fs";

const entries = {
  "hello.txt": "Hello, world!",
  "assets/logo.png": await file("assets/logo.png").bytes(),
  "empty/": null,
};

await new Archive(entries).bytes();                          // tar
await new Archive(entries, { format: "zip" }).bytes();
await new Archive(entries, { compress: "gzip" }).bytes();   // tar.gz

Methods

MethodResolves to
bytes()A copy of the archive's bytes (Uint8Array). An opened archive returns its source bytes unchanged.
blob()A Blob whose type is application/x-tar, application/gzip or application/zip, from the format in the bytes.
entries()An array of ArchiveEntry, in archive order. A snapshot: use the async iterator to stream it.
[Symbol.asyncIterator]An AsyncIterable<ArchiveEntry> over the same metadata, parsed one entry at a time.
files(filter?)A Map<string, File> of the file entries (directories skipped) that filter takes — see Filters.
extract(directory, options?)Writes the entries under directory (a path string or file()) and resolves to { files, directories } written. options.filter writes only the entries it takes (Filters); the whole archive is still validated. options may also set maxEntries / maxBytes; a limit it leaves out keeps the Archive's. Needs FileWrite.
Archive.write(destination, input, options?)Builds input as above and writes it to destination (a path string or file()), resolving to the byte count. Needs FileWrite.

for await (const entry of archive) streams metadata instead of collecting an array, so the guest holds only the current entry. Per-entry validation and the entry limits still apply, but the cross-entry checks run as entries arrive: a duplicate or file/directory collision later in the archive rejects the loop after earlier entries were yielded. A zip parser still holds its central directory, bounded by maxEntries.

An ArchiveEntry has path, size, directory and lastModified (epoch milliseconds, or null), plus lazy bytes(), text(), blob() and stream(). Reading an entry does not materialize the other entries: stream() yields bounded chunks, files(filter) reads only the entries the filter selected, and a file() source is scanned through the filesystem rather than loaded whole. (A tar.gz entry still has to decompress through its predecessors to reach its data; those bytes are discarded as they are skipped.)

JavaScript
import { Archive } from "runtime:archive";
import { file } from "runtime:fs";

const opened = new Archive(file("bundle.tar.gz"));
for (const entry of await opened.entries()) {
  if (!entry.directory) console.log(entry.path, entry.size);
}
const text = await (await opened.entries())[0].text();

const images = await opened.files(/^assets\/.*\.png$/);
await opened.extract("public");                  // creates public/ as needed
await Archive.write(file("bundle.zip"), { "hello.txt": "Hello" }, { format: "zip" });

Filters

files() and extract({ filter }) take the same filter:

  • A glob ("src/**") or a list of globs (["**/*.ts", "!**/*.test.ts"]), matched against the entry path with the rules of runtime:fs's Glob: * does not cross /, ** does. In a list, a pattern starting with ! excludes: an entry is taken when it matches a pattern without ! and none with it, and a list of only ! patterns takes everything else.

  • A RegExp, tested against the path.

  • A function (path) => boolean.

An invalid glob, or a list with a non-string in it, is a TypeError.

Safety

Every entry path is validated before that entry is returned or a file is written, and the eager listings finish the layout check before returning. A path must be relative, slash-separated, valid UTF-8, with no empty, . or .. component, no backslash and no drive letter. Tar symlinks and hard links, zip symlinks, special tar types (character/block/FIFO/sparse/global PAX) and encrypted zip entries are refused, as is zip compression other than Stored and Deflated. Duplicate entries, file/directory collisions, and names that would collide on a case-insensitive filesystem (Windows, macOS) are refused. On Windows, device names (NUL, COM1.txt), names with : or another character Windows forbids, and names ending in a dot or space are refused too. An archive is never sanitized into a different one. extract() checks the destination, parents and output paths for existing symlinks, then decodes and validates the whole archive into a temporary directory beneath the destination before moving staged files into place. Nothing beneath the destination is followed if it is a link, including one that appears during extraction. A corrupt or unsafe archive therefore does not partially change destination files. Staging uses temporary disk space up to the uncompressed output size. An I/O error while moving staged files into place can leave partial output.

Errors

FailureThrown
Not a well-formed archive of its detected formatError with code ERR_ARCHIVE_DECODE_FAILED
A format or compression method the runtime does not readError with code ERR_ARCHIVE_FORMAT_UNSUPPORTED
An encrypted zipError with code ERR_ARCHIVE_ENCRYPTED
An unsafe path, a link, a special entry, a duplicate or a collisionError with code ERR_ARCHIVE_UNSAFE_PATH
Over maxEntries or maxBytesRangeError with code ERR_ARCHIVE_TOO_LARGE
entry.bytes() for a name not in the archiveError with code ERR_NOT_FOUND
A bad argument or optionTypeError
Reading or writing a file(), or extractingThe runtime:fs error (NotAllowedError, ERR_NOT_FOUND, …)

See also

  • Archives guide — bundling downloads and unpacking uploads.

  • Internals: archives — what is validated, what is held in memory, and why extraction stages before writing.

Last updated on
Edit this page