runlocally

runlocally engineering notes

Unzip

How Unzip is built

By Geppetto · · Open Unzip →

These are the engineering notes for Unzip: the technologies it is built on, what each one is, and how it is used in the tool.

Tech used

Central directory vs. local file headers

A ZIP stores each entry twice over in metadata. Right before an entry’s (usually deflate-compressed) bytes sits a local file header — signature PK\x03\x04, then the compression method, CRC-32, sizes, and the file name. Then, at the very end of the archive, the whole set of entries is repeated in the central directory: a compact index that lists every entry’s name, sizes, timestamp, flags, and the byte offset of its local header. A reader normally works from that index — seek to the end, read the directory, and it knows every entry without touching a single payload. (The central directory was introduced in the Create ZIP notes; the local-header side matters here, and the Recover ZIP notes lean on it heavily.)

Listing with @zip.js/zip.js: ZipReader.getEntries()

The archive is read with the @zip.js/zip.js ZipReader (the client-side ZIP library from the Create ZIP notes), wrapped around a BlobReader(file). getEntries() parses only the central directory, so listing reads a small index rather than decompressing anything — it scales to large archives, and it works on encrypted archives too, because the index itself is normally not encrypted (only the entry data is). Each returned entry carries filename, uncompressedSize, compressedSize, lastModDate, encrypted, and filenameUTF8.

Extracting per entry: getData(new BlobWriter())

entry.getData(new BlobWriter()) decompresses a single entry on demand into a Blob. Extracting one file opens a fresh ZipReader, finds the entry by name, and closes the reader in a finally — so no reader is held open across the UI’s lifetime. Directory and encrypted entries reject here rather than returning empty or garbage bytes.

Flagged states: encrypted and non-UTF-8 names

Two per-entry flags from getEntries() drive how an entry is shown. An encrypted entry is listed but marked locked and not decoded — removing a password you know is a separate job, handled by Unlock ZIP. When filenameUTF8 is false and the name contains bytes above U+007F, the name may be mojibake — the Shift_JIS / CP932 problem from the Fix ZIP Filenames notes — so those entries point to Fix ZIP Filenames.

Shell

Same static Astro + Preact island and Service-Worker PWA shell as the other tools (see the HEIC notes); zip.js runs inflate in the Web Workers described in the Create ZIP notes.

Implementation & operational notes

“Download all” is per-file, not a re-zip. The extract-all path opens the reader once, decompresses every non-directory, non-encrypted entry to a Blob, then triggers one browser download per file — an object URL and a synthetic <a download> for each. There is no re-archiving step; the files land individually. Because several downloads fire in sequence, the browser may show a one-time “allow multiple downloads?” prompt for the site.

Encrypted entries are skipped, not opened. The extract-all pass filters out encrypted entries up front, and single-entry extraction throws on an encrypted entry. Their data can’t be decrypted without a password, so the tool surfaces them as locked instead of writing corrupt output.

Malformed archives throw a clear error. If getEntries() can’t parse the central directory, the failure is caught and reported as “not a readable ZIP,” rather than crashing the island.

Try it / source