///|
/// Resource bounds for metadata-only directory reads. These bound encoded
/// directory bytes and retained entry count, never declared payload sizes.
/// Defaults permit any directory byte size and at most 2^31 - 1 entries;
/// callers should select bounds appropriate to their metadata memory budget.
#valtype
pub struct DirectoryLimits {
priv max_directory_bytes : UInt64
priv max_entries : UInt64
}
///|
/// Construct directory bounds. A zero ceiling permits only an empty resource.
pub fn DirectoryLimits::DirectoryLimits(
max_directory_bytes~ : UInt64 = 0xffff_ffff_ffff_ffffUL,
max_entries~ : UInt64 = 0x7fff_ffffUL,
) -> DirectoryLimits {
{ max_directory_bytes, max_entries, }
}
///|
/// A directory listing in central-directory order, including duplicate names.
/// Owns metadata only. A successful listing does not validate local headers,
/// decompress payloads, or verify their CRC-32 values.
pub struct Directory {
priv entries : Array[DirectoryEntry]
priv comment : Bytes
priv central_offset : UInt64
}
///|
/// Entries in central-directory order. This view cannot modify the listing.
pub fn Directory::entries(self : Directory) -> ArrayView[DirectoryEntry] {
self.entries
}
///|
/// The archive comment, owned independently of supplied read buffers.
pub fn Directory::comment(self : Directory) -> BytesView {
self.comment
}
///|
/// Metadata declared by a central-directory entry, without its payload.
/// Sizes remain 64-bit even when the payload cannot fit in a MoonBit Bytes.
/// Unknown compression methods and encrypted payloads can still be listed.
pub struct DirectoryEntry {
priv name : String
priv compressed_size : UInt64
priv uncompressed_size : UInt64
priv local_offset : UInt64
priv method_id : Int32
priv flags : Int32
priv crc : UInt32
priv creator_system : Int32
priv external_attributes : UInt32
priv dos_date : Int32
priv dos_time : Int32
}
///|
/// The entry name, decoded using the same UTF-8 policy as `read`.
pub fn DirectoryEntry::name(self : DirectoryEntry) -> String {
self.name
}
///|
/// The declared compressed payload size, excluding headers and descriptors.
pub fn DirectoryEntry::compressed_size(self : DirectoryEntry) -> UInt64 {
self.compressed_size
}
///|
/// The declared uncompressed size; listing never allocates this many bytes.
pub fn DirectoryEntry::uncompressed_size(self : DirectoryEntry) -> UInt64 {
self.uncompressed_size
}
///|
/// The ZIP compression method number, including methods this library cannot decode.
pub fn DirectoryEntry::method_id(self : DirectoryEntry) -> Int32 {
self.method_id
}
///|
/// The declared CRC-32. Directory reading does not verify payload integrity.
pub fn DirectoryEntry::crc32(self : DirectoryEntry) -> UInt32 {
self.crc
}
///|
/// Whether the name or DOS/Unix file attributes identify a directory.
pub fn DirectoryEntry::is_directory(self : DirectoryEntry) -> Bool {
self.name.has_suffix("/") ||
(self.external_attributes & 0x10U) != 0 ||
(
(self.creator_system == 3 || self.creator_system == 19) &&
((self.external_attributes >> 16) & 0xf000U) == 0x4000U
)
}
///|
/// Whether the entry's general-purpose flag declares payload encryption.
/// This does not imply support for decrypting it or for encrypted directories.
pub fn DirectoryEntry::is_encrypted(self : DirectoryEntry) -> Bool {
(self.flags & FLAG_ENCRYPTED) != 0
}
///|
/// The host-system identifier from the entry's version-made-by field.
pub fn DirectoryEntry::creator_system(self : DirectoryEntry) -> Int32 {
self.creator_system
}
///|
/// Original external file attributes, interpreted according to `creator_system`.
pub fn DirectoryEntry::external_attributes(self : DirectoryEntry) -> UInt32 {
self.external_attributes
}
///|
/// Original packed DOS modification date (year/month/day); no timezone is stored.
pub fn DirectoryEntry::dos_date(self : DirectoryEntry) -> Int32 {
self.dos_date
}
///|
/// Original packed DOS modification time (hour/minute/two-second units).
pub fn DirectoryEntry::dos_time(self : DirectoryEntry) -> Int32 {
self.dos_time
}
///|
/// One bounded request into the same immutable archive supplied to the reader.
/// The offset is absolute; the length describes a single in-memory buffer.
pub struct ReadRange {
priv offset : UInt64
priv length : Int32
}
///|
/// Absolute byte offset from the start of the archive.
pub fn ReadRange::offset(self : ReadRange) -> UInt64 {
self.offset
}
///|
/// Exact byte count to supply on the next call, at most 65,557 bytes.
pub fn ReadRange::length(self : ReadRange) -> Int32 {
self.length
}
///|
/// The next range to read, or a completed, independently owned metadata listing.
pub(all) enum DirectoryStep {
NeedRead(ReadRange)
Done(Directory)
}
///|
/// Shared wire parser output. Only the in-memory reader retains source records
/// and patch positions; the directory reader keeps the owned entry metadata.
priv struct ParsedDirectoryRecord {
entry : DirectoryEntry
name_len : Int32
central_zip64_offset_position : Int32?
}
///|
fn parse_directory_record(
reader : Cursor,
) -> ParsedDirectoryRecord raise ZipError {
guard reader.read_u32_raw() == CENTRAL_HEADER_SIG else {
raise ZipError(InvalidSignature, "zip: invalid central directory signature")
}
let version_made_by = reader.read_u16()
ignore(reader.read_u16())
let flags = reader.read_u16()
let method_id = reader.read_u16()
let dos_time = reader.read_u16()
let dos_date = reader.read_u16()
let crc = reader.read_u32_raw()
let comp_size_raw = reader.read_u32_raw()
let uncomp_size_raw = reader.read_u32_raw()
let name_len = reader.read_u16()
let extra_len = reader.read_u16()
let comment_len = reader.read_u16()
let disk = reader.read_u16()
ignore(reader.read_u16())
let external_attributes = reader.read_u32_raw()
let local_offset_raw = reader.read_u32_raw()
let name_bytes = reader.read_bytes(name_len)
let extra_bytes = reader.read_bytes(extra_len)
let name = decode_name(name_bytes)
reader.skip(comment_len)
guard disk == 0 || disk == 0xffff else {
raise ZipError(
UnsupportedFeature,
"zip: multi-disk archives are not supported",
)
}
let need_uncomp = uncomp_size_raw == MAX_U32
let need_comp = comp_size_raw == MAX_U32
let need_offset = local_offset_raw == MAX_U32
let (zip_uncomp, zip_comp, zip_offset, offset_position) = read_zip64_extra(
extra_bytes,
need_uncomp,
need_comp,
need_offset,
need_disk=disk == 0xffff,
)
{
entry: {
name,
compressed_size: resolve_zip64_field(need_comp, zip_comp, comp_size_raw),
uncompressed_size: resolve_zip64_field(
need_uncomp, zip_uncomp, uncomp_size_raw,
),
local_offset: resolve_zip64_field(
need_offset, zip_offset, local_offset_raw,
),
method_id,
flags,
crc,
creator_system: version_made_by >> 8,
external_attributes,
dos_date,
dos_time,
},
name_len,
central_zip64_offset_position: offset_position.map(position => {
46 + name_len + position
}),
}
}