///|
/// The ZIP reader: central-directory parsing, local-record validation, bounded
/// materialization of every entry, and the raw source records retained for
/// byte-preserving rewrites.
///|
priv struct EntryMeta {
name : String
flags : Int
method_id : Int
crc : UInt
compressed_size : Int
uncompressed_size : Int
local_offset : Int
central_record : Bytes
central_zip64_offset_position : Int?
data_descriptor : Bool
}
///|
/// The parsed local header region of one entry, with the byte range of its
/// payload plus its (optional) data descriptor.
priv struct LocalRecord {
compressed : BytesView
end : Int
}
///|
fn read_local_record(
bytes : BytesView,
meta : EntryMeta,
central_offset : Int,
) -> LocalRecord raise ZipError {
guard meta.local_offset >= 0 && meta.local_offset < central_offset else {
raise ZipError(Truncated, "zip: local header offset out of bounds")
}
let reader = Cursor(bytes, meta.local_offset)
guard reader.read_u32_raw() == LOCAL_HEADER_SIG else {
raise ZipError(InvalidSignature, "zip: invalid local header signature")
}
ignore(reader.read_u16())
let flags = reader.read_u16()
ensure_flags_supported(flags)
guard flags == meta.flags else {
raise ZipError(UnsupportedFeature, "zip: local and central flags differ")
}
let method_id = reader.read_u16()
guard method_id == meta.method_id else {
raise ZipError(UnsupportedFeature, "zip: local and central methods differ")
}
ignore(reader.read_u16())
ignore(reader.read_u16())
let local_crc = reader.read_u32_raw()
let comp_size_raw = reader.read_u32_raw()
let uncomp_size_raw = reader.read_u32_raw()
let name_len = reader.read_u16()
let extra_len = reader.read_u16()
let local_name = reader.read_bytes(name_len)
let local_extra = reader.read_bytes(extra_len)
guard decode_name(local_name) == meta.name else {
raise ZipError(UnsupportedFeature, "zip: local and central names differ")
}
let has_descriptor = (flags & FLAG_DATA_DESCRIPTOR) != 0
let need_local_uncomp = uncomp_size_raw == MAX_U32
let need_local_comp = comp_size_raw == MAX_U32
let (local_zip_uncomp, local_zip_comp, _, _) = read_zip64_extra(
local_extra, need_local_uncomp, need_local_comp, false,
)
guard local_crc == meta.crc || (has_descriptor && local_crc == 0) else {
raise ZipError(UnsupportedFeature, "zip: local and central CRC-32 differ")
}
guard local_size_matches(
comp_size_raw,
local_zip_comp,
meta.compressed_size,
has_descriptor,
) &&
local_size_matches(
uncomp_size_raw,
local_zip_uncomp,
meta.uncompressed_size,
has_descriptor,
) else {
raise ZipError(UnsupportedFeature, "zip: local and central sizes differ")
}
guard reader.pos <= central_offset &&
meta.compressed_size <= central_offset - reader.pos else {
raise ZipError(Truncated, "zip: compressed payload out of bounds")
}
let compressed = reader.read_bytes(meta.compressed_size)
let end = if has_descriptor {
// The descriptor's sizes are 64-bit iff the local header declared its
// sizes through the ZIP64 extra field (0xffffffff placeholders).
let zip64 = need_local_uncomp || need_local_comp
let end = reader.pos + data_descriptor_size(bytes, reader.pos, meta, zip64)
guard end <= central_offset else {
raise ZipError(Truncated, "zip: data descriptor out of bounds")
}
end
} else {
reader.pos
}
{ compressed, end, }
}
///|
fn check_sizes(
uncompressed_size : Int,
entry_limit : Int,
total_limit : Int,
used_total : Int,
) -> Unit raise ZipError {
guard uncompressed_size <= entry_limit else {
raise ZipError(
LimitExceeded(EntryUncompressedBytes, entry_limit, uncompressed_size),
"zip: entry exceeds the per-entry uncompressed limit",
)
}
guard used_total >= 0 && uncompressed_size <= total_limit - used_total else {
raise ZipError(
LimitExceeded(
TotalUncompressedBytes,
total_limit,
(used_total + uncompressed_size).min(0x7fff_ffff),
),
"zip: entries exceed the total uncompressed limit",
)
}
}
///|
fn inflate_entry(
compressed : BytesView,
max_output : Int,
limit_kind : LimitKind,
reported_limit : Int,
cancelled : () -> Bool,
) -> Bytes raise ZipError {
let decoded = try
@flate.inflate_exact(
compressed.to_owned(),
max_output=Some(max_output),
cancelled~,
preallocated=true,
)
catch {
InflateError(OutputLimitExceeded, _) =>
raise ZipError(
LimitExceeded(limit_kind, reported_limit, max_output),
"zip: decompressed size limit exceeded",
)
err => raise wrap_inflate_error(err)
} noraise {
decoded => decoded
}
decoded
}
///|
/// Tracks the raw source records retained for byte-preserving rewrites against
/// a caller-supplied ceiling.
priv struct SourceBudget {
limit : Int
mut used : Int
}
///|
fn SourceBudget::SourceBudget(limit : Int) -> SourceBudget {
{ limit, used: 0, }
}
///|
fn SourceBudget::charge(
self : SourceBudget,
amount : Int,
) -> Unit raise ZipError {
guard amount >= 0 else {
raise ZipError(UnsupportedFeature, "zip: negative preserved source size")
}
guard amount <= self.limit - self.used else {
raise ZipError(
LimitExceeded(
PreservedSourceBytes,
self.limit,
(self.used + amount).min(0x7fff_ffff),
),
"zip: preserved source records exceed their limit",
)
}
self.used = self.used + amount
}
///|
/// The parsed end-of-central-directory header: the central directory's bounds,
/// the classic EOCD position, the optional ZIP64 EOCD position, and the archive
/// comment.
priv struct EndOfCentral {
eocd_offset : Int
entry_count : Int
central_size : Int
central_offset : Int
central_boundary : Int
zip64_eocd_offset : Int?
comment : Bytes
}
///|
/// Resolve the ZIP64 end-of-central-directory fields for a classic EOCD whose
/// entry count or central size/offset spilled into the ZIP64 sentinels.
/// Returns (entry_count, central_size, central_offset, zip64_eocd_offset).
fn zip64_central_bounds(
bytes : BytesView,
eocd_offset : Int,
disk_entries : Int,
total_entries : Int,
central_size_raw : UInt,
central_offset_raw : UInt,
) -> (Int, Int, Int, Int) raise ZipError {
let (zip_entries, zip_size, zip_offset, zip64_offset) = read_zip64_eocd(
bytes, eocd_offset,
)
guard disk_entries == 0xFFFF || disk_entries.to_uint64() == zip_entries else {
raise ZipError(
UnsupportedFeature,
"zip: classic and zip64 entry counts differ",
)
}
guard total_entries == 0xFFFF || total_entries.to_uint64() == zip_entries else {
raise ZipError(
UnsupportedFeature,
"zip: classic and zip64 entry counts differ",
)
}
guard central_size_raw == MAX_U32 ||
UInt64::extend_uint(central_size_raw) == zip_size else {
raise ZipError(
UnsupportedFeature,
"zip: classic and zip64 central sizes differ",
)
}
guard central_offset_raw == MAX_U32 ||
UInt64::extend_uint(central_offset_raw) == zip_offset else {
raise ZipError(
UnsupportedFeature,
"zip: classic and zip64 central offsets differ",
)
}
let entry_count = (if total_entries == 0xFFFF {
zip_entries
} else {
total_entries.to_uint64()
}).to_int_checked()
let central_size = (if central_size_raw == MAX_U32 {
zip_size
} else {
UInt64::extend_uint(central_size_raw)
}).to_int_checked()
let central_offset = (if central_offset_raw == MAX_U32 {
zip_offset
} else {
UInt64::extend_uint(central_offset_raw)
}).to_int_checked()
(entry_count, central_size, central_offset, zip64_offset)
}
///|
/// Enforce the entry-count and central-bounds limits and assemble the parsed
/// end-of-central-directory record.
fn validate_central_bounds(
bytes : BytesView,
limits : ReadLimits,
eocd_offset : Int,
entry_count : Int,
central_size : Int,
central_offset : Int,
central_boundary : Int,
zip64_eocd_offset : Int?,
comment : Bytes,
) -> EndOfCentral raise ZipError {
guard entry_count <= limits.max_entries else {
raise ZipError(
LimitExceeded(Entries, limits.max_entries, entry_count),
"zip: entry count exceeds its limit",
)
}
guard central_offset <= central_boundary &&
central_boundary <= bytes.length() &&
central_size == central_boundary - central_offset else {
raise ZipError(Truncated, "zip: central directory out of bounds")
}
{
eocd_offset,
entry_count,
central_size,
central_offset,
central_boundary,
zip64_eocd_offset,
comment,
}
}
///|
/// Locate and validate the end-of-central-directory record (and the ZIP64 EOCD
/// it points at), enforcing the package size, entry count, and central-bounds
/// limits. Returns the parsed bounds for the central-directory phase.
fn parse_end_of_central(
bytes : BytesView,
limits : ReadLimits,
cancelled : () -> Bool,
) -> EndOfCentral raise ZipError {
check_cancelled(cancelled)
guard bytes.length() <= limits.max_package_bytes else {
raise ZipError(
LimitExceeded(PackageBytes, limits.max_package_bytes, bytes.length()),
"zip: package exceeds the input size limit",
)
}
guard find_end_of_central(bytes) is Some(eocd_offset) else {
raise ZipError(
MissingEndOfCentral,
"zip: end of central directory not found",
)
}
let reader = Cursor(bytes, eocd_offset)
ignore(reader.read_u32_raw())
let disk_no = reader.read_u16()
let disk_start = reader.read_u16()
let disk_entries = reader.read_u16()
let total_entries = reader.read_u16()
let central_size_raw = reader.read_u32_raw()
let central_offset_raw = reader.read_u32_raw()
let comment_len = reader.read_u16()
guard disk_no == 0 && disk_start == 0 else {
raise ZipError(
UnsupportedFeature,
"zip: multi-disk archives are not supported",
)
}
let comment = copy_cancellable(reader.read_bytes(comment_len), cancelled)
let needs_zip64 = disk_entries == 0xFFFF ||
total_entries == 0xFFFF ||
central_size_raw == MAX_U32 ||
central_offset_raw == MAX_U32
guard !needs_zip64 else {
let (entry_count, central_size, central_offset, zip64_offset) = zip64_central_bounds(
bytes, eocd_offset, disk_entries, total_entries, central_size_raw, central_offset_raw,
)
return validate_central_bounds(
bytes,
limits,
eocd_offset,
entry_count,
central_size,
central_offset,
zip64_offset,
Some(zip64_offset),
comment,
)
}
guard disk_entries == total_entries else {
raise ZipError(
UnsupportedFeature,
"zip: multi-disk archives are not supported",
)
}
validate_central_bounds(
bytes,
limits,
eocd_offset,
total_entries,
central_size_raw.to_int_checked(),
central_offset_raw.to_int_checked(),
eocd_offset,
None,
comment,
)
}
///|
/// Resolve a central-directory 32-bit field that may spill into a ZIP64 extra
/// entry: the classic field carries the `MAX_U32` sentinel when ZIP64 is in
/// use. `read_zip64_extra` guarantees a ZIP64 value whenever `need` is true (it
/// raises when the field is absent or mis-sized), so `unwrap` is safe there.
fn resolve_zip64_field(
need : Bool,
zip64_value : UInt64?,
classic_raw : UInt,
) -> Int raise ZipError {
guard need else { return classic_raw.to_int_checked() }
zip64_value.unwrap().to_int_checked()
}
///|
/// Parse one central-directory record into `EntryMeta`, enforcing the
/// preserved-source ceiling.
fn parse_central_record(
central_bytes : BytesView,
central_reader : Cursor,
source_budget : SourceBudget,
cancelled : () -> Bool,
) -> EntryMeta raise ZipError {
let central_record_start = central_reader.pos
guard central_reader.read_u32_raw() == CENTRAL_HEADER_SIG else {
raise ZipError(InvalidSignature, "zip: invalid central directory signature")
}
ignore(central_reader.read_u16())
ignore(central_reader.read_u16())
let flags = central_reader.read_u16()
ensure_flags_supported(flags)
let method_id = central_reader.read_u16()
ignore(central_reader.read_u16())
ignore(central_reader.read_u16())
let crc = central_reader.read_u32_raw()
let comp_size_raw = central_reader.read_u32_raw()
let uncomp_size_raw = central_reader.read_u32_raw()
let name_len = central_reader.read_u16()
let extra_len = central_reader.read_u16()
let comment_len = central_reader.read_u16()
let disk = central_reader.read_u16()
ignore(central_reader.read_u16())
ignore(central_reader.read_u32_raw())
let local_offset_raw = central_reader.read_u32_raw()
let name_bytes = central_reader.read_bytes(name_len)
let extra_bytes = central_reader.read_bytes(extra_len)
let name = decode_name(name_bytes)
central_reader.skip(comment_len)
guard disk == 0 else {
raise ZipError(
UnsupportedFeature,
"zip: multi-disk archives are not supported",
)
}
source_budget.charge(central_reader.pos - central_record_start)
let central_record = copy_cancellable(
central_bytes[central_record_start:central_reader.pos],
cancelled,
)
let need_uncomp = uncomp_size_raw == MAX_U32
let need_comp = comp_size_raw == MAX_U32
let need_offset = local_offset_raw == MAX_U32
let (zip_uncomp, zip_comp, zip_offset, offset_position) = read_zip64_extra(
extra_bytes, need_uncomp, need_comp, need_offset,
)
let uncompressed_size = resolve_zip64_field(
need_uncomp, zip_uncomp, uncomp_size_raw,
)
let compressed_size = resolve_zip64_field(need_comp, zip_comp, comp_size_raw)
let local_offset = resolve_zip64_field(
need_offset, zip_offset, local_offset_raw,
)
let central_zip64_offset_position = offset_position.map(position => {
46 + name_len + position
})
{
name,
flags,
method_id,
crc,
compressed_size,
uncompressed_size,
local_offset,
central_record,
central_zip64_offset_position,
data_descriptor: (flags & FLAG_DATA_DESCRIPTOR) != 0,
}
}
///|
/// Parse every central-directory record into `EntryMeta`, enforcing the entry
/// count and preserved-source ceilings.
fn parse_central_directory(
bytes : BytesView,
eocd : EndOfCentral,
limits : ReadLimits,
source_budget : SourceBudget,
cancelled : () -> Bool,
) -> Array[EntryMeta] raise ZipError {
let central_bytes = bytes[eocd.central_offset:eocd.central_boundary]
let central_reader = Cursor(central_bytes, 0)
let max_plausible = eocd.central_size / 46
let metas : Array[EntryMeta] = Array::new(
capacity=eocd.entry_count.min(max_plausible),
)
let mut used_total = 0
for entry_index in 0.. Bool,
) -> TrailerTemplate raise ZipError {
guard eocd.zip64_eocd_offset is Some(zip64_offset) else {
source_budget.charge(bytes.length() - eocd.eocd_offset)
return {
classic_end_record: copy_cancellable(bytes[eocd.eocd_offset:], cancelled),
zip64_end_record: None,
zip64_locator: None,
}
}
let locator_offset = eocd.eocd_offset - 20
source_budget.charge(
locator_offset - zip64_offset + 20 + (bytes.length() - eocd.eocd_offset),
)
{
classic_end_record: copy_cancellable(bytes[eocd.eocd_offset:], cancelled),
zip64_end_record: Some(
copy_cancellable(bytes[zip64_offset:locator_offset], cancelled),
),
zip64_locator: Some(
copy_cancellable(bytes[locator_offset:eocd.eocd_offset], cancelled),
),
}
}
///|
/// Materialize one entry: validate its local record against the central
/// metadata, decompress or copy its payload under the caller's limits, and
/// retain the raw source records for byte-preserving rewrites.
fn decode_entry(
bytes : BytesView,
meta : EntryMeta,
central_offset : Int,
limits : ReadLimits,
actual_total : Int,
source_budget : SourceBudget,
cancelled : () -> Bool,
) -> Entry raise ZipError {
check_cancelled(cancelled)
let record = read_local_record(bytes, meta, central_offset)
source_budget.charge(record.end - meta.local_offset)
let entry_limit = limits.max_entry_uncompressed_bytes
let total_limit = limits.max_total_uncompressed_bytes
let remaining = total_limit - actual_total
let (effective_cap, limit_kind, reported_limit) = if remaining < entry_limit {
(remaining, TotalUncompressedBytes, total_limit)
} else {
(entry_limit, EntryUncompressedBytes, entry_limit)
}
let compression = method_from_code(meta.method_id)
let data = {
guard !(record.compressed.length() == 0 && meta.uncompressed_size == 0) else {
b""
}
match compression {
Store => {
guard record.compressed.length() <= entry_limit else {
raise ZipError(
LimitExceeded(
EntryUncompressedBytes,
entry_limit,
record.compressed.length(),
),
"zip: stored entry exceeds its limit",
)
}
copy_cancellable(record.compressed, cancelled)
}
Deflate =>
inflate_entry(
record.compressed,
effective_cap,
limit_kind,
reported_limit,
cancelled,
)
}
}
guard data.length() == meta.uncompressed_size else {
raise ZipError(
UnsupportedFeature,
"zip: decoded size does not match central directory",
)
}
guard data.length() <= total_limit - actual_total else {
raise ZipError(
LimitExceeded(
TotalUncompressedBytes,
total_limit,
(actual_total + data.length()).min(0x7fff_ffff),
),
"zip: entries exceed the total uncompressed limit",
)
}
let source = Some({
local_record: copy_cancellable(
bytes[meta.local_offset:record.end],
cancelled,
),
central_record: meta.central_record,
central_zip64_offset_position: meta.central_zip64_offset_position,
})
{
name: meta.name,
data,
compression,
crc32: meta.crc,
data_descriptor: meta.data_descriptor,
compressed_size: meta.compressed_size,
source,
origin_local_offset: Some(meta.local_offset),
}
}
///|
fn read_impl(
bytes : BytesView,
limits : ReadLimits,
cancelled : () -> Bool,
) -> Archive raise ZipError {
let eocd = parse_end_of_central(bytes, limits, cancelled)
let source_budget = SourceBudget(limits.max_preserved_source_bytes)
let metas = parse_central_directory(
bytes, eocd, limits, source_budget, cancelled,
)
let archive = {
entries: [],
comment: eocd.comment,
trailer: Some(build_trailer(bytes, eocd, source_budget, cancelled)),
}
let mut actual_total = 0
for meta in metas {
let entry = decode_entry(
bytes,
meta,
eocd.central_offset,
limits,
actual_total,
source_budget,
cancelled,
)
actual_total = actual_total + entry.data().length()
archive.entries.push(entry)
}
check_cancelled(cancelled)
archive
}
///|
/// Read a ZIP archive from `bytes`, decoding every entry. `limits` bounds the
/// package size, entry count, per-entry and total decompressed bytes, and the
/// source records retained for byte-preserving rewrites; `cancelled` is polled
/// across parsing and decoding. Each entry exposes the central directory's
/// stored CRC-32 via `Entry::crc32`; it is not verified during this read —
/// callers that need integrity checking compare it against
/// `@checksum.crc32(entry.data())`. Failures carry a stable `ZipErrorKind`
/// plus a diagnostic.
pub fn read(
bytes : BytesView,
limits? : ReadLimits = ReadLimits::default(),
cancelled? : () -> Bool = () => false,
) -> Archive raise ZipError {
read_impl(bytes, limits, cancelled)
}