///|
/// Checks a write against the shared sizing/allocation sink. All arithmetic is
/// subtraction based so a hostile size cannot overflow the check itself.
fn check_output_growth(
buf : FixedByteOutput,
additional : Int,
_max_output : Int?,
) -> Unit raise ZipError {
buf.check_growth(additional)
}
///|
fn write_u16_le_bounded(
buf : FixedByteOutput,
value : Int,
max_output : Int?,
) -> Unit raise ZipError {
if value < 0 || value > 0xffff {
raise UnsupportedFeature(msg="u16 overflow")
}
check_output_growth(buf, 2, max_output)
let raw = value.reinterpret_as_uint()
buf.write_byte((raw & 0xff).to_byte())
buf.write_byte(((raw >> 8) & 0xff).to_byte())
}
///|
fn write_u32_le_bounded(
buf : FixedByteOutput,
value : UInt,
max_output : Int?,
) -> Unit raise ZipError {
check_output_growth(buf, 4, max_output)
buf.write_byte((value & 0xff).to_byte())
buf.write_byte(((value >> 8) & 0xff).to_byte())
buf.write_byte(((value >> 16) & 0xff).to_byte())
buf.write_byte(((value >> 24) & 0xff).to_byte())
}
///|
fn write_u64_le_bounded(
buf : FixedByteOutput,
value : UInt64,
max_output : Int?,
) -> Unit raise ZipError {
check_output_growth(buf, 8, max_output)
for index in 0..<8 {
buf.write_byte(((value >> (8 * index)) & 0xff).to_byte())
}
}
///|
fn write_bytes_bounded(
buf : FixedByteOutput,
bytes : BytesView,
max_output : Int?,
) -> Unit raise ZipError {
check_output_growth(buf, bytes.length(), max_output)
buf.write_bytes(bytes)
}
///|
fn to_u32(value : Int) -> UInt raise ZipError {
if value < 0 {
raise UnsupportedFeature(msg="u32 exceeds Int range")
}
value.reinterpret_as_uint()
}
///|
fn to_u64(value : Int) -> UInt64 raise ZipError {
if value < 0 {
raise UnsupportedFeature(msg="u64 exceeds Int range")
}
UInt64::extend_uint(value.reinterpret_as_uint())
}
///|
fn build_zip64_extra(
uncomp~ : Int?,
comp~ : Int?,
offset~ : Int?,
) -> Bytes raise ZipError {
let mut field_count = 0
if uncomp is Some(_) {
field_count = field_count + 1
}
if comp is Some(_) {
field_count = field_count + 1
}
if offset is Some(_) {
field_count = field_count + 1
}
if field_count == 0 {
return b""
}
let data_len = 8 * field_count
let extra = FixedByteOutput::allocated(4 + data_len)
write_u16_le_bounded(extra, 0x0001, None)
write_u16_le_bounded(extra, data_len, None)
match uncomp {
Some(value) => write_u64_le_bounded(extra, to_u64(value), None)
None => ()
}
match comp {
Some(value) => write_u64_le_bounded(extra, to_u64(value), None)
None => ()
}
match offset {
Some(value) => write_u64_le_bounded(extra, to_u64(value), None)
None => ()
}
extra.finish()
}
///|
fn write_data_descriptor_bounded(
buf : FixedByteOutput,
crc : UInt,
comp_size : Int,
uncomp_size : Int,
zip64_sizes : Bool,
max_output : Int?,
) -> Unit raise ZipError {
write_u32_le_bounded(
buf,
data_descriptor_sig.reinterpret_as_uint(),
max_output,
)
write_u32_le_bounded(buf, crc, max_output)
if zip64_sizes {
write_u64_le_bounded(buf, to_u64(comp_size), max_output)
write_u64_le_bounded(buf, to_u64(uncomp_size), max_output)
} else {
write_u32_le_bounded(buf, to_u32(comp_size), max_output)
write_u32_le_bounded(buf, to_u32(uncomp_size), max_output)
}
}
///|
fn planned_entry_metadata(
out : FixedByteOutput,
entry : Entry,
local_offset : Int,
max_output : Int?,
) -> WrittenEntryMetadata raise ZipError {
let compressed_size = match entry.compression() {
Store => entry.data().length()
Deflate => {
let size = match out.remaining_limit() {
Some(remaining) =>
deflate_encoded_size(entry.data(), max_output=remaining) catch {
OutputLimitExceeded(_) =>
match max_output {
Some(limit) => raise OutputLimitExceeded(limit~)
None => raise OutputLimitExceeded(limit=remaining)
}
error => raise error
}
None => deflate_encoded_size(entry.data())
}
size
}
}
{ local_offset, compressed_size, crc32: crc32(entry.data()) }
}
///|
fn write_entry_payload(
out : FixedByteOutput,
entry : Entry,
expected_size : Int,
max_output : Int?,
) -> Unit raise ZipError {
if out.is_counting() {
out.count_bytes(expected_size)
return
}
match entry.compression() {
Store => {
if entry.data().length() != expected_size {
raise UnsupportedFeature(
msg="stored ZIP payload size changed after planning",
)
}
write_bytes_bounded(out, entry.data(), max_output)
}
Deflate => {
let actual = deflate_encode_to_output(entry.data(), out) catch {
OutputLimitExceeded(_) =>
match max_output {
Some(limit) => raise OutputLimitExceeded(limit~)
None =>
raise UnsupportedFeature(
msg="fixed ZIP output plan was undersized",
)
}
error => raise error
}
if actual != expected_size {
raise UnsupportedFeature(
msg="DEFLATE sizing and ZIP emission disagreed",
)
}
}
}
}
///|
priv struct WrittenEntryMetadata {
local_offset : Int
compressed_size : Int
crc32 : UInt
}
///|
priv enum RecordPatch {
PatchU16(Int, Int)
PatchU32(Int, UInt)
PatchU64(Int, UInt64)
}
///|
fn RecordPatch::offset(self : RecordPatch) -> Int {
match self {
PatchU16(offset, _) | PatchU32(offset, _) | PatchU64(offset, _) => offset
}
}
///|
fn RecordPatch::width(self : RecordPatch) -> Int {
match self {
PatchU16(_, _) => 2
PatchU32(_, _) => 4
PatchU64(_, _) => 8
}
}
///|
/// Streams one source record into the final output while replacing a fixed set
/// of scalar fields. This deliberately never materializes a patched record or
/// a second central-directory/trailer buffer.
fn write_patched_record(
out : FixedByteOutput,
template : BytesView,
patches : Array[RecordPatch],
max_output : Int?,
) -> Unit raise ZipError {
patches.sort_by(fn(left, right) { left.offset().compare(right.offset()) })
let mut cursor = 0
for patch in patches {
let offset = patch.offset()
let width = patch.width()
if offset < cursor ||
offset > template.length() ||
width > template.length() - offset {
raise UnsupportedFeature(
msg="ZIP record streaming patch is out of bounds",
)
}
write_bytes_bounded(out, template[cursor:offset], max_output)
match patch {
PatchU16(_, value) => write_u16_le_bounded(out, value, max_output)
PatchU32(_, value) => write_u32_le_bounded(out, value, max_output)
PatchU64(_, value) => write_u64_le_bounded(out, value, max_output)
}
cursor = offset + width
}
write_bytes_bounded(out, template[cursor:], max_output)
}
///|
fn add_central_size_patch(
patches : Array[RecordPatch],
template : BytesView,
classic_position : Int,
zip64_position : Int?,
value : Int,
) -> Unit raise ZipError {
let raw = read_u32_le_at_raw(template, classic_position)
let max_u32 : UInt = 0xFFFFFFFF
match zip64_position {
Some(position) => patches.push(PatchU64(position, to_u64(value)))
None if raw == max_u32 =>
raise UnsupportedFeature(
msg="ZIP64 size field is missing from replacement template",
)
None => ()
}
if raw != max_u32 {
patches.push(PatchU32(classic_position, to_u32(value)))
}
}
///|
fn add_local_size_patch(
patches : Array[RecordPatch],
template : BytesView,
classic_position : Int,
zip64_position : Int?,
value : Int,
preserve_zero_placeholder : Bool,
) -> Unit raise ZipError {
let raw = read_u32_le_at_raw(template, classic_position)
let max_u32 : UInt = 0xffffffff
match zip64_position {
Some(position) => patches.push(PatchU64(position, to_u64(value)))
None if raw == max_u32 =>
raise UnsupportedFeature(
msg="ZIP64 size field is missing from replacement template",
)
None => ()
}
if raw == max_u32 {
return
}
let zero : UInt = 0
if preserve_zero_placeholder && raw == zero {
return
}
patches.push(PatchU32(classic_position, to_u32(value)))
}
///|
fn add_classic_u16_patch(
patches : Array[RecordPatch],
template : BytesView,
offset : Int,
value : Int,
) -> Unit raise ZipError {
if read_u16_le_at(template, offset) != 0xffff {
patches.push(PatchU16(offset, if value > 0xffff { 0xffff } else { value }))
}
}
///|
fn add_classic_u32_patch(
patches : Array[RecordPatch],
template : BytesView,
offset : Int,
value : Int,
) -> Unit raise ZipError {
let max_u32 : UInt = 0xffffffff
if read_u32_le_at_raw(template, offset) != max_u32 {
patches.push(
PatchU32(
offset,
if value < 0 || value.reinterpret_as_uint() > max_u32 {
max_u32
} else {
to_u32(value)
},
),
)
}
}
///|
fn write_replacement_local_record(
out : FixedByteOutput,
entry : Entry,
metadata : WrittenEntryMetadata,
template : SourceEntryTemplate,
max_output : Int?,
) -> Unit raise ZipError {
let method_id = match entry.compression() {
Store => 0
Deflate => 8
}
if template.local_header_length < 30 ||
template.local_header_length > template.local_record.length() ||
template.central_record.length() < 46 ||
read_u32_le_at_raw(template.local_record, 0) !=
local_header_sig.reinterpret_as_uint() ||
read_u32_le_at_raw(template.central_record, 0) !=
central_header_sig.reinterpret_as_uint() {
raise UnsupportedFeature(msg="replacement ZIP header template is malformed")
}
let local_header = template.local_record[:template.local_header_length]
let local_method = read_u16_le_at(local_header, 8)
let central_method = read_u16_le_at(template.central_record, 10)
let local_flags = read_u16_le_at(local_header, 6)
let central_flags = read_u16_le_at(template.central_record, 8)
let uses_descriptor = (local_flags & flag_data_descriptor) != 0
if local_method != method_id ||
central_method != method_id ||
local_flags != central_flags ||
uses_descriptor != entry.data_descriptor() {
raise UnsupportedFeature(
msg="replacement ZIP header policy changed unexpectedly",
)
}
let crc = metadata.crc32
let data_len = entry.data().length()
let payload_len = metadata.compressed_size
let local_crc = read_u32_le_at_raw(local_header, 14)
let zero : UInt = 0
let patches : Array[RecordPatch] = []
if !uses_descriptor || local_crc != zero {
patches.push(PatchU32(14, crc))
}
add_local_size_patch(
patches,
local_header,
18,
template.local_zip64_compressed_position,
payload_len,
uses_descriptor,
)
add_local_size_patch(
patches,
local_header,
22,
template.local_zip64_uncompressed_position,
data_len,
uses_descriptor,
)
write_patched_record(out, local_header, patches, max_output)
write_entry_payload(out, entry, payload_len, max_output)
if uses_descriptor {
if template.descriptor_signed {
write_u32_le_bounded(
out,
data_descriptor_sig.reinterpret_as_uint(),
max_output,
)
}
write_u32_le_bounded(out, crc, max_output)
if template.descriptor_zip64 {
write_u64_le_bounded(out, to_u64(payload_len), max_output)
write_u64_le_bounded(out, to_u64(data_len), max_output)
} else {
write_u32_le_bounded(out, to_u32(payload_len), max_output)
write_u32_le_bounded(out, to_u32(data_len), max_output)
}
}
}
///|
fn write_generated_local_record(
out : FixedByteOutput,
entry : Entry,
metadata : WrittenEntryMetadata,
force_zip64 : Bool,
max_output : Int?,
) -> Unit raise ZipError {
let method_id = match entry.compression() {
Store => 0
Deflate => 8
}
let name_bytes = @encoding/utf8.encode(entry.name())
let name_len = name_bytes.length()
let data_len = entry.data().length()
let payload_len = metadata.compressed_size
if name_len > 0xFFFF {
raise UnsupportedFeature(msg="file name too long")
}
let crc = metadata.crc32
let use_descriptor = entry.data_descriptor()
let max_u32 : UInt = 0xFFFFFFFF
let needs_zip64_sizes = force_zip64 ||
data_len.reinterpret_as_uint() > max_u32 ||
payload_len.reinterpret_as_uint() > max_u32
let needs_zip64_offset = force_zip64 ||
metadata.local_offset.reinterpret_as_uint() > max_u32
let needs_zip64_entry = needs_zip64_sizes || needs_zip64_offset
let version = if needs_zip64_entry { 45 } else { 20 }
let flags = if use_descriptor {
flag_utf8 | flag_data_descriptor
} else {
flag_utf8
}
let local_extra = build_zip64_extra(
uncomp=if needs_zip64_sizes { Some(data_len) } else { None },
comp=if needs_zip64_sizes { Some(payload_len) } else { None },
offset=None,
)
write_u32_le_bounded(out, local_header_sig.reinterpret_as_uint(), max_output)
write_u16_le_bounded(out, version, max_output)
write_u16_le_bounded(out, flags, max_output)
write_u16_le_bounded(out, method_id, max_output)
write_u16_le_bounded(out, 0, max_output)
write_u16_le_bounded(out, 0, max_output)
if use_descriptor {
write_u32_le_bounded(out, 0, max_output)
let size_field = if needs_zip64_sizes { max_u32 } else { 0 }
write_u32_le_bounded(out, size_field, max_output)
write_u32_le_bounded(out, size_field, max_output)
} else {
write_u32_le_bounded(out, crc, max_output)
let comp_field = if needs_zip64_sizes {
max_u32
} else {
to_u32(payload_len)
}
let uncomp_field = if needs_zip64_sizes {
max_u32
} else {
to_u32(data_len)
}
write_u32_le_bounded(out, comp_field, max_output)
write_u32_le_bounded(out, uncomp_field, max_output)
}
write_u16_le_bounded(out, name_len, max_output)
write_u16_le_bounded(out, local_extra.length(), max_output)
write_bytes_bounded(out, name_bytes, max_output)
write_bytes_bounded(out, local_extra, max_output)
write_entry_payload(out, entry, payload_len, max_output)
if use_descriptor {
write_data_descriptor_bounded(
out, crc, payload_len, data_len, needs_zip64_sizes, max_output,
)
}
}
///|
fn write_preserved_central_record(
out : FixedByteOutput,
template : SourceEntryTemplate,
local_offset : Int,
max_output : Int?,
) -> Unit raise ZipError {
if template.central_record.length() < 46 ||
read_u32_le_at_raw(template.central_record, 0) !=
central_header_sig.reinterpret_as_uint() {
raise UnsupportedFeature(msg="preserved central record is malformed")
}
let patches : Array[RecordPatch] = []
match template.central_zip64_offset_position {
Some(position) => patches.push(PatchU64(position, to_u64(local_offset)))
None => patches.push(PatchU32(42, to_u32(local_offset)))
}
write_patched_record(out, template.central_record, patches, max_output)
}
///|
fn write_replacement_central_record(
out : FixedByteOutput,
entry : Entry,
template : SourceEntryTemplate,
metadata : WrittenEntryMetadata,
max_output : Int?,
) -> Unit raise ZipError {
if template.central_record.length() < 46 ||
read_u32_le_at_raw(template.central_record, 0) !=
central_header_sig.reinterpret_as_uint() {
raise UnsupportedFeature(msg="replacement central record is malformed")
}
let patches : Array[RecordPatch] = [PatchU32(16, metadata.crc32)]
add_central_size_patch(
patches,
template.central_record,
20,
template.central_zip64_compressed_position,
metadata.compressed_size,
)
add_central_size_patch(
patches,
template.central_record,
24,
template.central_zip64_uncompressed_position,
entry.data().length(),
)
match template.central_zip64_offset_position {
Some(position) =>
patches.push(PatchU64(position, to_u64(metadata.local_offset)))
None => patches.push(PatchU32(42, to_u32(metadata.local_offset)))
}
write_patched_record(out, template.central_record, patches, max_output)
}
///|
fn write_generated_central_record(
out : FixedByteOutput,
entry : Entry,
metadata : WrittenEntryMetadata,
force_zip64 : Bool,
max_output : Int?,
) -> Unit raise ZipError {
let method_id = match entry.compression() {
Store => 0
Deflate => 8
}
let name_bytes = @encoding/utf8.encode(entry.name())
let name_len = name_bytes.length()
let data_len = entry.data().length()
let payload_len = metadata.compressed_size
if name_len > 0xFFFF {
raise UnsupportedFeature(msg="file name too long")
}
let use_descriptor = entry.data_descriptor()
let max_u32 : UInt = 0xFFFFFFFF
let needs_zip64_sizes = force_zip64 ||
data_len.reinterpret_as_uint() > max_u32 ||
payload_len.reinterpret_as_uint() > max_u32
let needs_zip64_offset = force_zip64 ||
metadata.local_offset.reinterpret_as_uint() > max_u32
let needs_zip64_entry = needs_zip64_sizes || needs_zip64_offset
let version = if needs_zip64_entry { 45 } else { 20 }
let flags = if use_descriptor {
flag_utf8 | flag_data_descriptor
} else {
flag_utf8
}
let central_extra = build_zip64_extra(
uncomp=if needs_zip64_sizes { Some(data_len) } else { None },
comp=if needs_zip64_sizes { Some(payload_len) } else { None },
offset=if needs_zip64_offset { Some(metadata.local_offset) } else { None },
)
write_u32_le_bounded(
out,
central_header_sig.reinterpret_as_uint(),
max_output,
)
write_u16_le_bounded(out, version, max_output)
write_u16_le_bounded(out, version, max_output)
write_u16_le_bounded(out, flags, max_output)
write_u16_le_bounded(out, method_id, max_output)
write_u16_le_bounded(out, 0, max_output)
write_u16_le_bounded(out, 0, max_output)
write_u32_le_bounded(out, metadata.crc32, max_output)
let central_comp = if needs_zip64_sizes {
max_u32
} else {
to_u32(payload_len)
}
let central_uncomp = if needs_zip64_sizes {
max_u32
} else {
to_u32(data_len)
}
write_u32_le_bounded(out, central_comp, max_output)
write_u32_le_bounded(out, central_uncomp, max_output)
write_u16_le_bounded(out, name_len, max_output)
write_u16_le_bounded(out, central_extra.length(), max_output)
write_u16_le_bounded(out, 0, max_output)
write_u16_le_bounded(out, 0, max_output)
write_u16_le_bounded(out, 0, max_output)
write_u32_le_bounded(out, 0, max_output)
let offset_field = if needs_zip64_offset {
max_u32
} else {
to_u32(metadata.local_offset)
}
write_u32_le_bounded(out, offset_field, max_output)
write_bytes_bounded(out, name_bytes, max_output)
write_bytes_bounded(out, central_extra, max_output)
}
///|
fn archive_local_order(archive : Archive) -> Array[Int] {
let order = []
for index in 0..
left_offset.compare(right_offset)
(Some(_), None) => -1
(None, Some(_)) => 1
(None, None) => left.compare(right)
}
})
order
}
///|
fn write_preserved_zip64_trailer(
out : FixedByteOutput,
template : Zip64TrailerTemplate,
total_entries : Int,
central_size : Int,
central_offset : Int,
max_output : Int?,
) -> Unit raise ZipError {
if template.end_record.length() < 56 ||
template.locator.length() != 20 ||
template.classic_end_record.length() < 22 ||
read_u32_le_at_raw(template.end_record, 0) !=
zip64_eocd_sig.reinterpret_as_uint() ||
read_u32_le_at_raw(template.locator, 0) !=
zip64_locator_sig.reinterpret_as_uint() ||
read_u32_le_at_raw(template.classic_end_record, 0) !=
end_of_central_sig.reinterpret_as_uint() ||
read_u16_le_at(template.classic_end_record, 20) !=
template.classic_end_record.length() - 22 {
raise UnsupportedFeature(
msg="preserved ZIP64 trailer template is malformed",
)
}
let zip64_offset = out.length()
let count = to_u64(total_entries)
write_patched_record(
out,
template.end_record,
[
PatchU64(24, count),
PatchU64(32, count),
PatchU64(40, to_u64(central_size)),
PatchU64(48, to_u64(central_offset)),
],
max_output,
)
write_patched_record(
out,
template.locator,
[PatchU64(8, to_u64(zip64_offset))],
max_output,
)
let classic_patches : Array[RecordPatch] = []
add_classic_u16_patch(
classic_patches,
template.classic_end_record,
8,
total_entries,
)
add_classic_u16_patch(
classic_patches,
template.classic_end_record,
10,
total_entries,
)
add_classic_u32_patch(
classic_patches,
template.classic_end_record,
12,
central_size,
)
add_classic_u32_patch(
classic_patches,
template.classic_end_record,
16,
central_offset,
)
write_patched_record(
out,
template.classic_end_record,
classic_patches,
max_output,
)
}
///|
fn write_generated_zip64_records(
out : FixedByteOutput,
total_entries : Int,
central_size : Int,
central_offset : Int,
max_output : Int?,
) -> Unit raise ZipError {
let zip64_offset = out.length()
write_u32_le_bounded(out, zip64_eocd_sig.reinterpret_as_uint(), max_output)
write_u64_le_bounded(out, to_u64(44), max_output)
write_u16_le_bounded(out, 45, max_output)
write_u16_le_bounded(out, 45, max_output)
write_u32_le_bounded(out, 0, max_output)
write_u32_le_bounded(out, 0, max_output)
let entry_count = to_u64(total_entries)
write_u64_le_bounded(out, entry_count, max_output)
write_u64_le_bounded(out, entry_count, max_output)
write_u64_le_bounded(out, to_u64(central_size), max_output)
write_u64_le_bounded(out, to_u64(central_offset), max_output)
write_u32_le_bounded(out, zip64_locator_sig.reinterpret_as_uint(), max_output)
write_u32_le_bounded(out, 0, max_output)
write_u64_le_bounded(out, to_u64(zip64_offset), max_output)
write_u32_le_bounded(out, 1, max_output)
}
///|
fn write_generated_classic_end_record(
out : FixedByteOutput,
zip64 : Bool,
total_entries : Int,
central_size : Int,
central_offset : Int,
comment : Bytes,
max_output : Int?,
) -> Unit raise ZipError {
let max_u32 : UInt = 0xffffffff
write_u32_le_bounded(
out,
end_of_central_sig.reinterpret_as_uint(),
max_output,
)
write_u16_le_bounded(out, 0, max_output)
write_u16_le_bounded(out, 0, max_output)
if zip64 {
write_u16_le_bounded(out, 0xffff, max_output)
write_u16_le_bounded(out, 0xffff, max_output)
write_u32_le_bounded(out, max_u32, max_output)
write_u32_le_bounded(out, max_u32, max_output)
} else {
write_u16_le_bounded(out, total_entries, max_output)
write_u16_le_bounded(out, total_entries, max_output)
write_u32_le_bounded(out, to_u32(central_size), max_output)
write_u32_le_bounded(out, to_u32(central_offset), max_output)
}
if comment.length() > 0xffff {
raise UnsupportedFeature(msg="archive comment too long")
}
write_u16_le_bounded(out, comment.length(), max_output)
write_bytes_bounded(out, comment, max_output)
}
///|
priv struct ArchiveWritePlan {
entries : Array[WrittenEntryMetadata]
central_offset : Int
central_size : Int
needs_zip64_trailer : Bool
output_size : Int
}
///|
fn write_archive_records(
archive : Archive,
out : FixedByteOutput,
written : Array[WrittenEntryMetadata],
force_zip64 : Bool,
max_output : Int?,
) -> (Int, Int, Bool) raise ZipError {
let entry_count = archive.entries().length()
if written.length() != entry_count {
raise UnsupportedFeature(msg="ZIP write plan entry count changed")
}
// Per-entry ZIP64 fields do not require an archive-wide ZIP64 trailer when
// the classic EOCD's own counts, central size, and central offset all fit.
// Preserve a source trailer when present, and generate one only for those
// archive-wide fields (or an explicit force request).
let mut needs_zip64_trailer = force_zip64 || archive.zip64_trailer is Some(_)
let max_u32 : UInt = 0xFFFFFFFF
for index in archive_local_order(archive) {
let entry = archive.entries[index]
let local_offset = out.length()
match (entry.source, entry.source_unchanged) {
(Some(source), true) => {
if out.is_counting() {
written[index] = {
local_offset,
compressed_size: 0,
crc32: entry.crc32(),
}
} else if written[index].local_offset != local_offset {
raise UnsupportedFeature(
msg="ZIP local offsets changed after planning",
)
}
write_bytes_bounded(out, source.local_record, max_output)
}
(source, _) => {
if out.is_counting() {
written[index] = planned_entry_metadata(
out, entry, local_offset, max_output,
)
} else if written[index].local_offset != local_offset {
raise UnsupportedFeature(
msg="ZIP local offsets changed after planning",
)
}
match source {
Some(template) =>
write_replacement_local_record(
out,
entry,
written[index],
template,
max_output,
)
None =>
write_generated_local_record(
out,
entry,
written[index],
force_zip64,
max_output,
)
}
}
}
}
let central_offset = out.length()
for index in 0..
write_preserved_central_record(
out,
source,
written[index].local_offset,
max_output,
)
(Some(source), false) =>
write_replacement_central_record(
out,
entry,
source,
written[index],
max_output,
)
(None, _) =>
write_generated_central_record(
out,
entry,
written[index],
force_zip64,
max_output,
)
}
}
let central_size = out.length() - central_offset
let total_entries = entry_count
if force_zip64 ||
central_offset.reinterpret_as_uint() > max_u32 ||
central_size.reinterpret_as_uint() > max_u32 ||
total_entries > 0xFFFF {
needs_zip64_trailer = true
}
match (needs_zip64_trailer, archive.zip64_trailer) {
(true, Some(template)) =>
write_preserved_zip64_trailer(
out, template, total_entries, central_size, central_offset, max_output,
)
(true, None) => {
write_generated_zip64_records(
out, total_entries, central_size, central_offset, max_output,
)
write_generated_classic_end_record(
out,
true,
total_entries,
central_size,
central_offset,
archive.comment,
max_output,
)
}
(false, _) =>
write_generated_classic_end_record(
out,
false,
total_entries,
central_size,
central_offset,
archive.comment,
max_output,
)
}
(central_offset, central_size, needs_zip64_trailer)
}
///|
fn plan_archive_write(
archive : Archive,
force_zip64 : Bool,
max_output : Int?,
) -> ArchiveWritePlan raise ZipError {
let out = FixedByteOutput::counting(limit?=max_output)
let entries = Array::make(archive.entries().length(), {
local_offset: 0,
compressed_size: 0,
crc32: (0 : UInt),
})
let (central_offset, central_size, needs_zip64_trailer) = write_archive_records(
archive, out, entries, force_zip64, max_output,
)
{
entries,
central_offset,
central_size,
needs_zip64_trailer,
output_size: out.length(),
}
}
///|
fn emit_archive_write(
archive : Archive,
plan : ArchiveWritePlan,
force_zip64 : Bool,
max_output : Int?,
) -> Bytes raise ZipError {
let out = FixedByteOutput::allocated(plan.output_size)
let (central_offset, central_size, needs_zip64_trailer) = write_archive_records(
archive,
out,
plan.entries,
force_zip64,
max_output,
)
if central_offset != plan.central_offset ||
central_size != plan.central_size ||
needs_zip64_trailer != plan.needs_zip64_trailer ||
out.length() != plan.output_size {
raise UnsupportedFeature(msg="ZIP sizing and emission plans disagreed")
}
out.finish()
}
///|
fn write_with_options(
archive : Archive,
force_zip64? : Bool = false,
max_output? : Int,
) -> Bytes raise ZipError {
let plan = plan_archive_write(archive, force_zip64, max_output)
emit_archive_write(archive, plan, force_zip64, max_output)
}
///|
pub fn write(archive : Archive) -> Bytes raise ZipError {
write_with_options(archive)
}
///|
/// Serializes an archive with an output-storage-free sizing pass that enforces
/// a hard byte ceiling before allocating output. Local and central records,
/// trailers, and DEFLATE payloads then stream into one exact fixed buffer;
/// oversized output raises `OutputLimitExceeded` without materializing a
/// candidate package or duplicate central directory.
pub fn write_limited(
archive : Archive,
max_output_bytes~ : Int,
) -> Bytes raise ZipError {
write_with_options(archive, max_output=max_output_bytes)
}
///|
#cfg(target="native")
test "zip sizing rejects near-envelope generated records before allocation" {
let archive = Archive::new()
let long_suffix = "n".repeat(65_000)
for index in 0..<768 {
archive.add("generated-\{index}-\{long_suffix}", b"")
}
let expected_limit = 95 * 1024 * 1024
// `plan_archive_write` owns no byte storage. Crossing this near-100 MiB
// generated-record boundary therefore fails before any candidate allocation.
try plan_archive_write(archive, false, Some(expected_limit)) catch {
OutputLimitExceeded(limit~) => assert_eq(limit, expected_limit)
_ => fail("expected generated near-envelope planning rejection")
} noraise {
_ => fail("expected generated near-envelope archive to exceed its limit")
}
}
///|
test "zip write_limited streams generated central-heavy archives" {
let archive = Archive::new()
let long_suffix = "n".repeat(2048)
for index in 0..<512 {
archive.add("generated-\{index}-\{long_suffix}", b"")
}
let exact = write(archive)
guard find_end_of_central(exact) is Some(eocd_offset) else {
fail("missing generated central-heavy end record")
}
let central_size = u32_to_int(read_u32_le_at_raw(exact, eocd_offset + 12))
let central_offset = u32_to_int(read_u32_le_at_raw(exact, eocd_offset + 16))
assert_true(central_size > 1024 * 1024)
assert_eq(write_limited(archive, max_output_bytes=exact.length()), exact)
let central_prefix_limit = central_offset + central_size / 2
try write_limited(archive, max_output_bytes=central_prefix_limit) catch {
OutputLimitExceeded(limit~) => assert_eq(limit, central_prefix_limit)
_ => fail("expected generated central-directory output limit")
} noraise {
_ => fail("expected generated central-heavy archive to exceed its limit")
}
}
///|
test "zip write data descriptor" {
let archive = Archive::new()
archive.add("a.txt", b"hello", data_descriptor=true)
let bytes = write(archive)
let flags = read_u16_le_at(bytes, 6)
inspect((flags & flag_data_descriptor) != 0, content="true")
let crc_local = read_u32_le_at_raw(bytes, 14)
let comp_local = read_u32_le_at_raw(bytes, 18)
let uncomp_local = read_u32_le_at_raw(bytes, 22)
let zero_u32 : UInt = 0
inspect(crc_local == zero_u32, content="true")
inspect(comp_local == zero_u32, content="true")
inspect(uncomp_local == zero_u32, content="true")
let name_len = read_u16_le_at(bytes, 26)
let extra_len = read_u16_le_at(bytes, 28)
let data_offset = 30 + name_len + extra_len
let data_len = b"hello".length()
let descriptor_offset = data_offset + data_len
let sig = read_u32_le_at_raw(bytes, descriptor_offset)
inspect(sig == data_descriptor_sig.reinterpret_as_uint(), content="true")
let crc_value = read_u32_le_at_raw(bytes, descriptor_offset + 4)
let expected_crc = crc32(b"hello")
inspect(crc_value == expected_crc, content="true")
let comp_size = read_u32_le_at_raw(bytes, descriptor_offset + 8)
let uncomp_size = read_u32_le_at_raw(bytes, descriptor_offset + 12)
let len_u32 : UInt = data_len.reinterpret_as_uint()
inspect(comp_size == len_u32, content="true")
inspect(uncomp_size == len_u32, content="true")
let parsed = read(bytes)
inspect(parsed.get("a.txt") == Some(b"hello"), content="true")
}
///|
test "zip write zip64 structures" {
let archive = Archive::new()
archive.add("a.txt", b"hello")
let bytes = write_with_options(archive, force_zip64=true)
let parsed = read(bytes)
inspect(parsed.get("a.txt") == Some(b"hello"), content="true")
let name_len = read_u16_le_at(bytes, 26)
let extra_len = read_u16_le_at(bytes, 28)
let extra_offset = 30 + name_len
let zip64_id = read_u16_le_at(bytes, extra_offset)
let zip64_size = read_u16_le_at(bytes, extra_offset + 2)
inspect(zip64_id == 0x0001, content="true")
inspect(extra_len == 20, content="true")
inspect(zip64_size == 16, content="true")
let max_u32 : UInt = 0xFFFFFFFF
let comp_local = read_u32_le_at_raw(bytes, 18)
let uncomp_local = read_u32_le_at_raw(bytes, 22)
inspect(comp_local == max_u32, content="true")
inspect(uncomp_local == max_u32, content="true")
let eocd_offset = match find_end_of_central(bytes) {
Some(offset) => offset
None => fail("missing end of central directory")
}
let locator_offset = eocd_offset - 20
let locator_sig = read_u32_le_at_raw(bytes, locator_offset)
inspect(
locator_sig == zip64_locator_sig.reinterpret_as_uint(),
content="true",
)
let zip64_offset = read_u64_le_at(bytes, locator_offset + 8)
let zip64_sig = read_u32_le_at_raw(bytes, zip64_offset.to_int())
inspect(zip64_sig == zip64_eocd_sig.reinterpret_as_uint(), content="true")
}
///|
test "zip writer wb: helper guard and zip64 descriptor branches" {
let guard_out = FixedByteOutput::counting()
try write_u16_le_bounded(guard_out, -1, None) catch {
e => inspect(e is UnsupportedFeature(_), content="true")
} noraise {
_ => fail("expected write_u16_le_bounded to raise")
}
try to_u32(-1) catch {
e => inspect(e is UnsupportedFeature(_), content="true")
} noraise {
_ => fail("expected to_u32 to raise")
}
try to_u64(-1) catch {
e => inspect(e is UnsupportedFeature(_), content="true")
} noraise {
_ => fail("expected to_u64 to raise")
}
let offset_only = build_zip64_extra(uncomp=None, comp=None, offset=Some(7))
inspect(offset_only.length(), content="12")
let descriptor = FixedByteOutput::allocated(24)
write_data_descriptor_bounded(
descriptor,
(0x01020304).reinterpret_as_uint(),
5,
6,
true,
None,
)
let desc_bytes = descriptor.finish()
inspect(desc_bytes.length(), content="24")
inspect(
read_u32_le_at_raw(desc_bytes, 0) ==
data_descriptor_sig.reinterpret_as_uint(),
content="true",
)
inspect(read_u64_le_at(desc_bytes, 8), content="5")
inspect(read_u64_le_at(desc_bytes, 16), content="6")
}