///|
/// ZIP binary encoding utilities and format constants.
///
/// This module provides low-level ZIP file format encoding functions that serialize
/// Member metadata and File data into the binary structures defined in PKWARE's
/// ZIP specification. Handles little-endian integer encoding, DOS date/time
/// conversion, and proper header layout for maximum compatibility.
///
/// Key structures:
/// - Local File Header (before each file's compressed data)
/// - Central Directory File Header (archive index)
/// - End of Central Directory Record (terminator + metadata)
// ZIP file format constants for header sizes (per PKWARE specification)
///|
/// Local file header fixed portion size (30 bytes, excludes variable filename/extra fields).
let local_header_fixed_size : Int = 30 // Local file header fixed part
///|
/// Central directory file header fixed portion size (46 bytes, excludes variable fields).
let central_dir_header_fixed_size : Int = 46 // Central directory header fixed part
///|
/// End of Central Directory Record size (22 bytes, excludes optional comment).
let eocd_size : Int = 22 // End of central directory record size
///|
/// Write local file header for a member to buffer.
///
/// Local file header structure (30 bytes fixed + variable filename + extra):
///
/// Offset | Size | Field
/// -------|------|------------------------------------------
/// 0 | 4 | Local file header signature (0x04034b50)
/// 4 | 2 | Version needed to extract
/// 6 | 2 | General purpose bit flag
/// 8 | 2 | Compression method
/// 10 | 2 | Last mod file time (DOS format)
/// 12 | 2 | Last mod file date (DOS format)
/// 14 | 4 | CRC-32 of uncompressed data
/// 18 | 4 | Compressed size
/// 22 | 4 | Uncompressed size
/// 26 | 2 | Filename length (n)
/// 28 | 2 | Extra field length (m, currently 0)
/// 30 | n | Filename (UTF-8 encoded)
/// 30+n | m | Extra field (unused, m=0)
///
/// This header precedes each file's compressed data in the ZIP archive.
/// The returned offset is used by Central Directory headers for file location.
///
/// Returns: byte offset where this local header was written in the buffer.
fn Member::write_local_header(self : Member, buf : Buffer) -> Int {
let local_header_offset = buf.length()
let path_bytes = self.path().to_string().utf8()
let (dos_date, dos_time) = @types.ptime_to_dos_date_time(self.mtime())
match self.kind() {
Dir => {
buf.write_uint_le(ZIP_LOCAL_FILE_SIG)
buf.write_uint16_le(@file.version_needed_default)
buf.write_uint16_le(@file.gp_flag_default)
buf.write_uint16_le(0) // compression method (stored)
buf.write_uint16_le(dos_time)
buf.write_uint16_le(dos_date)
buf.write_uint_le(0) // CRC-32
buf.write_uint_le(0) // compressed size
buf.write_uint_le(0) // uncompressed size
buf.write_uint16_le(path_bytes.length().to_uint16())
buf.write_uint16_le(0) // extra field length
buf.write_bytes(path_bytes)
}
File(f) => {
buf.write_uint_le(ZIP_LOCAL_FILE_SIG)
buf.write_uint16_le(f.version_needed_to_extract)
buf.write_uint16_le(f.gp_flags)
buf.write_uint16_le(f.compression.to_int().to_uint16())
buf.write_uint16_le(dos_time)
buf.write_uint16_le(dos_date)
buf.write_uint_le(f.decompressed_crc32)
buf.write_int_le(f.compressed_size)
buf.write_int_le(f.decompressed_size)
buf.write_uint16_le(path_bytes.length().to_uint16())
buf.write_uint16_le(0) // extra field length
buf.write_bytes(path_bytes)
// Write compressed data
buf.write_bytesview(
f.compressed_bytes[f.start:f.start + f.compressed_size],
)
}
}
local_header_offset
}
///|
/// Write central directory header for a member to buffer
///
/// Central directory header structure (46 bytes fixed + variable):
/// - signature (4 bytes): 0x02014b50
/// - version made by (2 bytes)
/// - version needed to extract (2 bytes)
/// - general purpose bit flag (2 bytes)
/// - compression method (2 bytes)
/// - last mod file time (2 bytes, DOS format)
/// - last mod file date (2 bytes, DOS format)
/// - CRC-32 (4 bytes)
/// - compressed size (4 bytes)
/// - uncompressed size (4 bytes)
/// - file name length (2 bytes)
/// - extra field length (2 bytes)
/// - file comment length (2 bytes)
/// - disk number start (2 bytes)
/// - internal file attributes (2 bytes)
/// - external file attributes (4 bytes, Unix permissions in high 16 bits)
/// - relative offset of local header (4 bytes)
/// - file name (variable)
/// - extra field (variable, currently 0)
/// - file comment (variable, currently 0)
fn Member::write_central_directory_header(
self : Member,
buf : Buffer,
local_header_offset : Int,
) -> Unit {
let path_bytes = self.path().to_string().utf8()
let (dos_date, dos_time) = @types.ptime_to_dos_date_time(self.mtime())
buf.write_uint_le(ZIP_CENTRAL_DIR_SIG)
match self.kind() {
Dir => {
buf.write_uint16_le(@file.version_made_by_default)
buf.write_uint16_le(@file.version_needed_default)
buf.write_uint16_le(@file.gp_flag_default)
buf.write_uint16_le(0) // compression
buf.write_uint16_le(dos_time)
buf.write_uint16_le(dos_date)
buf.write_uint_le((0 : UInt)) // CRC
buf.write_uint_le((0 : UInt)) // compressed size
buf.write_uint_le((0 : UInt)) // uncompressed size
buf.write_uint16_le(path_bytes.length().to_uint16())
buf.write_uint16_le(0) // extra field length
buf.write_uint16_le(0) // file comment length
buf.write_uint16_le(0) // disk number start
buf.write_uint16_le(0) // internal file attributes
buf.write_int_le(self.mode().to_int() << 16) // external file attributes
buf.write_int_le(local_header_offset)
buf.write_bytes(path_bytes)
}
File(f) => {
buf.write_uint16_le(f.version_made_by)
buf.write_uint16_le(f.version_needed_to_extract)
buf.write_uint16_le(f.gp_flags)
buf.write_uint16_le(f.compression.to_int().to_uint16())
buf.write_uint16_le(dos_time)
buf.write_uint16_le(dos_date)
buf.write_uint_le(f.decompressed_crc32)
buf.write_int_le(f.compressed_size)
buf.write_int_le(f.decompressed_size)
buf.write_uint16_le(path_bytes.length().to_uint16())
buf.write_uint16_le(0) // extra field length
buf.write_uint16_le(0) // file comment length
buf.write_uint16_le(0) // disk number start
buf.write_uint16_le(0) // internal file attributes
buf.write_int_le(self.mode().to_int() << 16) // external file attributes
buf.write_int_le(local_header_offset)
buf.write_bytes(path_bytes)
}
}
}
///|
/// Calculate the size needed to encode an archive
pub fn Archive::encoding_size(self : Archive) -> Int {
let mut size = 0
for _, m in self.members {
let path_bytes = m.path().to_string().utf8().length()
// Local file header + path + central directory header + path
size += local_header_fixed_size +
path_bytes +
central_dir_header_fixed_size +
path_bytes
match m.kind() {
Dir => ()
File(f) => size += f.compressed_size
}
}
// End of central directory record
size += eocd_size
size
}
///|
/// Encode archive to bytes
pub fn Archive::to_bytes(self : Archive, first? : Fpath) -> Bytes raise {
// Check member count limit
let count = self.member_count()
if count > @member.max_member_count {
fail(
"Archive has \{count} members, exceeds maximum \{@member.max_member_count}",
)
}
let total_size = self.encoding_size()
// Use Buffer from stdlib instead of manual array indexing
let buf = Buffer(size_hint=total_size)
let central_dir_entries : Array[(Int, Member)] = []
// Helper to encode a member - write local header and record position
let encode_member = fn(m : Member) {
let local_header_offset = m.write_local_header(buf)
central_dir_entries.push((local_header_offset, m))
}
// Encode members in order (first, if specified, then rest in sorted order)
match first {
Some(first_path) =>
match self.find(first_path) {
Some(first_member) => {
encode_member(first_member)
// Encode rest in sorted order
for path, m in self.members {
if path != first_path {
encode_member(m)
}
}
}
None =>
// First path not found, just encode all in sorted order
for _, m in self.members {
encode_member(m)
}
}
None =>
// Encode all in sorted order
for _, m in self.members {
encode_member(m)
}
}
// Write central directory
let central_dir_start = buf.length()
for entry in central_dir_entries {
let (offset, m) = entry
m.write_central_directory_header(buf, offset)
}
let central_dir_size = buf.length() - central_dir_start
// Write end of central directory record
buf.write_uint_le(ZIP_EOCD_SIG)
buf.write_uint16_le(0) // disk number
buf.write_uint16_le(0) // disk with central directory
buf.write_uint16_le(count.to_uint16()) // entries on this disk
buf.write_uint16_le(count.to_uint16()) // total entries
buf.write_int_le(central_dir_size)
buf.write_int_le(central_dir_start)
buf.write_uint16_le(0) // comment length
buf.to_bytes()
}