This version of the module is deprecated: Use moonbitlang/mbtexcel instead

    #zip

    A pure MoonBit implementation of ZIP archive reading and writing. This package provides ZIP container handling for the XLSX file format.

    #Features

    • Read and write ZIP archives
    • Bounded reads and fail-before-growth bounded writes
    • DEFLATE compression support
    • GZIP compression/decompression
    • CRC32 checksum calculation

    #Types

    #Archive

    Archive is the main container for ZIP entries:

    ///|
    pub struct Archive {
    entries : Array[Entry]
    }

    #Entry

    Entry represents a single file in the archive:

    ///|
    pub struct Entry {
    name : String // File path within archive
    data : Bytes // Uncompressed file content
    compression : Compression
    data_descriptor : Bool
    }

    #Compression

    Supported compression methods:

    ///|
    pub enum Compression {
    Store // No compression
    Deflate // DEFLATE algorithm
    }

    #Usage

    #Creating Archives

    ///|
    test "create archive" {
    let archive = @zip.Archive::new()

    // Add uncompressed file
    archive.add("hello.txt", b"Hello, World!")

    // Add compressed file
    archive.add("data.txt", b"Large content here...", compression=Deflate)

    // List entries
    debug_inspect(
    archive.entries().map(entry => entry.name()),
    content="[\"hello.txt\", \"data.txt\"]",
    )

    // Get file content
    inspect(archive.get("hello.txt") == Some(b"Hello, World!"), content="true")
    }

    #Reading Archives

    ///|
    test "read archive" {
    // Create an archive
    let archive = @zip.Archive::new()
    archive.add("file1.txt", b"Content 1")
    archive.add("file2.txt", b"Content 2")

    // Write to bytes
    let bytes = @zip.write(archive)

    // Read back
    let loaded = @zip.read(bytes)
    debug_inspect(
    loaded.entries().map(entry => entry.name()),
    content="[\"file1.txt\", \"file2.txt\"]",
    )
    inspect(loaded.get("file1.txt") == Some(b"Content 1"), content="true")
    }

    #GZIP Compression

    ///|
    test "gzip" {
    let data : Bytes = b"Hello, World! This is some test data to compress."

    // Compress
    let compressed = @zip.gzip(data)

    // Decompress
    let decompressed = @zip.gunzip(compressed)
    inspect(
    decompressed,
    content="b\"Hello, World! This is some test data to compress.\"",
    )
    }

    #Iterating Entries

    ///|
    test "iterate entries" {
    let archive = @zip.Archive::new()
    archive.add("a.txt", b"A")
    archive.add("b.txt", b"B")
    archive.add("c.txt", b"C")

    // Iterate over entries
    for entry in archive.entries() {
    println("File: \{entry.name()}, Size: \{entry.data().length()}")
    }
    }

    #API Reference

    #Archive Methods

    MethodDescription
    Archive::new()Create an empty archive
    add(name, data, compression?, data_descriptor?)Add a file to the archive
    get(name)Get file content by name, returns Bytes?
    entries()Get view of all entries
    fork()Create a shallow, independently mutable snapshot
    bounded_source_package_size(...)Query non-forgeable pristine bounded-read provenance
    retained_size_estimate()Conservatively estimate retained archive memory

    #Functions

    FunctionDescription
    read(bytes)Parse ZIP bytes into Archive
    read_limited(bytes, ...)Parse while enforcing entry and expansion limits
    write(archive)Serialize Archive to ZIP bytes
    write_limited(archive, max_output_bytes=...)Prove the exact size without byte storage, then stream records and DEFLATE payloads into one fixed buffer below the ceiling
    gzip(bytes)Compress bytes using GZIP
    gunzip(bytes)Decompress GZIP bytes

    #Error Handling

    Operations that can fail raise ZipError:

    ///|
    pub suberror ZipError {
    OutOfBounds(offset~ : Int)
    InvalidSignature(expected~ : Int, actual~ : Int, offset~ : Int)
    MissingEndOfCentral
    UnsupportedFeature(msg~ : String)
    UnsupportedCompression(method_id~ : Int)
    InvalidUtf8(offset~ : Int)
    OutputLimitExceeded(limit~ : Int)
    ResourceLimitExceeded(kind~ : String, limit~ : Int, actual~ : Int)
    }

    Example error handling:

    ///|
    test "error handling" {
    // Invalid ZIP data
    try @zip.read(b"not a zip file") catch {
    _ => ()
    } noraise {
    _ => fail("expected read to raise")
    }
    }

    ZipError

    pub suberror ZipError {
    OutOfBounds(offset~ : Int)
    InvalidSignature(expected~ : Int, actual~ : Int, offset~ : Int)
    MissingEndOfCentral
    UnsupportedFeature(msg~ : String)
    UnsupportedCompression(method_id~ : Int)
    InvalidUtf8(offset~ : Int)
    OutputLimitExceeded(limit~ : Int)
    ResourceLimitExceeded(kind~ : String, limit~ : Int, actual~ : Int)
    ReadCancelled
    } derive(
    Debug
    )

    ZipError::to_repr

    #deprecated("implicit trait-method promotion is being removed; call via the trait")
    fn ZipError::to_repr(ZipError) ->
    Repr

    Archive

    type Archive

    Archive::add

    fn Archive::add(self : Archive, name : String, data : BytesView, compression? : Compression, data_descriptor? : Bool) -> Unit

    Archive::bounded_source_package_size

    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?

    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.

    Archive::entries

    fn Archive::entries(self : Archive) -> ArrayView[Entry]

    Archive::fork

    fn Archive::fork(self : Archive) -> Archive

    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.

    Archive::get

    fn Archive::get(self : Archive, name : StringView) -> BytesView?

    Returns a read-only view of an entry payload. The view may share storage with archive forks, but callers cannot use it to mutate archive state.

    Archive::new

    fn Archive::new() -> Archive

    Archive::replace

    fn Archive::replace(self : Archive, name : StringView, data : BytesView) -> Bool

    Replaces an exact entry while retaining its compression policy and source position. The next write preserves every other source entry byte-for-byte.

    Archive::retained_size_estimate

    fn Archive::retained_size_estimate(self : Archive) -> Int64

    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.

    Compression

    pub(all) enum Compression {
    Store
    Deflate
    } derive(
    Debug
    )

    Compression::to_repr

    #deprecated("implicit trait-method promotion is being removed; call via the trait")
    fn Compression::to_repr(Compression) ->
    Repr

    Entry

    type Entry

    Entry::central_directory_file_header_size

    fn Entry::central_directory_file_header_size(self : Entry) -> Int

    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.

    Entry::compression

    fn Entry::compression(self : Entry) -> Compression

    Entry::crc32

    fn Entry::crc32(self : Entry) -> UInt

    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.

    Entry::data

    fn Entry::data(self : Entry) -> BytesView

    Entry::data_descriptor

    fn Entry::data_descriptor(self : Entry) -> Bool

    Entry::name

    fn Entry::name(self : Entry) -> String

    crc32

    fn crc32(bytes : BytesView) -> UInt

    Computes the CRC-32 (IEEE, as used by ZIP) of a byte view.

    crc32_cancellable

    fn crc32_cancellable(bytes : BytesView, cancelled? : () -> Bool) -> UInt raise ZipError

    Computes ZIP CRC-32 while polling cancelled at bounded byte intervals. This is intended for archive verification on untrusted, size-bounded input; callers that do not need cooperative cancellation can use crc32.

    gunzip

    fn gunzip(bytes : BytesView) -> Bytes raise ZipError

    gzip

    fn gzip(bytes : BytesView) -> Bytes raise ZipError

    read

    fn read(bytes : BytesView) -> Archive raise ZipError

    Reads a ZIP archive using the library's compatibility defaults.

    read_limited

    fn read_limited(bytes : BytesView, max_package_bytes~ : Int, max_entries~ : Int, max_entry_uncompressed_bytes~ : Int, max_total_uncompressed_bytes~ : Int, max_total_preserved_source_bytes~ : Int, cancelled? : () -> Bool) -> Archive raise ZipError

    Reads a ZIP archive while enforcing caller-provided limits on source-package bytes, entries, inflated payloads, and retained byte-preservation records. Every bounded allocation is rejected before it is materialized.

    write

    fn write(archive : Archive) -> Bytes raise ZipError

    write_limited

    fn write_limited(archive : Archive, max_output_bytes~ : Int) -> Bytes raise ZipError

    Serializes an archive with an output-storage-free sizing pass that enforces a hard byte ceiling before allocating output. Local and central records, trailers, and DEFLATE payloads then stream into one exact fixed buffer; oversized output raises OutputLimitExceeded without materializing a candidate package or duplicate central directory.