flate

    Pure-MoonBit, runtime-agnostic DEFLATE (RFC 1951) engine with gzip/zlib/zip wrappers — suspendable and io-free.

    compression
    deflate
    inflate
    gzip
    zlib
    zip
    rfc1951
    Download zip
    Version
    0.7.3
    License
    Apache-2.0
    Last updated
    7 days ago
    Downloads
    8K

    #flate

    Pure-MoonBit DEFLATE (RFC 1951) — a runtime-agnostic, suspendable, io-free compression engine, with thin gzip (RFC 1952), zlib (RFC 1950), and zip (APPNOTE.TXT) container wrappers.

    #Install

    moon add moonbit-community/flate

    #Quick start

    The one-shot API is useful when the complete payload is already in memory:

    ///|
    test "README raw DEFLATE round-trip" {
    let source = b"runtime-agnostic streaming compression"
    let compressed = @flate.deflate_all(source)
    assert_eq(@flate.inflate_all(compressed), source)
    }

    The streaming API is a pure push state machine. It owns no I/O object, so both an async event loop and a future synchronous reader/writer adapter can feed and drain the same engine with whatever buffers their runtime provides:

    ///|
    test "README streaming Deflater" {
    let source = b"small buffers exercise suspension and resume"
    let encoder = @flate.Deflater()
    let chunk = FixedArray::make(3, b'\x00')
    let compressed = Buffer()
    let mut first = true
    for ;; {
    let input = if first { source[:] } else { b""[:] }
    let status = encoder.step(
    input,
    chunk.mut_view(),
    action=if first { Finish } else { Continue },
    )
    let consumed = encoder.last_consumed()
    let produced = encoder.last_produced()
    assert_eq(consumed, if first { source.length() } else { 0 })
    first = false
    for i in 0..<produced {
    compressed.write_byte(chunk[i])
    }
    if status is Done {
    break
    }
    }
    assert_eq(@flate.inflate_all(compressed.to_bytes()), source)
    }

    #State-machine contract

    • step returns the state-machine Status; last_consumed() and last_produced() describe exactly the prefixes accepted and written by that call. Drop only input[:consumed]; large input views may be partially consumed when output is backpressured, keeping internal memory bounded.
    • NeedMoreOutput preserves all pending work. Resume with another non-empty output view; one byte is sufficient.
    • Inflater owns the bounded tail of an incomplete atomic unit. On NeedMoreInput, feed the next non-overlapping chunk; no growing replay window is required.
    • Encoders take one DeflateAction: Continue, SyncFlush, or Finish. Pass the requested action with the final input of that batch. If consumed < input.length(), re-present that suffix with the same action. Once the complete view is accepted, the request remains latched across output backpressure.
    • Raw Inflater::step(..., end=true) and the container decoders turn physical EOF before Done into a stable truncation error. After any decoder error, discard or reset the raw engine; container decoder instances stably rethrow the same error.
    • Once Done is returned, new input is not consumed; reset the raw engine or create a new wrapper before reuse.
    • Configure a raw preset dictionary through Deflater(...)/Inflater(...), or while starting a fresh stream through reset(dictionary=...); dictionary selection cannot be mutated after a stream begins.
    • gzip/zlib decoders may accept transport read-ahead beyond their trailer; after Done, recover that suffix with unused_input() before parsing the next protocol frame. For an exact one-member gzip boundary, construct the decoder with multistream=false.
    • Raw failures expose a stable InflateErrorKind (Truncated, Corrupt, TrailingData, OutputLimitExceeded, or Cancelled) alongside diagnostic text; gzip/zlib similarly expose GzipErrorKind/ZlibErrorKind.
    • One-shot decoding (inflate_all, inflate_all_limited, inflate_exact) accepts an optional cancelled callback, polled at entry and then every ~4-8 KiB of decoded output; returning true raises InflateError(Cancelled, _), so untrusted-input loops can react to cancellation without streaming plumbing.

    #Exact and bounded one-shot decoding

    inflate_all remains the convenient trusted-input API: it accepts a raw DEFLATE prefix and grows its output without a limit. Use inflate_exact when the input must contain exactly one raw stream, and inflate_all_limited when decoded size must be bounded:

    ///|
    test "README exact and limited inflate" {
    let source = b"bounded convenience API"
    let compressed = @flate.deflate_all(source)
    assert_eq(@flate.inflate_exact(compressed), source)
    assert_eq(
    @flate.inflate_all_limited(compressed, max_output=source.length()),
    source,
    )
    assert_eq(
    @flate.inflate_exact(compressed, max_output=Some(source.length())),
    source,
    )
    }

    inflate_exact combines exact framing with optional bounds: max_output caps the decoded size (raising OutputLimitExceeded before any oversized result is built; negative limits are rejected), and preallocated=true replays the deterministic decode a second time into one exactly sized allocation. The preallocated mode trades roughly double the decode work for a peak memory of about one decoded output (instead of a growing buffer plus a final copy), which matters for very large single streams. The default stays single-pass for callers that do not need the memory bound.

    #ZIP container

    The @zip package reads and writes the ZIP (APPNOTE.TXT) container format over the same engine. It is io-free and suspendable like the rest of the library:

    ///|
    test "README zip round-trip" {
    let archive = @zip.Archive()
    archive.add("hello.txt", b"hello, zip")
    let bytes = @zip.write(archive)
    let parsed = @zip.read(bytes)
    assert_eq(parsed.get("hello.txt"), Some(b"hello, zip"[:]))
    }

    read accepts a ReadLimits value (package bytes, entry count, per-entry and total decompressed bytes, retained source records) and a cancelled callback, so untrusted archives cannot exhaust memory; exceeding a limit raises ZipError(LimitExceeded(kind, limit, actual)). STORED and DEFLATE entries, ZIP64 sizes/offsets, data descriptors, UTF-8 names, and archive comments are handled. write_preserving re-emits pristine entries byte-for-byte from their retained source records (round-tripping readwrite_preserving exactly), while write_limited / write_preserving_limited enforce an output ceiling without materializing an oversized candidate.

    #Architecture

    ENCODE: bytes → raw DEFLATE ═══════════════════════════ deflate_all Deflater::step deflate_all_split(input) deflate_all_optimal(input) one-shot streaming, suspendable; one-shot, adaptive one-shot, offline fixed 16 KB sync flush, preset dict block splitting (zopfli-style) blocks │ │ │ │ └──────┬───────┘ │ │ ▼ ▼ ▼ ┌─ parser ─────────────────┐ ┌─ planner ─────────────────┐ ┌─ parser ───────────────────┐ │ lz77.mbt │ │ split_plan.mbt │ │ optimal_parse.mbt │ │ greedy/lazy hash chains │ │ observation-divergence │ │ squeeze: iterated │ │ (levels 1-9; 0 = stored) │ │ split, arbitrated vs │ │ cost-optimal shortest path │ ├─ block driver ───────────┤ │ the 16 KB cadence │ ├─ planner ──────────────────┤ │ block_planner.mbt (strm) │ └────────────┬──────────────┘ │ optimal_plan.mbt │ │ or deflate_all.mbt inline│ │ │ content-driven splitting │ │ both: 16 KB blocks │ │ │ (FindMinimum + resplit) │ └────────────┬─────────────┘ └─────────────┬──────────────┘ └────────────┬───────────────────┘ ▼ packed tokens + caller-owned frequencies │ ← the seam: any parser/planner │ pair feeds the same writer ▼ block_writer.mbt ◄── huffman_build.mbt one block: stored / (package-merge length-limited fixed / dynamic, canonical codes) whichever is smallest │ ▼ bitwriter.mbt (LSB-first) → raw DEFLATE bit stream DECODE: raw DEFLATE → bytes ═══════════════════════════ raw DEFLATE bit stream │ huffman_table.mbt — code lengths → zlib-style chunked tables │ ┌───────────┴─────────────┐ ▼ ▼ Inflater::step inflate_all (inflate.mbt) (inflate_all.mbt) streaming, suspendable one-shot, growable output — (internal atomic staging); trusted input only 32 KB window, 1-byte output progress, preset dictionary CONTAINERS: thin framing over the engine ════════════════════════════════════════ gzip/ Encoder/Decoder, gzip_compress/gunzip (RFC 1952) 10 B header (optional fields skipped in O(1)) + CRC-32 + ISIZE; decodes concatenated multi-member streams (§2.2) zlib/ Encoder/Decoder, zlib_compress/zlib_decompress (RFC 1950) 2 B header (+ FDICT dictionary id) + Adler-32 zip/ read/write, Archive/Entry, bounded reads, ZIP64, (APPNOTE.TXT) data descriptors, byte-preserving rewrites checksum/ incremental CRC-32 / Adler-32 digests shared by both pipelines: tables.mbt RFC 1951 symbol tables, fixed codes, window size decode_rules.mbt pure distance/count/stored-block/history validation shared by the one-shot and streaming decoders status.mbt Done / NeedMoreInput / NeedMoreOutput

    inflate_all intentionally keeps its direct, growable-output MemDecoder fast path instead of paying the streaming state machine's staging and circular-window costs. Both decoders call the same pure RFC validation rules, and a deterministic differential-fuzz suite checks them across randomized chunk/output schedules, truncations, and bit flips.

    #Effort tiers

    All standard DEFLATE on the wire. Levels 0-9 (zlib's tuning table) on deflate_all / Deflater. deflate_all_split is a one-shot mid-tier that tokenizes each large window once with the full 32 KB history and cuts blocks where the literal/match observation distribution drifts (libdeflate's observation-divergence splitter, split_plan.mbt), arbitrating each chunk against the fixed 16 KB cadence by exact cost so it is never larger — typically several percent smaller on prose, code, and repetitive data. deflate_all_optimal adds zopfli-style iterated optimal parsing (optimal_parse.mbt) plus content-driven block splitting (optimal_plan.mbt) — drop-in replacements for the greedy parser (lz77.mbt) and the threshold planner (block_planner.mbt) behind the same (tokens, frequencies) → emit_block seam.

    The containers expose both tiers: gzip_compress / zlib_compress and their streaming Encoders take level 0-9; gzip_compress_optimal / zlib_compress_optimal apply the offline zopfli path (one-shot only — a streaming encoder cannot suspend a whole-input optimal parse), for compress-once, serve-forever artifacts.

    InflateError

    pub suberror InflateError {
    InflateError(InflateErrorKind, String)
    }

    Error raised while decoding a raw DEFLATE stream.

    DeflateAction

    pub(all) enum DeflateAction {
    Continue
    SyncFlush
    Finish
    } derive(Eq,
    Debug
    )

    Requested action for one streaming compression step.

    The action applies after the complete input view has been consumed. If a step reports partial consumption, re-present the remaining suffix with the same action.

    DeflateAction::equal

    DeflateAction::not_equal

    fn DeflateAction::not_equal(x : DeflateAction, y : DeflateAction) -> Bool

    Deflater

    pub struct Deflater {
    // private fields
    }

    Streaming DEFLATE compressor : a push-based, suspendable state machine. It emits the stream block-by-block as input arrives, and chooses fixed- or dynamic-Huffman per block.

    Deflater::Deflater

    fn Deflater::Deflater(level? : Int, dictionary? : BytesView) -> Deflater

    Create a fresh compressor. level 0-9 (clamped) trades speed for ratio with zlib-equivalent tuning; 0 emits stored blocks only, 6 is the default. dictionary, when supplied, preloads the match window with its last 32 KB; the decoder must use the same preset dictionary.

    Deflater::is_finished

    fn Deflater::is_finished(self : Deflater) -> Bool

    Whether the final compressed stream has been fully emitted.

    Deflater::last_consumed

    fn Deflater::last_consumed(self : Deflater) -> Int

    Number of input bytes accepted by the latest step call. This is zero initially, after reset, and for the stable Done state.

    Deflater::last_produced

    fn Deflater::last_produced(self : Deflater) -> Int

    Number of output bytes written by the latest step call. This is zero initially, after reset, and for the stable Done state.

    Deflater::reset

    fn Deflater::reset(self : Deflater, dictionary? : BytesView) -> Unit

    Reset to the start of a fresh DEFLATE stream, keeping the compression level and reusing allocations across many streams (e.g. ZIP entries). With no dictionary, the history is cleared. Supplying one immediately preloads its last 32 KB for the new stream; the decoder must use the same preset dictionary.

    Deflater::step

    fn Deflater::step(self : Deflater, input : BytesView, output : MutArrayView[Byte], action? : DeflateAction) -> Status

    Run one compression step. Accepts as much of input as fits in one bounded block window, emits blocks, and drains compressed bytes into output. Returns the status; read last_consumed() and last_produced() immediately afterwards for this call's counts. Drop only input[:last_consumed()] and re-present the suffix. With a small output buffer a large input may be consumed only partially, which propagates output backpressure instead of growing internal memory with the caller's input size. Once a final block has been emitted, calls only drain that block and consume no new input; after completion they idempotently return Done with both counts zero.

    Use action=Finish with the final input view. The action is accepted after that entire view has been consumed, then remains latched across NeedMoreOutput; if a call reports partial consumption, re-present the suffix with action=Finish. Later input is left unconsumed.

    action=SyncFlush similarly requests zlib-style Z_SYNC_FLUSH: everything buffered is compressed and the bit stream is padded to a byte boundary with an empty stored block, so all produced bytes are final and transmittable while the stream continues. A sync-flush is sticky across output backpressure once accepted. Continue only accepts input. An empty output may accept at most one bounded block window before returning NeedMoreOutput.

    InflateErrorKind

    pub(all) enum InflateErrorKind {
    Truncated
    Corrupt
    TrailingData
    OutputLimitExceeded
    Cancelled
    } derive(Eq,
    Debug
    )

    Stable classification for failures raised by the raw DEFLATE APIs.

    InflateErrorKind::equal

    InflateErrorKind::not_equal

    fn InflateErrorKind::not_equal(x : InflateErrorKind, y : InflateErrorKind) -> Bool

    Inflater

    pub struct Inflater {
    // private fields
    }

    Streaming DEFLATE decompressor (RFC 1951): a push-based, suspendable state machine. All cross-call state lives here, so it pauses on NeedMoreInput/NeedMoreOutput and resumes on the next step. Supports stored, fixed- and dynamic-Huffman blocks. step makes progress with as little as one byte of output room (the output view): a match too large for the buffer is copied in pieces across calls.

    Inflater::Inflater

    fn Inflater::Inflater(dictionary? : BytesView) -> Inflater

    Create a fresh decompressor positioned at the start of a DEFLATE stream. If supplied, dictionary preloads the history window with its final 32 KiB.

    Inflater::is_finished

    fn Inflater::is_finished(self : Inflater) -> Bool

    Whether the end of the DEFLATE stream has been reached.

    Inflater::last_consumed

    fn Inflater::last_consumed(self : Inflater) -> Int

    Number of input bytes accepted by the latest step call. This is zero initially, after reset, after a call raises, and in the stable Done state.

    Inflater::last_produced

    fn Inflater::last_produced(self : Inflater) -> Int

    Number of output bytes written by the latest step call. This is zero initially, after reset, after a call raises, and in the stable Done state.

    Inflater::reset

    fn Inflater::reset(self : Inflater, dictionary? : BytesView) -> Unit

    Reset to the start of a fresh DEFLATE stream, reusing allocations (the 32 KB window and Huffman table storage) across many streams (e.g. ZIP entries). The stale window contents are unreachable afterwards: any distance reaching past the new stream's start is rejected (dist > wpos). Pass dictionary to preload the fresh stream's history window.

    Inflater::step

    fn Inflater::step(self : Inflater, input : BytesView, output : MutArrayView[Byte], end? : Bool) -> Status raise InflateError

    Run one decompression step. Reads from input, writes into output, and returns the status. After a normal return, read last_consumed() and last_produced() for this call's counts, and drop exactly input[:last_consumed()]. An empty output makes no progress (NeedMoreOutput, zero produced); one output byte always suffices to advance. Both counts are cleared before every call and remain zero if the call raises.

    Headers and Huffman symbols remain atomic internally, but an incomplete unit's small tail is retained by the engine. Therefore callers may feed ordinary non-overlapping chunks: on NeedMoreInput the complete supplied view has been accepted and need not be re-presented. Pass end=true with the physical final input view; the signal is sticky across output backpressure, and an incomplete stream then raises InflateError instead of returning NeedMoreInput forever. Once any error is raised, later calls stably raise the same error until reset().

    Status

    pub(all) enum Status {
    Done
    NeedMoreInput
    NeedMoreOutput
    } derive(Eq,
    Debug
    )

    Outcome of one step of the streaming DEFLATE state machine.

    The engine is runtime-agnostic: it moves bytes between an input view and an output buffer and suspends by returning one of these, so any driver — a synchronous loop or an async adapter — can resume it.

    Status::equal

    fn Status::equal(Status, Status) -> Bool

    Status::not_equal

    fn Status::not_equal(x : Status, y : Status) -> Bool

    Status::to_repr

    DEFLATE_BLOCK_SIZE

    let DEFLATE_BLOCK_SIZE : Int

    Target tokenizable bytes per block, shared by the streaming Deflater and one-shot deflate_all. 16 KB = zlib's default token-buffer size (memLevel 8 gives lit_bufsize = 2^(8+6) = 16384), the field-tested balance: large enough to amortize the dynamic-Huffman table header over many bytes, small enough that one table tracks roughly-stationary statistics and that streaming bounds latency and memory rather than spanning whole megabytes.

    HASH_BITS

    let HASH_BITS : Int

    HUFFMAN_CHUNK_BITS

    let HUFFMAN_CHUNK_BITS : Int

    HUFFMAN_VALUE_SHIFT

    let HUFFMAN_VALUE_SHIFT : Int

    MAX_NUM_DIST

    let MAX_NUM_DIST : Int

    MAX_NUM_LIT

    let MAX_NUM_LIT : Int

    MIN_LOOKAHEAD

    let MIN_LOOKAHEAD : Int

    Bytes of unconsumed lookahead reserved beyond a non-final block boundary so matches there see their full extent.

    NUM_CODES

    let NUM_CODES : Int

    OPTIMAL_MASTER_BLOCK

    let OPTIMAL_MASTER_BLOCK : Int

    Splitting and parsing happen within master chunks of this many input bytes, bounding the planner's working memory (zopfli's MASTER_BLOCK_SIZE plays the same role).

    WINDOW_SIZE

    let WINDOW_SIZE : Int

    The 32 KB sliding window: the largest back-reference distance DEFLATE can express, since distance codes top out at 32768 (RFC 1951 §3.2.5, "the compressor terminates a block ... distances are limited to 32K bytes"; the distance code table in §3.2.5 maps the largest code to base 24577 + 8192).

    deflate_all

    fn deflate_all(input : Bytes, level? : Int) -> Bytes

    Compress bytes into a complete raw DEFLATE stream held entirely in memory. Because the whole input is in hand, no sliding window is needed: this tokenizes directly over input, advancing a single offset, with none of the streaming Deflater's per-block buffering.

    deflate_all_optimal

    fn deflate_all_optimal(input : Bytes, iterations? : Int) -> Bytes

    Compress with zopfli-style effort: content-driven block splitting plus iterated optimal parsing, with a second splitting pass over the optimal tokens (blocksplittinglast). Tens to hundreds of times slower than deflate_all — for compress-once, serve-forever artifacts. iterations is the squeeze count per block (zopfli's default is 15).

    deflate_all_split

    fn deflate_all_split(input : Bytes, level? : Int) -> Bytes

    Compress with content-driven block splitting: tokenize each master chunk once (full 32 KB cross-block history), cut the token stream where the symbol statistics drift, and emit one dynamic-Huffman block per segment. A mid-tier between the fixed-cadence deflate_all and the offline zopfli deflate_all_optimal.

    inflate_all

    fn inflate_all(input : Bytes, cancelled? : () -> Bool) -> Bytes raise InflateError

    Decompress a raw DEFLATE stream held entirely in memory. Bytes after the final block are ignored; use inflate_exact when an exact framing boundary is required. The output grows without bound; for untrusted input use inflate_all_limited or drive Inflater with caller-sized output buffers. If supplied, cancelled is polled at entry and then every 4096 produced bytes; returning true raises InflateError(Cancelled, _).

    inflate_all_limited

    fn inflate_all_limited(input : Bytes, max_output~ : Int, cancelled? : () -> Bool) -> Bytes raise InflateError

    Decompress one raw DEFLATE stream without allowing the returned output to exceed max_output. Bytes after the final block retain inflate_all's prefix semantics and are ignored. cancelled follows inflate_all's polling contract.

    inflate_exact

    fn inflate_exact(input : Bytes, max_output? : Int?, cancelled? : () -> Bool, preallocated? : Bool) -> Bytes raise InflateError

    Decompress exactly one raw DEFLATE stream. Unlike inflate_all, this rejects any bytes after the final block.

    Optional controls:
    • max_output: decoded output beyond this size raises InflateError(OutputLimitExceeded, _). A negative value is rejected the same way.
    • cancelled: polled at entry and then once per internal step (every 8192 produced bytes); returning true raises InflateError(Cancelled, _).
    • preallocated: when true, decode runs twice — a counting pass that validates the whole stream, then a second pass filling one exactly sized allocation. Peak memory stays at roughly one decoded output, at the cost of roughly double the decode work; the default false grows one buffer in a single pass, trading transient memory for one decode pass.