///|
/// Convert an `Int64` ZIP64 value to a sync-API-safe `Int`.
///
/// Rejects negative values and values that exceed `max_int_val()` with
/// `Zip64ValueTooLarge`, so the sync APIs never silently allocate or index
/// past `Int` range. `context` is included in the error message to identify
/// which ZIP64 field tripped the check.
fn zip64_to_int(v : Int64, context : String) -> Int raise FzipError {
if v < 0L {
raise fzip_err(Zip64ValueTooLarge, msg=context + ": negative value")
}
if v > max_int_val().to_int64() {
raise fzip_err(
Zip64ValueTooLarge,
msg=context + ": value exceeds Int range",
)
}
v.to_int()
}
///|
/// Read a ZIP64 8-byte little-endian value as a sync-API-safe `Int`.
///
/// Reads the low and high 32-bit words separately so adversarial values whose
/// `Int64` representation would be negative (or anything past `max_int_val()`)
/// are rejected before they can reach allocation or indexing arithmetic.
/// Prefer this over `b8(...).to_int()` for any ZIP64 size, count, or offset
/// that originates outside the writer.
fn read_zip64_int(
data : FixedArray[Byte],
offset : Int,
context : String,
) -> Int raise FzipError {
let lo = b4(data, offset)
let hi = b4(data, offset + 4)
if hi != 0U {
raise fzip_err(
Zip64ValueTooLarge,
msg=context + ": value > 2^32 not representable",
)
}
if lo > max_int_val().reinterpret_as_uint() {
raise fzip_err(
Zip64ValueTooLarge,
msg=context + ": value exceeds Int range",
)
}
lo.reinterpret_as_int()
}
///|
/// Convert a classic ZIP 32-bit unsigned size/offset field to a sync-API-safe
/// `Int`.
///
/// Classic ZIP fields can legally describe values above MoonBit's signed
/// `Int` ceiling. Reject those before reinterpretation so a value like
/// `0x80000000` cannot become negative and bypass later bounds checks.
fn zip32_to_int(v : UInt, context : String) -> Int raise FzipError {
if v > max_int_val().reinterpret_as_uint() {
raise fzip_err(
Zip64ValueTooLarge,
msg=context + ": value exceeds Int range",
)
}
v.reinterpret_as_int()
}
///|
/// Writer-side checked helper: validates the ZIP64 value fits the sync API's
/// `Int` range, then writes it as a fixed 8-byte little-endian value via `w8`.
fn write_zip64_int(
data : FixedArray[Byte],
offset : Int,
value : Int64,
context : String,
) -> Unit raise FzipError {
let _ = zip64_to_int(value, context)
w8(data, offset, value)
}
///|
/// Resolved per-entry size + offset triple after reconciling the classic
/// 32-bit fields with the ZIP64 extended information extra field. Using a
/// named struct (instead of a positional tuple) prevents callers from
/// confusing compressed size, uncompressed size, and local header offset —
/// which are otherwise indistinguishable `Int`s.
priv struct ZipEntrySizes {
compressed : Int
uncompressed : Int
local_offset : Int
}
///|
/// Parse a central directory entry's ZIP64 extended information extra field
/// (header id `0x0001`, PKWARE APPNOTE §4.5.3).
///
/// The classic 32-bit values are passed in as labeled parameters. For each
/// classic field that holds the sentinel `0xFFFFFFFF`, the matching 8-byte
/// value is read from the ZIP64 extra in spec-fixed order: uncompressed
/// size, compressed size, then local header offset. Unrelated extra fields
/// before the ZIP64 one are skipped. Returns the resolved `ZipEntrySizes`
/// — sentinel values never propagate out as real sizes.
///
/// Errors:
/// * `ExtraFieldTooLong` if `extra_len` exceeds `max_extra_field_length`.
/// * `InvalidZipData` if an extra field is truncated, the ZIP64 extra is
/// missing required size/offset fields, the ZIP64 extra is missing
/// entirely, or a non-zero disk number is present.
/// * `Zip64ValueTooLarge` if a ZIP64 size or offset cannot be represented
/// as the sync API's `Int`.
fn read_zip64_entry_extra(
data : FixedArray[Byte],
extra_offset : Int,
extra_len : Int,
classic_compressed~ : UInt,
classic_uncompressed~ : UInt,
classic_local_offset~ : UInt,
) -> ZipEntrySizes raise FzipError {
if extra_len > max_extra_field_length {
raise fzip_err(ExtraFieldTooLong, msg="extra field length exceeds maximum")
}
let need_uncomp = classic_uncompressed == zip_uint32_max
let need_comp = classic_compressed == zip_uint32_max
let need_local = classic_local_offset == zip_uint32_max
let end = extra_offset + extra_len
let mut p = extra_offset
while p + 4 <= end {
let header_id = b2(data, p)
let field_len = b2(data, p + 2)
if p + 4 + field_len > end {
raise fzip_err(InvalidZipData, msg="extra field truncated")
}
if header_id == zip64_extra_field_id {
let field_start = p + 4
let field_end = field_start + field_len
let mut fp = field_start
let uncompressed = if need_uncomp {
if fp + 8 > field_end {
raise fzip_err(
InvalidZipData,
msg="ZIP64 extra missing uncompressed size",
)
}
let v = read_zip64_int(data, fp, "ZIP64 entry uncompressed size")
fp = fp + 8
v
} else {
zip32_to_int(
classic_uncompressed, "ZIP64 entry classic uncompressed size",
)
}
let compressed = if need_comp {
if fp + 8 > field_end {
raise fzip_err(
InvalidZipData,
msg="ZIP64 extra missing compressed size",
)
}
let v = read_zip64_int(data, fp, "ZIP64 entry compressed size")
fp = fp + 8
v
} else {
zip32_to_int(classic_compressed, "ZIP64 entry classic compressed size")
}
let local_offset = if need_local {
if fp + 8 > field_end {
raise fzip_err(
InvalidZipData,
msg="ZIP64 extra missing local header offset",
)
}
let v = read_zip64_int(data, fp, "ZIP64 entry local header offset")
fp = fp + 8
v
} else {
zip32_to_int(
classic_local_offset, "ZIP64 entry classic local header offset",
)
}
// The 4-byte disk-start-number is optional; if present, it must be 0.
// Any other trailing byte count means the ZIP64 payload is malformed.
let remaining = field_end - fp
if remaining != 0 && remaining != 4 {
raise fzip_err(
InvalidZipData,
msg="ZIP64 extra has invalid trailing bytes",
)
}
if remaining == 4 {
let disk = b4(data, fp)
if disk != 0U {
raise fzip_err(
InvalidZipData,
msg="multi-disk ZIP archives are not supported",
)
}
}
return { compressed, uncompressed, local_offset }
}
p = p + 4 + field_len
}
raise fzip_err(InvalidZipData, msg="required ZIP64 extra field not present")
}
///|
/// Build the 16-byte payload for a ZIP64 extended information extra field
/// stored inside a LOCAL file header (PKWARE APPNOTE §4.5.3 + §4.4.8/§4.4.9
/// notes on local-header interoperability). When either size in the local
/// header needs the 32-bit sentinel, the local-header ZIP64 extra MUST carry
/// BOTH 8-byte size values in the spec-fixed order: uncompressed then
/// compressed. Local headers never include the local header offset — that
/// belongs to the central directory only.
///
/// Returned bytes are the field PAYLOAD only; the surrounding 4-byte
/// `(header_id, data_size)` pair is stamped in by the writer when this
/// payload is appended to the entry's extras list.
fn build_zip64_extra_local_payload(
uncompressed : Int64,
compressed : Int64,
) -> FixedArray[Byte] raise FzipError {
let _ = zip64_to_int(uncompressed, "ZIP64 local extra uncompressed size")
let _ = zip64_to_int(compressed, "ZIP64 local extra compressed size")
let buf = FixedArray::make(16, b'\x00')
w8(buf, 0, uncompressed)
w8(buf, 8, compressed)
buf
}
///|
/// Build the variable-length payload for a ZIP64 extended information extra
/// field stored inside a CENTRAL DIRECTORY entry. Each value is included
/// only when the matching classic field is being written as the 32-bit
/// sentinel. The order is fixed by APPNOTE §4.5.3: uncompressed, compressed,
/// local header offset.
///
/// `None` for any of the three values means "the classic 32-bit field still
/// holds the real value, do not include this field in the ZIP64 extra".
fn build_zip64_extra_cd_payload(
uncompressed : Int64?,
compressed : Int64?,
local_offset : Int64?,
) -> FixedArray[Byte] raise FzipError {
let values : Array[(Int64, String)] = []
match uncompressed {
Some(v) => values.push((v, "ZIP64 CD extra uncompressed size"))
None => ()
}
match compressed {
Some(v) => values.push((v, "ZIP64 CD extra compressed size"))
None => ()
}
match local_offset {
Some(v) => values.push((v, "ZIP64 CD extra local offset"))
None => ()
}
let buf = FixedArray::make(values.length() * 8, b'\x00')
for i, kv in values {
let (v, ctx) = kv
let _ = zip64_to_int(v, ctx)
w8(buf, i * 8, v)
}
buf
}
///|
/// Write a ZIP64 end-of-central-directory record (PKWARE APPNOTE §4.3.14).
/// fzip emits the minimum 56-byte form: a 12-byte fixed header (signature +
/// 8-byte `record size`) followed by the 44-byte field block. The
/// version_made_by / version_needed bytes are stamped at 45 (low byte) /
/// 0 (high byte = compatibility OS) per APPNOTE §4.4.3.2. Disk numbers are
/// zero — multi-disk ZIP is unsupported. Each 64-bit count/size/offset is
/// validated through `write_zip64_int` so values beyond the sync API's
/// `Int` ceiling raise `Zip64ValueTooLarge` rather than silently
/// overflowing.
fn write_zip64_eocd_record(
d : FixedArray[Byte],
b : Int,
entries : Int64,
cd_size : Int64,
cd_offset : Int64,
) -> Unit raise FzipError {
w4(d, b, zip64_eocd_signature)
// record size = 44 (bytes after this field)
w8(d, b + 4, 44L)
// version made by (low = 45 spec, high = 0 OS), version needed = 45.
w2(d, b + 12, 45)
w2(d, b + 14, 45)
// disk number (+16) and disk-with-CD (+20): zero for single-disk archives.
// entries on this disk == total entries
write_zip64_int(d, b + 24, entries, "ZIP64 EOCD entries on disk")
write_zip64_int(d, b + 32, entries, "ZIP64 EOCD total entries")
write_zip64_int(d, b + 40, cd_size, "ZIP64 EOCD central directory size")
write_zip64_int(d, b + 48, cd_offset, "ZIP64 EOCD central directory offset")
}
///|
/// Write a 20-byte ZIP64 end-of-central-directory locator (PKWARE APPNOTE
/// §4.3.15). Single-disk archives only — disk-containing-zip64-EOCD = 0,
/// total disks = 1.
fn write_zip64_locator(
d : FixedArray[Byte],
b : Int,
zip64_eocd_offset : Int64,
) -> Unit raise FzipError {
w4(d, b, zip64_locator_signature)
// disk-containing-zip64-EOCD at +4: zero.
write_zip64_int(d, b + 8, zip64_eocd_offset, "ZIP64 EOCD locator offset")
w4(d, b + 16, 1U) // total disks
}