Everything you can do with a ZIP file
Not “unzip it”. A ZIP archive carries a complete, precise description of itself in its last few kilobytes, and that description is enough to browse it, pull one file out of it, prove every member is intact, spot the ways it has been tampered with, and draw a map of every byte in it — without unpacking anything. This is a tour of what omniviewer.org/zip does with that, tab by tab, and of the format underneath that makes each one possible. Everything here runs in your browser: no upload, no install, no size limit worth naming.
First: half the files you own are ZIP files
A ZIP is a container. Compress some files independently, write each one behind a small header, then write an index of all of them at the end. Phil Katz published it in 1989 and put the specification in the public domain, which is the entire reason it won — and why so much of the software world is literally a ZIP file with a different extension:
| Extension | What it is | Where it opens |
|---|---|---|
.zip | The archive itself | /zip |
.jar .war | Java archive | /zip |
.apk .ipa | Android / iOS app package | /zip |
.xlsx .pptx | Office Open XML | /zip |
.epub | E‑book | /zip |
.whl .nupkg | Python wheel, NuGet package | /zip |
.xpi .crx | Browser extension | /zip |
.docx | Word document (also a ZIP) | /docx, which reads the XML inside |
Routing is by content, not by name: the sniff reads the first four bytes, 50 4B 03 04 — PK, Phil Katz’s initials, then a version number — so a .jar renamed backup.dat still lands on the ZIP toolkit. If you want the format itself in depth, with field tables and parsing code, that is the companion piece: inside a ZIP file — the central directory and DEFLATE. This page is about what you can do.
Why a 20 GB archive opens as fast as a 20 KB one
The index sits at the end of the file. A ZIP ends with a 22‑byte End of Central Directory record that names the offset and size of the central directory, and the central directory holds one record per member: name, sizes, CRC‑32, compression method, permissions, timestamp, and the offset of that member’s own bytes.
So listing an archive costs exactly two reads — a ~64 KB tail window, then the directory range that window points at. The compressed payloads are never touched. The cost of opening an archive scales with how many entries it has, not how much it weighs.
In a browser this matters more than it sounds. A File from a drop or a picker is a handle, not bytes; file.slice(a, b) is a lazy range read. Nothing is loaded until something awaits it. The same path runs against a URL with HTTP Range requests, so ?url= inspects a remote archive without downloading it. All of it happens in a Web Worker, so the page never freezes.
Browse it — the ENTRIES tab
The default view is the archive as a real folder tree, built from the path segments in the directory. (ZIP has no folders: a directory entry is just a zero‑length member whose name ends in /, and it is optional — plenty of archives contain src/lib/util.js with no entry for src/ at all, so the tree is reconstructed rather than read.)
Every row carries what the directory knows about that member:
- Unpacked and packed size, and the compression ratio between them.
- Method — stored or deflate in almost every real archive. A well‑built one stores its already‑compressed members (PNGs, JPEGs, MP4s) and deflates its text; you can see that decision at a glance.
- CRC‑32, unix permissions and the timestamp.
- A filter box over name and path, for archives where scrolling is not a plan.
Preview decompresses one member on the spot and shows it inline — text up to 2 MB, images up to 32 MB — so you can read a manifest, check a config or look at a logo without saving anything. Only that member’s byte range is read.
Open a member in its own toolkit
A preview is a convenience. The stronger move is to hand the member back to OmniViewer’s own format detection: click a config.json inside an archive and you get /json’s entire tab set — formatting, tree, graph, search — not a text blob. A PNG inside an .epub gets the PNG chunk viewer. An XML part inside an .xlsx gets the XML toolkit.
The archive stays above you as a breadcrumb, and the browser’s Back button walks it. Because a member can itself be an archive, this nests — a JAR inside a WAR inside a ZIP — and because every ancestor keeps its file alive to make the walk back possible, the nesting is bounded: 5 levels deep, 1 GB for any single opened member, 10 GB held across the whole chain. Each refusal says which limit it hit and leaves the download path open; nothing is ever silently truncated.
LICENSE or photo.bin has no useful extension, so its toolkit is chosen from the sniff of its actual bytes — which is the only thing that works when the name inside an archive is whatever the person who built it typed.
Extract — one file, or a selection, straight to disk
Pulling a single member out is four small reads: the local header (its own name and extra‑field lengths decide where the data starts), that member’s compressed range, an inflate through the browser’s built‑in DecompressionStream('deflate-raw'), and a CRC‑32 check against the value the directory recorded. If the CRC disagrees, you are told — a corrupt member is reported, not silently saved.
Beyond one file, every file and folder has a checkbox (indeterminate on a partial folder), with a running count and unpacked total, and one Extract button. What happens then depends on what the browser can do:
- Where the browser can write a folder (Chromium’s directory picker), each member is streamed from the decompressor straight into a file on disk and never held whole. That is the point of it: a 50 GB archive unpacks without 50 GB of memory.
- Everywhere else, members are saved one at a time as ordinary downloads — and the count is stated up front, so the browser’s “allow multiple downloads?” prompt is not a surprise.
.. segment, or an absolute, drive‑qualified or UNC path, is rejected outright. Stripping the traversal would silently re‑root the entry somewhere nobody asked for and turn a detectable attack into a confusing one. Backslashes are normalised to separators first — a sanitiser that only looks for / is exactly what a ..\..\ walks past.
Only members the browser can actually decompress are selectable. Offering a tick for a bzip2 entry would promise something Extract could not keep.
Unlock it — encrypted archives
When an archive has encrypted members, a password field appears above the tree. Type the password once and every locked entry previews, opens and extracts like any other. Both schemes a ZIP can carry are supported:
- WinZip AES — real AES at 128, 192 or 256 bits. PBKDF2‑HMAC‑SHA1 (1000 iterations) derives an encryption key, an authentication key and a two‑byte password verifier; the payload is AES‑CTR with WinZip’s little‑endian counter starting at 1, authenticated by a trailing HMAC‑SHA1.
- ZipCrypto — the legacy scheme, cryptographically broken for decades and still written by older tools. Supported because files in the wild use it, not because it protects anything.
A wrong password is answered instantly, from the archive’s own verifier byte, rather than from a decompression that failed for reasons unknown. Decryption is a stream, not a buffer, so an encrypted member extracts straight to disk and verifies at any size like an unencrypted one.
The password is sent to the Web Worker, tested there, and kept in that worker’s memory only. It is never returned to the page, never stored, never put in a URL; closing the file forgets it.
Audit it — what is decidable without decompressing
ZIP’s security problems are consequences of its design rather than bugs in any one tool, and nearly all of them are visible in the index. So AUDIT answers “does this archive look dangerous?” instantly, from the same two reads that listed it, decompressing nothing:
| Finding | What it means |
|---|---|
| Zip slip | An entry name with a .. segment. A naive extractor joins it to the output directory and writes outside it. |
| Absolute / backslash paths | The same attack in its other spellings: /etc/passwd, C:\Windows\…, ..\..\. |
| Zip bombs | A member whose declared expansion ratio is far past what real data reaches — and the archive‑wide version, where the unpacked total dwarfs the file on disk. Both readable from the declared sizes, before paying for them. |
| Header disagreement | A local header that contradicts the central directory. Scanners list from one copy and extractors read from the other, which is precisely the Android Master Key shape. |
| Missing local header | A directory record pointing at a place with no header in it. |
| Encryption | Which members are locked, under which scheme, at which strength. |
| Symlinks | A tiny member whose contents are a target path. Extract a link to /etc, then a member “into” it, and the write escapes without any name containing ... |
| Duplicate names | Two members called config.json. Which one lands on disk is the extractor’s ordering — so one can shadow the copy a reviewer read. |
| Executable payloads & exec bits | Installers and scripts, and members carrying the unix executable bit. |
| Offsets out of range | Entry offsets pointing past the member area — a broken or hand‑built archive. |
| Trailing / prepended data | Bytes after the archive ends (a detached signature, a second archive, a smuggled payload) or in front of it (a self‑extracting stub). |
| Split archives, future timestamps, missing timestamps, truncated listings | Reported for what they are: structural facts, not verdicts. |
Findings are sorted by severity and the checks that passed are listed too — “no path traversal in any entry name” is an answer, and an audit that only ever shows problems leaves you guessing whether it looked.
Verify it — every member, every CRC
AUDIT’s expensive twin, and the pair is the product. AUDIT asks whether an archive looks dangerous and answers from the index; VERIFY asks whether every member is actually intact, and pays for it: each entry is decompressed and folded chunk by chunk into a running CRC‑32, then compared against the value the central directory promised. It starts only on an explicit click, shows progress, and can be cancelled.
Every output chunk is discarded as soon as it has been folded in, so peak memory is one stream chunk rather than one member — a member far larger than the 512 MB single‑entry extraction cap still verifies. This is the tab for “did this download finish properly?” and “is this backup still good?”
Per‑entry results are reported rather than aborting the run, and skipped is held apart from failed: a member that could not be checked (a locked entry with no password typed, a bzip2 entry) is a different claim from one that is broken. A WinZip AES entry in AE‑2 mode stores its CRC as zero by design — integrity there comes from the trailing HMAC — so it is reported as unchecked, not as a failure.
Map it — the STRUCTURE tab
A hex viewer shows you bytes. STRUCTURE shows you what the bytes are: every structural record listed at its real file offset, ordered by offset — which is not the same as ordered by entry.
- Every local file header, its compressed payload, and any data descriptor trailing it.
- The central directory, the ZIP64 record and locator when present, and the EOCD.
- Extra fields decoded, not counted: ZIP64 sizes, Info‑ZIP and NTFS timestamps, unix uid/gid, Unicode path fields and WinZip AES parameters are read out. An undocumented vendor field is still shown, at its real size, as hex.
- The gaps. Space belonging to no record is emitted as its own row — so a prepended self‑extracting stub, padding between members, or a payload appended after the archive ends becomes something you can see at its real size rather than something you suspect.
- Where a local header disagrees with the directory, the row says so.
Measure it — the STATS tab
Archive‑wide tiles: files, folders, unpacked size, overall compression ratio, space saved, whether ZIP64 is in use. Then the compression‑method mix, the largest file types, the biggest members, and the complete entry table in a virtual‑scroll view that stays smooth at two hundred thousand rows. A persistent info bar across every tab carries size · entries · ratio · the audit verdict.
Read the bytes — RAW, HEX and STRINGS
Under all of it, the file itself: the raw byte view, a hex viewer that scrolls a 20 GB file as comfortably as a small one (it renders a window, not a document), and a STRINGS scan that pulls the readable text out of a binary. Nothing about these is ZIP‑specific, which is exactly why they are useful when something is malformed enough that the structured tabs disagree with each other.
Here is what the tabs above are reading, in the shipped sample archive. Every dump below is the real bytes of project-release.zip, which you can open and check.
The first member’s local file header, at offset 0
00000000: 50 4b 03 04 0a 00 00 08 00 00 24 4d ee 5c 00 00 PK........$M.\..
00000010: 00 00 00 00 00 00 00 00 00 00 12 00 09 00 68 61 ..............ha
00000020: 72 62 6f 72 2d 6c 6f 6f 70 2d 31 2e 34 2e 30 2f rbor-loop-1.4.0/
00000030: 55 54 05 00 01 34 04 56 6a 50 4b 03 04 14 00 00 UT...4.VjPK.....
50 4b 03 04 is the signature every ZIP starts with — this is the four bytes the sniff routes on. Then version 10, flags 0x0800 (bit 11: the name is UTF‑8), method 0 = stored, DOS time and date. The three zeroed 32‑bit fields are CRC and both sizes — correct here, because this member is the directory harbor-loop-1.4.0/ and has no contents. Name length 12 00 = 18, extra length 09 00 = 9. The extra field is 55 54 = UT, Info‑ZIP’s extended timestamp, carrying the real modification time the DOS field can only round to two seconds. And at 0x39 the next PK 03 04 already begins: members sit back to back, each one independently compressed.
The same member, described a second time — the central directory at 0x2b2d
00002b2d: 50 4b 01 02 1e 03 0a 00 00 08 00 00 24 4d ee 5c PK..........$M.\
00002b3d: 00 00 00 00 00 00 00 00 00 00 00 00 12 00 09 00 ................
00002b4d: 00 00 00 00 00 00 10 00 ed 41 00 00 00 00 68 61 .........A....ha
00002b5d: 72 62 6f 72 2d 6c 6f 6f 70 2d 31 2e 34 2e 30 2f rbor-loop-1.4.0/
50 4b 01 02, and then almost the same fields again — ZIP stores every member’s metadata twice. 1e 03 is “version made by”: high byte 03 = UNIX, which is what makes the next highlighted field meaningful. 10 00 ed 41 is the external attributes, 0x41ed0010, whose high sixteen bits 0x41ed are the unix mode 040755 — a directory, rwxr-xr-x. That is where the permission column in ENTRIES comes from. The four zero bytes after it are this member’s local header offset: 0, pointing back at the dump above. The two copies can disagree, nothing in the spec requires a tool to check, and different tools trust different copies — which is the whole Master Key family of attacks, and why AUDIT and STRUCTURE both cross‑check them.
The 22 bytes that make it all work — the EOCD at 0x303c
0000303c: 50 4b 05 06 00 00 00 00 0f 00 0f 00 0f 05 00 00 PK..............
0000304c: 2d 2b 00 00 5b 00 68 61 72 62 6f 72 2d 6c 6f 6f -+..[.harbor-loo
0000305c: 70 20 31 2e 34 2e 30 20 e2 80 94 20 70 61 63 6b p 1.4.0 ... pack
50 4b 05 06, then: 0f 00 = 15 entries, directory size 0x0000050f = 1295 bytes, directory offset 0x00002b2d — the exact address of the previous dump. Comment length 5b 00 = 91, and the comment itself follows in plain text (“harbor‑loop 1.4.0 — packaged 2026‑07‑14…”). Those sixteen bytes are the entire reason a 20 GB archive lists instantly: find this record in a tail window, follow its offset, stop. Note the consequence of the comment — the record can sit up to 65,535 bytes from the end of the file, and a comment (or a nested ZIP, or a compressed payload) can contain PK 05 06 by chance, so the search runs backwards and the last consistent match wins.
And an encrypted member — locked-release.zip, at offset 0
00000000: 50 4b 03 04 14 00 01 00 63 00 00 60 1d 5d 00 00 PK......c..`.]..
00000010: 00 00 4a 01 00 00 ea 01 00 00 1d 00 0b 00 68 61 ..J...........ha
00000020: 72 62 6f 75 72 2d 6c 6f 6f 70 2d 31 2e 34 2e 30 rbour-loop-1.4.0
00000030: 2f 52 45 41 44 4d 45 2e 74 78 74 01 99 07 00 02 /README.txt.....
00000040: 00 41 45 03 08 00 e5 11 17 8f ff 7d 3b 28 46 40 .AE........};(F@
00000050: 89 ce eb e7 05 54 14 96 a7 26 87 25 54 4d 28 2e .....T...&.%TM(.
01 00 — bit 0 set, this member is encrypted. Method reads 63 00 = 99, which is not a compression method at all but WinZip AES’s marker; the real method is hidden in the extra field. And there it is at 0x3b: header ID 0x9901 (01 99 little‑endian), body length 7, version 2 (AE‑2), vendor tag AE, strength 03 = AES‑256, actual method 0x0008 = deflate. From 0x46 the payload begins with a 16‑byte random salt (8, 12 or 16, by strength) followed by the two‑byte password verifier — which is what lets a wrong password be rejected immediately instead of after a failed inflate. Note also the CRC field at 0x0e: zero, as AE‑2 specifies, because integrity here is the HMAC‑SHA1 trailing the ciphertext. That is exactly why VERIFY reports such members as unchecked rather than broken. This archive ships as a sample; its password is omniviewer.
The limits, stated
Every ceiling below is deliberate, and every one of them is reported when it is hit — a viewer that quietly lists fewer files than the archive contains is worse than one that says it stopped.
| Limit | Value | Why |
|---|---|---|
| Archive size | no practical limit | Only the index is read; payloads are ranged reads. |
| Entries listed | 200,000 · 64 MB of directory | A directory claiming ten million members would build a ten‑million‑element array on the heap. |
| Single‑entry extraction | 512 MB | An extracted member is materialised whole in memory. |
| Streamed extraction | uncapped | Written chunk by chunk to disk, never held. |
| VERIFY per member | uncapped | Chunks are folded into a CRC and discarded. |
| STRUCTURE rows | 5,000 entries | One extra local‑header read per listed entry. |
| Inline preview | 2 MB text · 32 MB image | A preview is a convenience; past it the row offers a download. |
| Nesting | 5 levels · 1 GB per member · 10 GB per chain | Every ancestor stays open so the breadcrumb can walk back. |
Two format-level notes. ZIP64 is handled, so archives past 4 GB and past 65,535 entries read correctly. And the codecs that can be decompressed are stored and deflate, which between them cover essentially every ZIP in the wild; bzip2, LZMA, XZ and Zstandard members are listed, measured and audited in full, but not unpacked — the browser ships DEFLATE, and the parsing path here carries no compression library.
Try all of it
No upload, no server, no account. The archive is read by your browser and never leaves your computer — and neither does anything you extract from it.
omniviewer. It goes to a Web Worker and no further.
Drop your own ZIP, JAR, APK, XLSX or EPUB →
Any size. The table of contents costs two reads, whatever the archive weighs.
Read next
- Inside a ZIP file: the central directory and DEFLATE — the format itself, with the full field tables and real parsing code.
- How Word stores your document — what happens when the ZIP you opened turns out to be a
.docx. - How we auto-detect format — why a renamed archive still lands on the right toolkit, inside or outside a container.
- The format map — every format OmniViewer opens and the tabs each one gets.
References
- PKWARE, APPNOTE.TXT — the ZIP File Format Specification (ZIP64 is §4.5, encryption §7).
- WinZip, AES Encryption Information: Encryption Specification AE-1 and AE-2.
- RFC 1951 — DEFLATE Compressed Data Format Specification, version 1.3.
- RFC 2898 — PBKDF2; RFC 2202 — HMAC‑SHA1 test vectors.
- Info-ZIP, proginfo/extrafld.txt — the extra-field IDs the spec leaves to convention.
- David Fifield, A better zip bomb (2019); Snyk, Zip Slip (2018).