README

#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.

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

ยฉ 2026 mooncakes.io