zip
How ZIP Files Actually Work: Local Headers, Central Directory & the End Record
A developer-friendly deep-dive into the ZIP binary format — the three structural sections every archive contains, why a ZIP reader seeks from the end rather than the beginning, how STORE and DEFLATE differ, and what really happens when a tool reports your archive is corrupt.
You’ve unzipped thousands of files. But here’s the part most developers don’t know: a ZIP reader doesn’t start reading at the beginning of the file. It seeks to the end first. Once you understand why, the entire format makes sense.
The three sections
Every valid ZIP archive has exactly this layout on disk:
- A sequence of local file entries. Each entry is a local file header followed immediately by the compressed (or uncompressed) file data.
- A central directory. A separate listing of every file in the archive, with metadata and byte offsets pointing back to the corresponding local entries.
- An End of Central Directory record (EOCD). A small trailer at the very end that says where the central directory starts and how many entries it contains.
On disk it looks like this:
[Local File Header + Data] ← file 1
[Local File Header + Data] ← file 2
[Local File Header + Data] ← file N
[Central Directory Entry] ← file 1 metadata + offset
[Central Directory Entry] ← file 2 metadata + offset
[Central Directory Entry] ← file N metadata + offset
[End of Central Directory] ← offset to central dir, entry count
The local headers and data all come first. The central directory comes after all of them. The EOCD is always last.
Magic bytes: PK everywhere
Open any ZIP in a hex editor and the first bytes are 50 4B 03 04. That’s ASCII PK followed by \x03\x04. The “PK” stands for Phil Katz, the author of PKZIP — the original ZIP tool from 1989. His initials are embedded in every ZIP file ever created.
Each structural section has its own signature:
| Signature | Hex | What it marks |
|---|---|---|
PK\x03\x04 | 50 4B 03 04 | Local file header |
PK\x01\x02 | 50 4B 01 02 | Central directory entry |
PK\x05\x06 | 50 4B 05 06 | End of Central Directory |
A parser that doesn’t see 50 4B 03 04 at byte offset 0 knows immediately: this isn’t a valid ZIP archive, or the file has been truncated.
Why ZIP is read back-to-front
Here’s the design decision that surprises people: ZIP readers start at the end, not the beginning. The reader seeks to the last few hundred bytes of the file, scans backward for the 50 4B 05 06 EOCD signature, and reads the trailer.
The EOCD record contains:
- The number of entries in the archive.
- The total size of the central directory in bytes.
- The byte offset where the central directory begins.
Armed with that offset, the reader jumps directly to the central directory, enumerates all entries, then seeks to any individual local header on demand — without ever scanning the local data blocks linearly.
This design was intentional. In the 1980s, ZIP archives were often spanned across multiple floppy disks. The central directory living on the last disk (at the end of the last file) meant you could read the entire table of contents from a single disk, without needing disk 1. That constraint is gone today, but the benefit remains: tools like Python’s zipfile module or unzip -l can list every file in a 10 GB archive in milliseconds, because they only read the central directory — a few kilobytes at most — and never touch the compressed data unless asked.
Local headers vs. central directory entries
Both structures store filename, compressed size, uncompressed size, and CRC-32. So why does the format have both?
Local headers exist for streaming writes. When an archiver writes a file, it may not know the final compressed size or CRC before it starts emitting compressed bytes. It writes the local header with placeholder values (zero), streams the data, then appends a data descriptor afterward with the real CRC and sizes. The central directory — written only once, at the very end — picks up the authoritative final values.
The practical rule: writers use local headers; readers use the central directory. A reader only visits a local header when it needs to find the exact byte offset of the actual data to extract.
STORE vs. DEFLATE
Every file in a ZIP is stored with a compression method. Two dominate in practice:
-
STORE (method
0). No compression. The bytes are written verbatim. Compressed size equals uncompressed size. Archivers choose STORE automatically for files that are already compressed — JPEG, PNG, MP4, PDF — where DEFLATE would produce output larger than the input. STORE is also the right choice when you need a predictable, exact output size: because there’s no compression pipeline, you know before writing a single byte exactly how large the entry will be. -
DEFLATE (method
8). The standard ZIP compression algorithm — the same one inside gzip and zlib. It uses LZ77 back-references and Huffman coding. For source code, CSV, JSON, HTML, and similar content, DEFLATE typically cuts file size by 60–80%. For already-compressed formats, it may save nothing or make things worse.
Most archivers pick the method automatically based on file type and a quick trial compression.
Why renaming a file to .zip doesn’t make it a ZIP
A ZIP archive is defined by its binary structure, not its extension. Renaming report.pdf to report.zip produces a file whose first bytes are %PDF- — not 50 4B 03 04 — and that has no EOCD at the end. Any spec-compliant ZIP parser will reject it immediately with a variant of “not a zip file.”
The reverse is also useful to know. .jar (Java), .docx (Word), .apk (Android), and .epub (ebooks) are all valid ZIP archives wearing different extensions. Rename any of them to .zip and your archive manager will list the files inside — because the internal signatures are present and correct.
The file extension is a hint for the operating system. The format lives entirely in the bytes.
What “bad archive” actually means
When an extraction tool reports a corrupt archive, it almost always traces back to one of three specific failures:
- EOCD not found. The tool seeks backward from the end looking for
50 4B 05 06and doesn’t find it. Almost always a truncated download — the file stopped writing before the central directory was appended. - Central directory unreadable. The EOCD was found and points to an offset, but the bytes there aren’t valid
50 4B 01 02entries. Causes include mid-write interruption, accidental binary modification, or an archive that was naively concatenated with something else. - CRC mismatch on extraction. The EOCD and central directory are fine, but when the actual bytes of an entry are decompressed and hashed, the CRC-32 doesn’t match the value stored in the central directory. This means the data itself was corrupted — in transit, in storage, or during a partial overwrite — not the headers.
Tools like unzip -t (test mode) or Python’s zipfile.testzip() validate by re-reading each local entry, decompressing the data, and comparing the computed CRC against what the central directory says it should be.
Seeing it in practice
Because STORE mode gives byte-for-byte predictable output sizes, it’s the right approach when you need a test ZIP of a specific size. The Sample File Generator creates valid, extractable ZIP archives — with real 50 4B 03 04 local headers, a complete central directory, and a proper EOCD — at any exact target size. It uses STORE mode with incompressible fill data so the archive hits the requested size precisely and extracts cleanly in any tool that follows the spec. (It also generates TAR and GZIP archives by the same logic, but not RAR or 7z.)
TL;DR
A ZIP is three sections stacked end-to-end: local data entries, a central directory, and a trailing EOCD. Readers always seek to the EOCD first, recover the central directory’s offset, then navigate by offset rather than scanning linearly. STORE writes bytes verbatim for predictable size; DEFLATE compresses for smaller size. The PK magic bytes and correct headers are what define a ZIP — the extension is irrelevant. When an archive tool reports a bad file, start by checking whether the EOCD and central directory are intact — both live at the end, and both are the first casualties of a truncated or interrupted write.