How it works
Three decoders, one worker, and a set of rules about what to refuse. This page describes the parts that are unusual enough to be worth explaining.
Which decoder reads your archive
Format comes from the file’s signature, never its name. From there:
- ZIPis read by a parser written for this site. It walks the central directory, so the listing carries the container’s own metadata — per-entry compression method, stored CRC-32, exact compressed size, Zip64 extents, encryption flags. fflate does the DEFLATE.
- TARis likewise read here, including PAX and GNU long-name records. gzip is undone with the browser’s native
DecompressionStream. - RAR and 7z go to libarchive, compiled to WebAssembly. So does any ZIP that is encrypted or uses a compression method the JavaScript path does not implement.
The inspector tells you which one ran, because it changes what you can expect: the JavaScript reader gives per-entry compressed sizes and checksums, and libarchive’s streaming reader does not expose either.
The engine is only fetched when you need it
The WebAssembly binary is about a megabyte. Loading a marketing page does not download it, and neither does opening a ZIP. It is fetched the first time you open an archive that actually needs it, and cached from then on.
It is served from this origin, not a CDN, so no third party learns that you opened a RAR. Before it is instantiated its SHA-256 is compared against a hash fixed at build time:
libarchive.js 2.0.2
sha256 8a66066ed3ea1e6db0469ec752d8fca888a6bf737c6cff21fdbcb62241a72424If those disagree the engine is not run. The same hash is checked again during the build, so a swapped binary fails the deploy rather than reaching a browser.
Why nothing runs on the main thread
Decompression is synchronous, CPU-bound work. Done on the main thread it freezes the page — no scrolling, no cancel button, and eventually a browser warning. Everything here happens in a Web Worker: the page holds a File handle going in and a Blob handle coming out, and neither of those copies the bytes across the boundary.
The decorative ember animation in the header is CSS-only, and it is paused outright while a job is running. A decorative animation competing with a decompression for the compositor is a real cost for no benefit — and a page animating smoothly while the CPU is saturated misrepresents what the machine is doing.
Memory, and why workers get retired
Emscripten’s heap grows to fit the largest thing it has ever expanded and never shrinks. Extract one 200 MB file and the worker keeps a 231 MB heap for as long as it lives — measured, not assumed. Across a hundred-archive batch that becomes a steady climb towards a dead tab.
So a worker whose heap passes 256 MB is terminated once it goes idle, and the next job starts in a fresh one. Re-fetching the engine costs nothing because it is cached; the memory is genuinely reclaimed.
Concurrency follows the device. On a roomy desktop, several archives at once. In Safari or on a phone, exactly one — WebKit’s per-tab ceiling is low enough that two WebAssembly decoders running together can end the session.
Entry paths are treated as hostile
An entry name inside an archive is attacker-controlled text. The classic attack is an entry called ../../../../etc/passwd, which escapes wherever you extracted to. Blocked here, along with absolute paths, Windows drive letters, and — the one that catches naive implementations — names that only become traversal after Unicode normalisation. ../../escape.txt written with fullwidth full stops is not .. to a string comparison, but NFKC maps it to exactly that, and plenty of filesystems normalise on write.
Those entries are blocked and named, not repaired. Silently rewriting ../../etc/passwd to etc/passwd would put a file you never asked for into your output looking entirely legitimate. Cosmetic problems — backslash separators, trailing dots, Windows device names like CON, embedded control characters — are fixed, and the fix is reported.
Collisions get a numbered suffix rather than an overwrite, compared case-insensitively and in NFC, because Windows and macOS both treat README.txt and readme.txt as the same file.
Limits are checked before decompression, not during
Entry count, total expanded size, largest single entry and expansion ratio all come from the archive’s own declared metadata. A 46 KB ZIP claiming to expand to 64 MB of zeros is refused with the ratio shown, rather than being attempted until the tab dies.
Every output is read back
When an archive is finished it is parsed again — with the same central-directory or tar reader an unrelated tool would use — and every entry’s name and size compared against what was written. If that fails, the download is not offered. A corrupt archive that only reveals itself on the recipient’s machine is worse than an error here.
Each output also gets a SHA-256, and each extracted file gets its own, so you can check them independently.
Failure is per-file
One damaged entry does not sink an archive, and one bad archive does not sink a batch. A CRC mismatch or a decoder error is recorded against that entry, the entry is left out, and the walk continues. You get everything that was readable plus a list of what was not and why.
Formats and limits has the specific numbers and the full support matrix.