///|
/// A byte sink that either counts (sizing pass) or writes into a growable
/// buffer (emit pass), while enforcing an optional output ceiling.
priv struct Output {
limit : Int?
buf : Buffer?
mut size : Int
}
///|
fn Output::counting(limit : Int?) -> Output {
{ limit, buf: None, size: 0, }
}
///|
fn Output::writing(limit : Int?) -> Output {
{ limit, buf: Some(Buffer()), size: 0, }
}
///|
fn Output::is_counting(self : Output) -> Bool {
self.buf is None
}
///|
fn Output::grow(self : Output, additional : Int) -> Unit raise ZipError {
guard additional >= 0 else {
raise ZipError(UnsupportedFeature, "zip: negative output size")
}
if self.limit is Some(limit) {
guard additional <= limit - self.size else {
raise ZipError(
LimitExceeded(OutputBytes, limit, self.size + additional),
"zip: output exceeds its byte limit",
)
}
}
self.size = self.size + additional
}
///|
fn Output::write_byte(self : Output, b : Byte) -> Unit raise ZipError {
self.grow(1)
if self.buf is Some(buf) {
buf.write_byte(b)
}
}
///|
fn Output::write_bytes(self : Output, bytes : BytesView) -> Unit raise ZipError {
self.grow(bytes.length())
if self.buf is Some(buf) {
buf.write_bytes(bytes)
}
}
///|
/// Counts `n` payload bytes without materializing them (used by the sizing pass
/// for DEFLATE payloads whose compressed bytes are not held).
fn Output::count(self : Output, n : Int) -> Unit raise ZipError {
self.grow(n)
}
///|
fn Output::write_u16(self : Output, value : Int) -> Unit raise ZipError {
if value < 0 || value > 0xffff {
raise ZipError(UnsupportedFeature, "zip: u16 overflow")
}
let raw = value.to_uint_checked()
self.write_byte((raw & 0xff).to_byte())
self.write_byte(((raw >> 8) & 0xff).to_byte())
}
///|
fn Output::write_u32(self : Output, value : UInt) -> Unit raise ZipError {
self.write_byte((value & 0xff).to_byte())
self.write_byte(((value >> 8) & 0xff).to_byte())
self.write_byte(((value >> 16) & 0xff).to_byte())
self.write_byte(((value >> 24) & 0xff).to_byte())
}
///|
fn Output::write_u64(self : Output, value : UInt64) -> Unit raise ZipError {
for index in 0..<8 {
self.write_byte(((value >> (8 * index)) & 0xff).to_byte())
}
}
///|
fn Output::finish(self : Output) -> Bytes {
guard self.buf is Some(buf) else { return b"" }
buf.to_bytes()
}
///|
fn write_u16_buf(buf : Buffer, value : Int) -> Unit raise ZipError {
let raw = value.to_uint_checked()
buf.write_byte((raw & 0xff).to_byte())
buf.write_byte(((raw >> 8) & 0xff).to_byte())
}
///|
fn write_u64_buf(buf : Buffer, value : UInt64) -> Unit {
for index in 0..<8 {
buf.write_byte(((value >> (8 * index)) & 0xff).to_byte())
}
}
///|
fn build_zip64_extra(
uncomp~ : Int?,
comp~ : Int?,
offset~ : Int?,
) -> Bytes raise ZipError {
let mut count = 0
if uncomp is Some(_) {
count = count + 1
}
if comp is Some(_) {
count = count + 1
}
if offset is Some(_) {
count = count + 1
}
if count == 0 {
return b""
}
let buf = Buffer()
write_u16_buf(buf, ZIP64_EXTRA_ID)
write_u16_buf(buf, 8 * count)
if uncomp is Some(value) {
write_u64_buf(buf, value.to_uint64_checked())
}
if comp is Some(value) {
write_u64_buf(buf, value.to_uint64_checked())
}
if offset is Some(value) {
write_u64_buf(buf, value.to_uint64_checked())
}
buf.to_bytes()
}
///|
/// The materialized (or sized) payload of one fresh entry.
priv struct PreparedPayload {
compressed_size : Int
compressed : Bytes?
}
///|
/// Compress the entry's payload once. The sizing pass discards the bytes after
/// measuring; the emit pass keeps them so each entry is DEFLATEd exactly once
/// per pass.
fn prepare_payload(
entry : Entry,
level : Int,
counting : Bool,
) -> PreparedPayload {
match entry.compression {
Store => { compressed_size: entry.data.length(), compressed: None, }
Deflate => {
let compressed = @flate.deflate_all(entry.data, level~)
if counting {
{ compressed_size: compressed.length(), compressed: None, }
} else {
{ compressed_size: compressed.length(), compressed: Some(compressed), }
}
}
}
}
///|
fn method_code(entry : Entry) -> Int {
match entry.compression {
Store => 0
Deflate => 8
}
}
///|
fn entry_name_bytes(entry : Entry) -> Bytes raise ZipError {
let name = @encoding/utf8.encode(entry.name)
if name.length() > 0xffff {
raise ZipError(UnsupportedFeature, "zip: entry name too long")
}
name
}
///|
fn write_local_record(
out : Output,
entry : Entry,
name_bytes : Bytes,
payload : PreparedPayload,
use_descriptor : Bool,
force_zip64 : Bool,
) -> Unit raise ZipError {
let method_id = method_code(entry)
let data_len = entry.data.length()
let compressed_size = payload.compressed_size
let crc = entry.crc32
let needs_zip64_sizes = force_zip64 ||
data_len.to_uint_checked() > MAX_U32 ||
compressed_size.to_uint_checked() > MAX_U32
let version = if needs_zip64_sizes { 45 } else { 20 }
let flags = FLAG_UTF8 |
(if use_descriptor { FLAG_DATA_DESCRIPTOR } else { 0 })
let local_extra = if needs_zip64_sizes {
build_zip64_extra(
uncomp=Some(data_len),
comp=Some(compressed_size),
offset=None,
)
} else {
b""
}
out.write_u32(LOCAL_HEADER_SIG)
out.write_u16(version)
out.write_u16(flags)
out.write_u16(method_id)
out.write_u16(0)
out.write_u16(0)
if use_descriptor {
out.write_u32(0)
let size_field = if needs_zip64_sizes { MAX_U32 } else { 0 }
out.write_u32(size_field)
out.write_u32(size_field)
} else {
out.write_u32(crc)
let comp_field = if needs_zip64_sizes {
MAX_U32
} else {
compressed_size.to_uint_checked()
}
let uncomp_field = if needs_zip64_sizes {
MAX_U32
} else {
data_len.to_uint_checked()
}
out.write_u32(comp_field)
out.write_u32(uncomp_field)
}
out.write_u16(name_bytes.length())
out.write_u16(local_extra.length())
out.write_bytes(name_bytes)
out.write_bytes(local_extra)
// Both arms fall through to the data descriptor below, so the descriptor is
// emitted (and, in the sizing pass, counted) for STORED and DEFLATE entries
// alike — a `guard` would `return` out of the missing-payload branch and
// skip it.
if payload.compressed is Some(compressed) {
out.write_bytes(compressed)
} else {
match entry.compression {
Store => out.write_bytes(entry.data)
Deflate => out.count(compressed_size)
}
}
if use_descriptor {
out.write_u32(DATA_DESCRIPTOR_SIG)
out.write_u32(crc)
if needs_zip64_sizes {
out.write_u64(compressed_size.to_uint64_checked())
out.write_u64(data_len.to_uint64_checked())
} else {
out.write_u32(compressed_size.to_uint_checked())
out.write_u32(data_len.to_uint_checked())
}
}
}
///|
fn write_central_record(
out : Output,
entry : Entry,
name_bytes : Bytes,
local_offset : Int,
compressed_size : Int,
use_descriptor : Bool,
force_zip64 : Bool,
) -> Unit raise ZipError {
let method_id = method_code(entry)
let data_len = entry.data.length()
let needs_zip64_sizes = force_zip64 ||
data_len.to_uint_checked() > MAX_U32 ||
compressed_size.to_uint_checked() > MAX_U32
let needs_zip64_offset = force_zip64 ||
local_offset.to_uint_checked() > MAX_U32
let needs_zip64_entry = needs_zip64_sizes || needs_zip64_offset
let version = if needs_zip64_entry { 45 } else { 20 }
let flags = FLAG_UTF8 |
(if use_descriptor { FLAG_DATA_DESCRIPTOR } else { 0 })
let central_extra = build_zip64_extra(
uncomp=if needs_zip64_sizes { Some(data_len) } else { None },
comp=if needs_zip64_sizes { Some(compressed_size) } else { None },
offset=if needs_zip64_offset { Some(local_offset) } else { None },
)
out.write_u32(CENTRAL_HEADER_SIG)
out.write_u16(version)
out.write_u16(version)
out.write_u16(flags)
out.write_u16(method_id)
out.write_u16(0)
out.write_u16(0)
out.write_u32(entry.crc32)
let comp_field = if needs_zip64_sizes {
MAX_U32
} else {
compressed_size.to_uint_checked()
}
let uncomp_field = if needs_zip64_sizes {
MAX_U32
} else {
data_len.to_uint_checked()
}
out.write_u32(comp_field)
out.write_u32(uncomp_field)
out.write_u16(name_bytes.length())
out.write_u16(central_extra.length())
out.write_u16(0)
out.write_u16(0)
out.write_u16(0)
out.write_u32(0)
let offset_field = if needs_zip64_offset {
MAX_U32
} else {
local_offset.to_uint_checked()
}
out.write_u32(offset_field)
out.write_bytes(name_bytes)
out.write_bytes(central_extra)
}
///|
fn write_preserved_central_record(
out : Output,
source : SourceRecord,
local_offset : Int,
) -> Unit raise ZipError {
let template = source.central_record
guard template.length() >= 46 &&
read_u32_le_at_raw(template, 0) == CENTRAL_HEADER_SIG else {
raise ZipError(
UnsupportedFeature,
"zip: preserved central record is malformed",
)
}
guard source.central_zip64_offset_position is Some(position) else {
out.write_bytes(template[0:42])
out.write_u32(local_offset.to_uint_checked())
out.write_bytes(template[46:])
return
}
out.write_bytes(template[0:position])
out.write_u64(local_offset.to_uint64_checked())
out.write_bytes(template[position + 8:])
}
///|
fn write_generated_classic_end_record(
out : Output,
zip64 : Bool,
total_entries : Int,
central_size : Int,
central_offset : Int,
comment : Bytes,
) -> Unit raise ZipError {
out.write_u32(END_OF_CENTRAL_SIG)
out.write_u16(0)
out.write_u16(0)
if zip64 {
out.write_u16(0xffff)
out.write_u16(0xffff)
out.write_u32(MAX_U32)
out.write_u32(MAX_U32)
} else {
if total_entries > 0xffff {
raise ZipError(
UnsupportedFeature,
"zip: too many entries for a classic archive",
)
}
out.write_u16(total_entries)
out.write_u16(total_entries)
out.write_u32(central_size.to_uint_checked())
out.write_u32(central_offset.to_uint_checked())
}
if comment.length() > 0xffff {
raise ZipError(UnsupportedFeature, "zip: archive comment too long")
}
out.write_u16(comment.length())
out.write_bytes(comment)
}
///|
fn write_generated_zip64_trailer(
out : Output,
total_entries : Int,
central_size : Int,
central_offset : Int,
comment : Bytes,
) -> Unit raise ZipError {
let zip64_offset = out.size
out.write_u32(ZIP64_EOCD_SIG)
out.write_u64((44).to_uint64_checked())
out.write_u16(45)
out.write_u16(45)
out.write_u32(0)
out.write_u32(0)
let entry_count = total_entries.to_uint64_checked()
out.write_u64(entry_count)
out.write_u64(entry_count)
out.write_u64(central_size.to_uint64_checked())
out.write_u64(central_offset.to_uint64_checked())
out.write_u32(ZIP64_LOCATOR_SIG)
out.write_u32(0)
out.write_u64(zip64_offset.to_uint64_checked())
out.write_u32(1)
write_generated_classic_end_record(
out, true, total_entries, central_size, central_offset, comment,
)
}
///|
fn write_preserved_trailer(
out : Output,
trailer : TrailerTemplate,
total_entries : Int,
central_size : Int,
central_offset : Int,
) -> Unit raise ZipError {
let classic = trailer.classic_end_record
guard classic.length() >= 22 &&
read_u32_le_at_raw(classic, 0) == END_OF_CENTRAL_SIG else {
raise ZipError(UnsupportedFeature, "zip: preserved end record is malformed")
}
guard trailer.zip64_end_record is Some(end_record) else {
write_preserved_classic_end_record(
out, classic, total_entries, central_size, central_offset,
)
return
}
guard end_record.length() >= 56 &&
read_u32_le_at_raw(end_record, 0) == ZIP64_EOCD_SIG else {
raise ZipError(
UnsupportedFeature,
"zip: preserved zip64 end record is malformed",
)
}
let zip64_offset = out.size
// Patch the two entry-count fields, central size, and central offset
// (offsets 24/32/40/48 within the record) over the verbatim template.
out.write_bytes(end_record[0:24])
out.write_u64(total_entries.to_uint64_checked())
out.write_u64(total_entries.to_uint64_checked())
out.write_u64(central_size.to_uint64_checked())
out.write_u64(central_offset.to_uint64_checked())
out.write_bytes(end_record[56:])
guard trailer.zip64_locator is Some(locator) else {
raise ZipError(
UnsupportedFeature,
"zip: preserved zip64 locator is missing",
)
}
guard locator.length() == 20 &&
read_u32_le_at_raw(locator, 0) == ZIP64_LOCATOR_SIG else {
raise ZipError(
UnsupportedFeature,
"zip: preserved zip64 locator is malformed",
)
}
out.write_bytes(locator[0:8])
out.write_u64(zip64_offset.to_uint64_checked())
out.write_bytes(locator[16:])
// The classic record of a preserved ZIP64 archive carries sentinels;
// re-emit it verbatim.
out.write_bytes(classic)
}
///|
/// Streams a classic end record while patching both entry-count fields
/// (offsets 8, 10) and the central size/offset (offsets 12, 16). Fields
/// carrying ZIP64 sentinels are left untouched.
fn write_preserved_classic_end_record(
out : Output,
classic : Bytes,
total_entries : Int,
central_size : Int,
central_offset : Int,
) -> Unit raise ZipError {
if classic.length() < 22 ||
read_u32_le_at_raw(classic, 0) != END_OF_CENTRAL_SIG {
raise ZipError(UnsupportedFeature, "zip: preserved end record is malformed")
}
let count = total_entries.min(0xffff)
let mut cursor = 0
let entries_on_disk = read_u16_le_at(classic, 8)
if entries_on_disk != 0xffff {
out.write_bytes(classic[cursor:8])
out.write_u16(count)
cursor = 10
}
let total_on_disk = read_u16_le_at(classic, 10)
if total_on_disk != 0xffff {
out.write_bytes(classic[cursor:10])
out.write_u16(count)
cursor = 12
}
let size_field = read_u32_le_at_raw(classic, 12)
if size_field != MAX_U32 {
out.write_bytes(classic[cursor:12])
out.write_u32(central_size.to_uint_checked())
cursor = 16
}
let offset_field = read_u32_le_at_raw(classic, 16)
if offset_field != MAX_U32 {
out.write_bytes(classic[cursor:16])
out.write_u32(central_offset.to_uint_checked())
cursor = 20
}
out.write_bytes(classic[cursor:])
}
///|
/// Per-entry sizing/emission bookkeeping shared between the counting and emit
/// passes.
priv struct EntryPlan {
local_offset : Int
compressed_size : Int
}
///|
/// The order in which local records are written: entries that originated in a
/// read archive first, in their original local-record order, then entries
/// created in memory. A `replace` keeps the replaced entry's original position
/// (its source records are dropped for re-encoding, but its ordering hint
/// survives), so a byte-preserving rewrite never silently moves an edited part
/// to the end of the archive.
fn archive_local_order(archive : Archive) -> Array[Int] {
let order = []
for index in 0.. {
match
(
archive.entries[left].origin_local_offset,
archive.entries[right].origin_local_offset,
) {
(Some(a), Some(b)) => a.compare(b)
(Some(_), None) => -1
(None, Some(_)) => 1
(None, None) => left.compare(right)
}
})
order
}
///|
/// Writes every local record, then every central directory record, filling
/// `plans` with each entry's local offset and compressed size. Returns the
/// central directory's offset and size.
fn emit_archive(
out : Output,
archive : Archive,
order : Array[Int],
plans : Array[EntryPlan],
preserve : Bool,
level : Int,
force_zip64 : Bool,
) -> (Int, Int) raise ZipError {
for index in order {
let entry = archive.entries[index]
let local_offset = out.size
match (entry.source, preserve) {
(Some(source), true) => {
out.write_bytes(source.local_record)
plans[index] = { local_offset, compressed_size: entry.compressed_size, }
}
_ => {
let payload = prepare_payload(entry, level, out.is_counting())
write_local_record(
out,
entry,
entry_name_bytes(entry),
payload,
entry.data_descriptor,
force_zip64,
)
plans[index] = {
local_offset,
compressed_size: payload.compressed_size,
}
}
}
}
let central_offset = out.size
for index in order {
let entry = archive.entries[index]
match (entry.source, preserve) {
(Some(source), true) =>
write_preserved_central_record(out, source, plans[index].local_offset)
_ =>
write_central_record(
out,
entry,
entry_name_bytes(entry),
plans[index].local_offset,
plans[index].compressed_size,
entry.data_descriptor,
force_zip64,
)
}
}
let central_size = out.size - central_offset
(central_offset, central_size)
}
///|
fn write_trailer(
out : Output,
archive : Archive,
total_entries : Int,
central_size : Int,
central_offset : Int,
preserve : Bool,
force_zip64 : Bool,
) -> Unit raise ZipError {
let needs_zip64 = force_zip64 ||
total_entries > 0xffff ||
central_size.to_uint_checked() > MAX_U32 ||
central_offset.to_uint_checked() > MAX_U32
match (preserve, archive.trailer) {
(true, Some(trailer)) if !needs_zip64 || trailer.zip64_end_record is Some(_) =>
write_preserved_trailer(
out, trailer, total_entries, central_size, central_offset,
)
_ =>
if needs_zip64 {
write_generated_zip64_trailer(
out,
total_entries,
central_size,
central_offset,
archive.comment,
)
} else {
write_generated_classic_end_record(
out,
false,
total_entries,
central_size,
central_offset,
archive.comment,
)
}
}
}
///|
/// The sizing pass and the emit pass are deterministic on the same archive, so
/// a mismatch indicates an internal serialization invariant failure rather than
/// bad caller input; this defensive branch is excluded from behavior coverage.
#coverage.skip
fn assert_sizing_agrees(
counting_size : Int,
counting_central_offset : Int,
counting_central_size : Int,
central_offset : Int,
central_size : Int,
emitted_size : Int,
) -> Unit raise ZipError {
if central_offset != counting_central_offset ||
central_size != counting_central_size ||
emitted_size != counting_size {
raise ZipError(UnsupportedFeature, "zip: sizing and emission disagreed")
}
}
///|
fn write_impl(
archive : Archive,
preserve : Bool,
level : Int,
max_output : Int?,
) -> Bytes raise ZipError {
let order = archive_local_order(archive)
let plans = Array::make(archive.entries.length(), {
local_offset: 0,
compressed_size: 0,
})
guard max_output is Some(limit) else {
let out = Output::writing(None)
let (central_offset, central_size) = emit_archive(
out, archive, order, plans, preserve, level, false,
)
write_trailer(
out,
archive,
archive.entries.length(),
central_size,
central_offset,
preserve,
false,
)
return out.finish()
}
let counting = Output::counting(Some(limit))
let (counting_central_offset, counting_central_size) = emit_archive(
counting, archive, order, plans, preserve, level, false,
)
write_trailer(
counting,
archive,
archive.entries.length(),
counting_central_size,
counting_central_offset,
preserve,
false,
)
let size = counting.size
let out = Output::writing(Some(limit))
let (central_offset, central_size) = emit_archive(
out, archive, order, plans, preserve, level, false,
)
write_trailer(
out,
archive,
archive.entries.length(),
central_size,
central_offset,
preserve,
false,
)
assert_sizing_agrees(
size,
counting_central_offset,
counting_central_size,
central_offset,
central_size,
out.size,
)
out.finish()
}
///|
/// Serialize `archive` into a ZIP byte stream, encoding every entry fresh
/// (DEFLATE by default, STORED when the entry says so). `level` 0-9 trades
/// speed for ratio on the DEFLATE payloads.
pub fn write(archive : Archive, level? : Int = 6) -> Bytes raise ZipError {
write_impl(archive, false, level, None)
}
///|
/// Serialize `archive`, re-emitting pristine entries (those read from a
/// previous archive and never replaced) byte-for-byte from their retained
/// source records, so unchanged packages round-trip without loss. Entries
/// created or replaced in memory are encoded fresh.
pub fn write_preserving(
archive : Archive,
level? : Int = 6,
) -> Bytes raise ZipError {
write_impl(archive, true, level, None)
}
///|
/// Serialize `archive` like `write` while enforcing a hard output ceiling. The
/// output is sized first, so an over-limit archive raises
/// `LimitExceeded(OutputBytes, ...)` without materializing a candidate buffer.
pub fn write_limited(
archive : Archive,
max_output_bytes~ : Int,
level? : Int = 6,
) -> Bytes raise ZipError {
guard max_output_bytes >= 0 else {
raise ZipError(UnsupportedFeature, "zip: output limit must be non-negative")
}
write_impl(archive, false, level, Some(max_output_bytes))
}
///|
/// Serialize `archive` like `write_preserving` while enforcing a hard output
/// ceiling (sized first, so an over-limit archive raises without materializing
/// a candidate buffer).
pub fn write_preserving_limited(
archive : Archive,
max_output_bytes~ : Int,
level? : Int = 6,
) -> Bytes raise ZipError {
guard max_output_bytes >= 0 else {
raise ZipError(UnsupportedFeature, "zip: output limit must be non-negative")
}
write_impl(archive, true, level, Some(max_output_bytes))
}