Validation
Archive.open validates the entire index before returning. Every later
lookup and read therefore works from data that has already been bounds checked,
which is what lets read skip re-validating on the hot path.
Contents are not read by open. A corrupt data region surfaces from
read, verify, or unpack
as HashMismatch, never from opening the archive.
The order of checks
Section titled “The order of checks”- Magic and version. The first four bytes must be
ZPAK, the nextu32must be1. - Entry count against file size. A count is rejected if the file is too small to hold that many minimum-sized records. This runs before any allocation, so a header claiming four billion entries cannot make zpack allocate for four billion entries.
- Each entry record, in order - path length, path bytes, path validity, then the five numeric fields and the method byte.
- Every offset and size, once the data region’s start is known.
- Path conflicts, across the whole set.
What gets rejected
Section titled “What gets rejected”| Error | Cause |
|---|---|
BadMagic |
The file does not start with ZPAK - including a file shorter than four bytes |
UnsupportedVersion |
The version field is not 1 |
UnsupportedMethod |
An entry’s method byte is neither 0 nor 1 |
CorruptArchive |
Truncated index; an entry_count larger than the file could hold; an offset that lands inside the index or past EOF; an offset + stored_size that overflows u64; a size larger than stored_size could ever inflate to; or a stored entry whose stored_size differs from its size |
InvalidPath |
Empty; empty segment; a . or .. segment; a \, :, or NUL byte anywhere; or not valid UTF-8 |
PathTooLong |
Longer than 65535 bytes |
DuplicatePath |
The same path appears twice in the index |
PathConflict |
One entry’s path is a directory prefix of another’s, so both cannot exist on a filesystem at once |
AssetIdCollision |
Two different paths hash to the same asset id |
Full context for each, including which library calls produce it, is in the error reference.
Why these particular checks
Section titled “Why these particular checks”Entry count before allocation
Section titled “Entry count before allocation”The index is allocated up front, sized by entry_count. A corrupt or hostile
header could otherwise name a count that costs gigabytes to allocate. Comparing
the count against the smallest index that many entries could occupy bounds the
allocation by the file’s actual size.
Offsets bounded to the data region
Section titled “Offsets bounded to the data region”An offset below the data region’s start would let an entry read the index as if
it were file contents. An offset past EOF would read nothing at all. Both are
rejected, and offset + stored_size is checked for overflow rather than allowed
to wrap into a small number.
size bounded by stored_size
Section titled “size bounded by stored_size”This is the decompression-bomb bound. stored_size is already known to fit
inside the file, so size is checked against the most that many bytes could
possibly inflate to:
/// Densest possible deflate encoding: a 258 byte match in two bits.pub const max_deflate_ratio: u64 = 1032;That ratio is deflate’s theoretical ceiling - no honest entry reaches it, so a
larger declared size means the index is lying. Once size is bounded, every
later use of it is bounded too: a buffer length, an allocation, a running total.
Multiplication saturates rather than wrapping, so an absurd stored_size yields
a huge ceiling rather than a small one.
Stored entries must not lie about their length
Section titled “Stored entries must not lie about their length”A store entry is written verbatim, so stored_size and size describe the
same bytes. A mismatch means the index is lying about how many bytes to read
back, and is rejected as CorruptArchive.
Path conflicts
Section titled “Path conflicts”An index holding both data and data/config.json describes something a
filesystem cannot represent - creating either makes the other impossible. Such
an archive cannot be extracted at all.
Catching it at open keeps unpack from failing partway and
leaving a half-written tree behind. The check builds the set of directories
every path implies, then rejects any entry whose own path appears in it.
Asset id collisions
Section titled “Asset id collisions”Two paths hashing to the same u64 would make one of them unreachable through
findId. Rather than serve the wrong asset, open refuses the
archive - and pack refuses to create one.
Because open rejects collisions, an id obtained from an archive is
unambiguous, and find can key one hash map on ids while still serving path
lookups.
Validation at read time, not just open time
Section titled “Validation at read time, not just open time”Path validity is checked when the index is read - before a single byte is
written by unpack. That ordering is what confines extraction
to its destination directory:
- Absolute paths,
..segments, and empty segments are rejected, so a path cannot climb out of the destination. \is rejected so a path cannot mean one thing on Linux and another on Windows.:is rejected anywhere, not just after a drive letter. On Windows a colon names an alternate data stream, socfg:bak.txtwould write bytes into a stream oncfg- somewhereunpacknever reports.- NUL is rejected because it truncates a path at the syscall boundary.
- Non-UTF-8 is rejected because it cannot round-trip through a filesystem reliably.
Bounded decompression
Section titled “Bounded decompression”The index states each entry’s original size up front, so reading stops there.
Internally the reader is given a limit of size + 1 bytes: one past what was
promised, so a stream that keeps producing is detected as a length mismatch
rather than read to exhaustion.
A deflated entry that expands past its declared size fails. It is never written out and never buffered.
What is not checked
Section titled “What is not checked”- Contents.
openreads only the index. Useverifyfor the rest. - Overlapping entries. Two entries may point at the same or overlapping byte ranges. Each is individually in-bounds and hash-checked on read, so an overlap is odd but not unsafe.
- Gaps. Bytes in the data region that no entry references are ignored.
- Authenticity. See the security model.