flate

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

compression
deflate
inflate
gzip
zlib
rfc1951
moon add moonbit-community/flate@0.4.0
Download zip
Version
0.4.0
License
Apache-2.0
Last updated
last month
Downloads
639
README

#flate

Pure-MoonBit DEFLATE (RFC 1951) — a runtime-agnostic, suspendable, io-free compression engine, with thin gzip (RFC 1952) and zlib (RFC 1950) 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::new()
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::new/Inflater::new, 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, or OutputLimitExceeded) alongside diagnostic text; gzip/zlib similarly expose GzipErrorKind/ZlibErrorKind.

#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,
)
}

#Architecture

ENCODE: bytes → raw DEFLATE ═══════════════════════════ deflate_all Deflater::step deflate_all_optimal(input) one-shot streaming, suspendable; one-shot, offline (zopfli-style) greedy sync flush, preset dict │ │ │ └──────┬───────┘ │ ▼ ▼ ┌─ parser ─────────────────┐ ┌─ parser ───────────────────┐ │ lz77.mbt │ │ optimal_parse.mbt │ │ greedy/lazy hash chains │ │ squeeze: iterated │ │ (levels 1-9; 0 = stored) │ │ cost-optimal shortest path │ ├─ block driver ───────────┤ ├─ 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 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_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.

#
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::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::new

fn Deflater::new(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::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
} derive(Eq,
Debug
)

Stable classification for failures raised by the raw DEFLATE APIs.

#
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::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::new

fn Inflater::new(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::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.

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

#
inflate_all

fn inflate_all(input : Bytes) -> 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.

#
inflate_all_limited

fn inflate_all_limited(input : Bytes, max_output~ : Int) -> 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.

#
inflate_exact

fn inflate_exact(input : Bytes) -> Bytes raise InflateError

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