///|
/// Bounds for streaming one entry. The ceiling applies to actual decoded bytes
/// and also rejects an oversized directory declaration before reading payloads.
#valtype
pub struct EntryReadLimits {
priv max_output_bytes : UInt64
}
///|
/// Construct a decoded-output ceiling. Omission permits any UInt64 byte count.
pub fn EntryReadLimits::EntryReadLimits(
max_output_bytes~ : UInt64 = 0xffff_ffff_ffff_ffffUL,
) -> EntryReadLimits {
{ max_output_bytes, }
}
///|
/// Supply the requested range, resume with empty input and another output
/// buffer, or finish after all integrity checks have succeeded. Every successful
/// step may produce output, including a step returning NeedRead or Done.
pub(all) enum EntryStep {
NeedRead(ReadRange)
NeedMoreOutput
Done
}
///|
priv struct EntryLocalHeader {
crc : UInt32
compressed_size : UInt32
uncompressed_size : UInt32
name_length : Int32
extra_length : Int32
}
///|
priv enum EntryPhase {
Initial
Header
Name(EntryLocalHeader)
Extra(EntryLocalHeader)
PayloadInput(Int32)
Payload
Descriptor(Int32)
Complete
Failed(ZipErrorKind, String)
}
///|
/// Range-driven extraction of one Store or Deflate entry. Created by
/// Directory::open_entry so metadata and archive bounds cannot be forged.
/// Retains at most one 64 KiB compressed response, bounded local metadata, and
/// the inflater's workspace. Sizes and cumulative progress remain 64-bit.
pub struct EntryReader {
priv entry : DirectoryEntry
priv central_offset : UInt64
priv max_output : UInt64
priv crc : @checksum.Crc32
priv mut inflater : @flate.Inflater?
priv mut phase : EntryPhase
priv mut payload_offset : UInt64
priv mut descriptor_zip64 : Bool
priv mut requested : UInt64
priv mut consumed : UInt64
priv mut produced : UInt64
priv mut step_produced : Int32
priv mut pending : Bytes
priv mut pending_pos : Int32
}
///|
/// Open an entry by directory index, preserving the identity of duplicate names.
/// No I/O occurs here. Unknown methods, unsupported flags, encryption, and
/// declared output exceeding the limit are rejected before requesting data.
pub fn Directory::open_entry(
self : Directory,
index : Int32,
limits~ : EntryReadLimits = EntryReadLimits(),
) -> EntryReader raise ZipError {
guard index >= 0 && index < self.entries.length() else {
raise ZipError(
InvalidEntryIndex(index),
"zip: directory entry index out of bounds",
)
}
let entry = self.entries[index]
ensure_flags_supported(entry.flags)
let compression = method_from_code(entry.method_id)
guard entry.uncompressed_size <= limits.max_output_bytes else {
raise ZipError(
LimitExceeded(
EntryUncompressedBytes,
limits.max_output_bytes,
entry.uncompressed_size,
),
"zip: entry exceeds its decoded-output limit",
)
}
if compression is Store {
guard entry.compressed_size == entry.uncompressed_size else {
raise ZipError(SizeMismatch, "zip: stored entry sizes differ")
}
}
{
entry,
central_offset: self.central_offset,
max_output: limits.max_output_bytes,
crc: Crc32(),
inflater: if compression is Deflate {
Some(Inflater())
} else {
None
},
phase: Initial,
payload_offset: 0UL,
descriptor_zip64: false,
requested: 0UL,
consumed: 0UL,
produced: 0UL,
step_produced: 0,
pending: b"",
pending_pos: 0,
}
}
///|
/// Initially supply empty input. After NeedRead, supply exactly the requested
/// bytes from the same immutable archive used for the directory listing. After
/// NeedMoreOutput, supply empty input and another output array; accepted input
/// is retained internally and must not be replayed. Read last_produced after
/// every normal return and process output[:last_produced()] before reusing it.
/// Input and output buffers are released before returning. An empty output
/// array suspends payload decoding. Requests are at most 65,535 bytes for local
/// metadata and 64 KiB for payloads. Cancel by stopping between calls.
/// Done verifies the local header, exact compressed/decoded sizes, CRC-32 and
/// any data descriptor. Earlier output is unverified until Done. On error the
/// output is unspecified, last_produced is zero, and the error remains latched.
pub fn EntryReader::step(
self : EntryReader,
input : BytesView,
output : FixedArray[Byte],
) -> EntryStep raise ZipError {
self.step_produced = 0
if self.phase is Failed(kind, message) {
raise ZipError(kind, message)
}
self.step_active(input, output) catch {
ZipError(kind, message) => {
self.phase = Failed(kind, message)
self.step_produced = 0
self.pending = b""
self.inflater = None
raise ZipError(kind, message)
}
}
}
///|
/// Output written by the latest successful step, including NeedRead and Done.
pub fn EntryReader::last_produced(self : EntryReader) -> Int32 {
self.step_produced
}
///|
/// Cumulative compressed payload bytes consumed, excluding headers/descriptors.
pub fn EntryReader::total_consumed(self : EntryReader) -> UInt64 {
self.consumed
}
///|
/// Cumulative decoded bytes. Integrity is established only after Done.
pub fn EntryReader::total_produced(self : EntryReader) -> UInt64 {
self.produced
}
///|
fn EntryReader::step_active(
self : EntryReader,
input : BytesView,
output : FixedArray[Byte],
) -> EntryStep raise ZipError {
match self.phase {
Initial => {
expect_range_input(input, 0)
self.phase = Header
NeedRead({ offset: self.entry.local_offset, length: 30, })
}
Header => self.read_header(input)
Name(header) => {
expect_range_input(input, header.name_length)
self.read_name(header, input)
}
Extra(header) => {
expect_range_input(input, header.extra_length)
self.read_extra(header, input)
}
PayloadInput(length) => {
expect_range_input(input, length)
self.pending = copy_zip_input(input)
self.pending_pos = 0
self.requested += length.to_uint64()
self.phase = Payload
self.read_payload(output)
}
Payload => {
expect_range_input(input, 0)
self.read_payload(output)
}
Descriptor(length) => {
expect_range_input(input, length)
ignore(
checked_data_descriptor_size(
input,
self.entry.crc,
self.entry.compressed_size,
self.entry.uncompressed_size,
self.descriptor_zip64,
),
)
self.phase = Complete
Done
}
Complete => {
expect_range_input(input, 0)
Done
}
Failed(kind, message) => raise ZipError(kind, message)
}
}
///|
fn EntryReader::read_header(
self : EntryReader,
input : BytesView,
) -> EntryStep raise ZipError {
expect_range_input(input, 30)
let reader = Cursor(input, 0)
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 == self.entry.flags else {
raise ZipError(UnsupportedFeature, "zip: local and central flags differ")
}
guard reader.read_u16() == self.entry.method_id else {
raise ZipError(UnsupportedFeature, "zip: local and central methods differ")
}
ignore(reader.read_u16())
ignore(reader.read_u16())
let header = {
crc: reader.read_u32_raw(),
compressed_size: reader.read_u32_raw(),
uncompressed_size: reader.read_u32_raw(),
name_length: reader.read_u16(),
extra_length: reader.read_u16(),
}
let header_size = 30UL +
header.name_length.to_uint64() +
header.extra_length.to_uint64()
guard header_size <= self.central_offset - self.entry.local_offset &&
self.entry.compressed_size <=
self.central_offset - self.entry.local_offset - header_size else {
raise ZipError(Truncated, "zip: local header or payload out of bounds")
}
self.payload_offset = self.entry.local_offset + header_size
guard header.name_length != 0 else { return self.read_name(header, b"") }
self.phase = Name(header)
NeedRead({
offset: self.entry.local_offset + 30UL,
length: header.name_length,
})
}
///|
fn EntryReader::read_name(
self : EntryReader,
header : EntryLocalHeader,
input : BytesView,
) -> EntryStep raise ZipError {
guard decode_name(input) == self.entry.name else {
raise ZipError(UnsupportedFeature, "zip: local and central names differ")
}
guard header.extra_length != 0 else { return self.read_extra(header, b"") }
self.phase = Extra(header)
NeedRead({
offset: self.payload_offset - header.extra_length.to_uint64(),
length: header.extra_length,
})
}
///|
fn EntryReader::read_extra(
self : EntryReader,
header : EntryLocalHeader,
input : BytesView,
) -> EntryStep raise ZipError {
let descriptor = (self.entry.flags & FLAG_DATA_DESCRIPTOR) != 0
let need_uncomp = header.uncompressed_size == MAX_U32
let need_comp = header.compressed_size == MAX_U32
let (zip_uncomp, zip_comp, _, _) = read_zip64_extra(
input, need_uncomp, need_comp, false,
)
guard header.crc == self.entry.crc || (descriptor && header.crc == 0U) else {
raise ZipError(UnsupportedFeature, "zip: local and central CRC-32 differ")
}
guard local_size_matches(
header.compressed_size,
zip_comp,
self.entry.compressed_size,
descriptor,
) &&
local_size_matches(
header.uncompressed_size,
zip_uncomp,
self.entry.uncompressed_size,
descriptor,
) else {
raise ZipError(SizeMismatch, "zip: local and central sizes differ")
}
self.descriptor_zip64 = need_uncomp ||
need_comp ||
self.entry.compressed_size >= 0xffff_ffffUL ||
self.entry.uncompressed_size >= 0xffff_ffffUL
guard self.entry.compressed_size != 0UL else {
guard self.entry.uncompressed_size == 0UL else {
raise ZipError(Truncated, "zip: empty DEFLATE payload")
}
return self.finish_payload()
}
self.request_payload()
}
///|
fn EntryReader::request_payload(self : EntryReader) -> EntryStep {
let length = range_read_length(
self.entry.compressed_size - self.requested,
65536,
)
self.pending = b""
self.pending_pos = 0
self.phase = PayloadInput(length)
NeedRead({ offset: self.payload_offset + self.requested, length, })
}
///|
fn EntryReader::record_output(
self : EntryReader,
output : FixedArray[Byte],
produced : Int32,
consumed : Int32,
) -> Unit raise ZipError {
guard produced.to_uint64() <= 0xffff_ffff_ffff_ffffUL - self.produced else {
raise ZipError(SizeMismatch, "zip: decoded size exceeds UInt64 range")
}
let actual = self.produced + produced.to_uint64()
guard actual <= self.max_output else {
raise ZipError(
LimitExceeded(EntryUncompressedBytes, self.max_output, actual),
"zip: entry exceeds its decoded-output limit",
)
}
guard actual <= self.entry.uncompressed_size else {
raise ZipError(
SizeMismatch,
"zip: decoded size exceeds central directory size",
)
}
self.crc.update(borrowed_zip_buffer_bytes(output)[:produced])
self.produced = actual
self.consumed += consumed.to_uint64()
self.pending_pos += consumed
self.step_produced = produced
}
///|
fn EntryReader::read_payload(
self : EntryReader,
output : FixedArray[Byte],
) -> EntryStep raise ZipError {
guard output.length() != 0 else { return NeedMoreOutput }
if self.inflater is Some(inflater) {
let status = inflater.step_into(
self.pending[self.pending_pos:],
output,
end=self.requested == self.entry.compressed_size,
) catch {
error => raise wrap_inflate_error(error)
}
self.record_output(
output,
inflater.last_produced(),
inflater.last_consumed(),
)
match status {
Done => {
guard self.consumed == self.entry.compressed_size else {
raise ZipError(
InvalidDeflate,
"zip: trailing bytes in compressed payload",
)
}
self.finish_payload()
}
NeedMoreOutput => NeedMoreOutput
NeedMoreInput => {
guard self.requested < self.entry.compressed_size else {
raise ZipError(Truncated, "zip: truncated DEFLATE payload")
}
self.request_payload()
}
}
} else {
let count = (self.pending.length() - self.pending_pos).min(output.length())
output.blit_from_bytesview(
0,
self.pending[self.pending_pos:self.pending_pos + count],
)
self.record_output(output, count, count)
guard self.pending_pos == self.pending.length() else {
return NeedMoreOutput
}
if self.requested == self.entry.compressed_size {
self.finish_payload()
} else {
self.request_payload()
}
}
}
///|
fn EntryReader::finish_payload(self : EntryReader) -> EntryStep raise ZipError {
guard self.produced == self.entry.uncompressed_size else {
raise ZipError(
SizeMismatch,
"zip: decoded size differs from central directory size",
)
}
guard self.crc.finish() == self.entry.crc else {
raise ZipError(ChecksumMismatch, "zip: CRC-32 mismatch")
}
self.pending = b""
self.inflater = None
guard (self.entry.flags & FLAG_DATA_DESCRIPTOR) != 0 else {
self.phase = Complete
return Done
}
let offset = self.payload_offset + self.entry.compressed_size
let minimum = if self.descriptor_zip64 { 20 } else { 12 }
guard minimum.to_uint64() <= self.central_offset - offset else {
raise ZipError(Truncated, "zip: data descriptor out of bounds")
}
let length = range_read_length(self.central_offset - offset, minimum + 4)
self.phase = Descriptor(length)
NeedRead({ offset, length, })
}