///|
///|
priv struct SourceEntryTemplate {
local_record : Bytes
local_header_length : Int
central_record : Bytes
local_zip64_uncompressed_position : Int?
local_zip64_compressed_position : Int?
central_zip64_uncompressed_position : Int?
central_zip64_compressed_position : Int?
central_zip64_offset_position : Int?
descriptor_signed : Bool
descriptor_zip64 : Bool
}
///|
priv struct Zip64TrailerTemplate {
end_record : Bytes
locator : Bytes
classic_end_record : Bytes
}
///|
/// Provenance attached only by `read_limited`. Keeping it inside the opaque
/// archive makes the bounded-read claim non-forgeable by callers. Any
/// payload/entry mutation clears the claim; a shallow fork preserves it until
/// that fork is changed.
priv struct BoundedSource {
package_bytes : Int
max_entries : Int
max_entry_uncompressed_bytes : Int
max_total_uncompressed_bytes : Int
max_total_preserved_source_bytes : Int
}
///|
struct Entry {
name : String
data : Bytes
compression : Compression
data_descriptor : Bool
// CRC-32 of `data`: the central-directory value for entries read from an
// archive, or computed at add time for entries created in memory.
crc32 : UInt
source_local_offset : Int?
source : SourceEntryTemplate?
source_unchanged : Bool
}
///|
pub fn Entry::name(self : Entry) -> String {
self.name
}
///|
pub fn Entry::data(self : Entry) -> BytesView {
self.data
}
///|
pub fn Entry::compression(self : Entry) -> Compression {
self.compression
}
///|
pub fn Entry::data_descriptor(self : Entry) -> Bool {
self.data_descriptor
}
///|
/// The entry's CRC-32: as stored in the central directory for entries read
/// from an archive (compare with `crc32(entry.data())` to detect
/// corruption the inflater tolerates), or computed from the data for
/// entries created via `Archive::add`.
pub fn Entry::crc32(self : Entry) -> UInt {
self.crc32
}
///|
/// Size of this entry's OPC Central Directory File Header, excluding the
/// four-byte ZIP signature as defined by ECMA-376 Part 2 ยง7.3.6. Source
/// entries report their exact preserved name, Extra, and File Comment fields;
/// newly constructed entries have no Extra or File Comment fields yet.
pub fn Entry::central_directory_file_header_size(self : Entry) -> Int {
match self.source {
Some(source) => source.central_record.length() - 4
None => 42 + @encoding/utf8.encode(self.name).length()
}
}
///|
struct Archive {
entries : Array[Entry]
comment : Bytes
zip64_trailer : Zip64TrailerTemplate?
mut bounded_source : BoundedSource?
}
///|
pub fn Archive::new() -> Archive {
{ entries: [], comment: b"", zip64_trailer: None, bounded_source: None }
}
///|
/// Returns an independently mutable archive snapshot. Entry payloads remain
/// shared behind read-only views; `add` and `replace` defensively own their
/// inputs, so neither a fork nor its caller can mutate another archive's
/// payload through an external `Bytes` alias.
pub fn Archive::fork(self : Archive) -> Archive {
{
entries: self.entries.copy(),
comment: self.comment,
zip64_trailer: self.zip64_trailer,
bounded_source: self.bounded_source,
}
}
///|
/// Returns the exact serialized source size only when this pristine archive
/// came from `read_limited` with limits at least as strict as those requested.
/// This is a capability query: archives built with `new`/`read`, or changed
/// after a bounded read, return `None`.
pub fn Archive::bounded_source_package_size(
self : Archive,
max_entries~ : Int,
max_entry_uncompressed_bytes~ : Int,
max_total_uncompressed_bytes~ : Int,
max_total_preserved_source_bytes~ : Int,
) -> Int? {
match self.bounded_source {
Some(source) if source.max_entries <= max_entries &&
source.max_entry_uncompressed_bytes <= max_entry_uncompressed_bytes &&
source.max_total_uncompressed_bytes <= max_total_uncompressed_bytes &&
source.max_total_preserved_source_bytes <=
max_total_preserved_source_bytes => Some(source.package_bytes)
_ => None
}
}
///|
/// Conservatively estimates bytes retained by this materialized archive.
/// Payloads and preserved source records are counted exactly; decoded names
/// and per-entry/runtime bookkeeping include an explicit reserve. The source
/// package buffer from which the archive was read is not included.
pub fn Archive::retained_size_estimate(self : Archive) -> Int64 {
let mut total = self.comment.length().to_int64()
match self.zip64_trailer {
Some(trailer) => {
total = total + trailer.end_record.length().to_int64()
total = total + trailer.locator.length().to_int64()
total = total + trailer.classic_end_record.length().to_int64()
}
None => ()
}
if self.bounded_source is Some(_) {
total = total + 64L
}
for entry in self.entries {
total = total + entry.data.length().to_int64()
total = total + entry.name.length().to_int64() * 2L
// Entry/array slots, references, option wrappers, and allocator metadata.
total = total + 512L
match entry.source {
Some(source) => {
total = total + source.local_record.length().to_int64()
total = total + source.central_record.length().to_int64()
total = total + 128L
}
None => ()
}
}
total
}
///|
fn archive_from_source(
comment : Bytes,
zip64_trailer : Zip64TrailerTemplate?,
bounded_source : BoundedSource?,
) -> Archive {
{ entries: [], comment, zip64_trailer, bounded_source }
}
///|
pub fn Archive::add(
self : Archive,
name : String,
data : BytesView,
compression? : Compression = Store,
data_descriptor? : Bool = false,
) -> Unit {
let data = data.to_owned()
self.bounded_source = None
self.entries.push({
name,
data,
compression,
data_descriptor,
crc32: crc32(data),
source_local_offset: None,
source: None,
source_unchanged: false,
})
}
///|
/// Replaces an exact entry while retaining its compression policy and source
/// position. The next write preserves every other source entry byte-for-byte.
pub fn Archive::replace(
self : Archive,
name : StringView,
data : BytesView,
) -> Bool {
for index in 0.. BytesView? {
for entry in self.entries {
if entry.name()[:] == name {
return Some(entry.data())
}
}
None
}
///|
pub fn Archive::entries(self : Archive) -> ArrayView[Entry] {
self.entries
}