Sign in

    zstd

    A streaming Zstandard (RFC 8878) decoder in pure MoonBit: arbitrary input chunks, bounded output slices, memory bounded by the window.

    zstd
    zstandard
    compression
    decompression
    rfc8878
    streaming
    Download zip
    Author
    Version
    0.3.1
    License
    Apache-2.0
    Last updated
    4 hours ago
    Downloads
    339

    Dependencies

    #gaato/zstd

    A streaming Zstandard (RFC 8878) decoder in pure MoonBit. Native, JS, Wasm and Wasm GC.

    moon add gaato/zstd

    One shot:

    ///|
    test "decompress a whole stream" {
    let compressed = b"\x28\xb5\x2f\xfd\x20\x05\x29\x00\x00\x68\x65\x6c\x6c\x6f" // zstd.compress(b"hello")
    let text = @zstd.decompress(compressed, max_output=16 * 1024 * 1024)
    assert_eq(text, b"hello")
    }

    Streaming: feed input as it arrives, take output in slices of the size you choose. A stream may hold several frames and nothing marks the last one, so every step says whether its input is the last: end=false before, end=true with the last input. Status is gaato/codec_bytes's, re-exported: match Done, NeedMoreInput and NeedMoreOutput unqualified, or write @zstd.Status::Done.

    ///|
    fn decode_all(chunks : Array[Bytes], sink : (BytesView) -> Unit) -> Unit raise {
    let decoder = @zstd.Decoder(max_window_bits=23) // 8 MiB, as RFC 9659 asks of HTTP
    let out = FixedArray::make(64 * 1024, b'\x00')
    for i, chunk in chunks {
    let mut input = chunk[:]
    for ;; {
    let status = decoder.step_into(input, out, end=i == chunks.length() - 1)
    input = input[decoder.last_consumed():]
    sink(Bytes::from_array(out[:decoder.last_produced()]))
    match status {
    Done => return
    NeedMoreInput => break
    NeedMoreOutput => ()
    }
    }
    }
    }

    ///|
    test "decode a stream chunk by chunk" {
    let decoded = Buffer()
    decode_all(
    [b"\x28\xb5\x2f", b"\xfd\x20\x05\x29\x00\x00\x68", b"\x65\x6c\x6c\x6f"],
    bytes => decoded.write_bytes(bytes.to_owned()),
    )
    assert_eq(decoded.to_bytes(), b"hello")
    }

    A frame followed by something else: until=EndOfMember stops right after the first frame and consumes nothing past it, so what follows is input[last_consumed():]. A skippable frame counts as a frame here.

    ///|
    test "stop after the first frame" {
    let frame = b"\x28\xb5\x2f\xfd\x20\x05\x29\x00\x00\x68\x65\x6c\x6c\x6f"
    let input = Bytes::from_array([..frame[:], ..b"rest"])
    let decoder = @zstd.Decoder(until=EndOfMember)
    let out = FixedArray::make(64, b'\x00')
    let status = decoder.step_into(input, out, end=true)
    assert_true(status is Done)
    assert_eq(Bytes::from_array(out[:decoder.last_produced()]), b"hello")
    assert_eq(input[decoder.last_consumed():], b"rest")
    }

    Decoder implements gaato/codec_bytes's Decode, so code written against that contract (an HTTP client, a command-line tool) takes it as it takes any other decoder there.

    #What it does

    • Decodes every RFC 8878 frame without a dictionary: raw, RLE and compressed blocks, Huffman-coded literals (one or four streams, with or without a new tree), every FSE table mode, and the content checksum (XXH64). A stream may hold several frames, and skippable frames among them, as the zstd tool writes them.
    • Takes input in chunks of any size, down to one byte, and writes output into buffers of any size, under the rules of gaato/codec_bytes.
    • Memory is the window (at most the frame's Window_Size, and no more than its output so far), one compressed block of at most 128 KiB and its literals, plus about 11 KiB of tables. Decoder(max_window_bits=n) refuses frames that need a window over 2^n bytes, before allocating anything; the default 27 (128 MiB) is the reference decoder's. memory_usage() reports the window and the block buffers: at most 2^n bytes plus 256 KiB.
    • Every malformed stream raises gaato/codec's CodecError(kind, message), with the kind Truncated, Corrupt, TrailingData (bytes after the last frame that start no frame), ChecksumMismatch, LimitExceeded (a window over max_window_bits, with resource: Memory; more than max_output from decompress, with resource: Output) or DictionaryRequired (with the frame's dictionary id). It does not abort. In a catch on these functions, CodecError(kind, _) and the kinds match unqualified, with no import; to name the types elsewhere, import gaato/codec_bytes, which re-exports them.
    • A step that raises first hands out the output that comes before the error (answering NeedMoreOutput while it does not fit), and last_consumed() and last_produced() count what that step took and wrote: a corrupt stream gives the same output, then the same error, however it is split. On TrailingData, last_consumed() stops at the end of the stream.
    • The header decisions (frame, block and literals section headers, the window size and the jump table) and the Huffman decoding table built from a tree description are proved against RFC 8878 with moon prove; PROOFS.md lists what is proved and what is trusted.

    Not included: an encoder, and dictionaries (RFC 8878 says the zstd content coding should not use one; a frame that names one raises DictionaryRequired(Some(id))).

    #License

    Apache-2.0. The test frames under testdata/upstream come from facebook/zstd under its BSD license; see NOTICE.

    #gaato/zstd

    A streaming Zstandard (RFC 8878) decoder in pure MoonBit. Native, JS, Wasm and Wasm GC.

    moon add gaato/zstd

    One shot:

    ///|
    test "decompress a whole stream" {
    let compressed = b"\x28\xb5\x2f\xfd\x20\x05\x29\x00\x00\x68\x65\x6c\x6c\x6f" // zstd.compress(b"hello")
    let text = @zstd.decompress(compressed, max_output=16 * 1024 * 1024)
    assert_eq(text, b"hello")
    }

    Streaming: feed input as it arrives, take output in slices of the size you choose. A stream may hold several frames and nothing marks the last one, so every step says whether its input is the last: end=false before, end=true with the last input. Status is gaato/codec_bytes's, re-exported: match Done, NeedMoreInput and NeedMoreOutput unqualified, or write @zstd.Status::Done.

    ///|
    fn decode_all(chunks : Array[Bytes], sink : (BytesView) -> Unit) -> Unit raise {
    let decoder = @zstd.Decoder(max_window_bits=23) // 8 MiB, as RFC 9659 asks of HTTP
    let out = FixedArray::make(64 * 1024, b'\x00')
    for i, chunk in chunks {
    let mut input = chunk[:]
    for ;; {
    let status = decoder.step_into(input, out, end=i == chunks.length() - 1)
    input = input[decoder.last_consumed():]
    sink(Bytes::from_array(out[:decoder.last_produced()]))
    match status {
    Done => return
    NeedMoreInput => break
    NeedMoreOutput => ()
    }
    }
    }
    }

    ///|
    test "decode a stream chunk by chunk" {
    let decoded = Buffer()
    decode_all(
    [b"\x28\xb5\x2f", b"\xfd\x20\x05\x29\x00\x00\x68", b"\x65\x6c\x6c\x6f"],
    bytes => decoded.write_bytes(bytes.to_owned()),
    )
    assert_eq(decoded.to_bytes(), b"hello")
    }

    A frame followed by something else: until=EndOfMember stops right after the first frame and consumes nothing past it, so what follows is input[last_consumed():]. A skippable frame counts as a frame here.

    ///|
    test "stop after the first frame" {
    let frame = b"\x28\xb5\x2f\xfd\x20\x05\x29\x00\x00\x68\x65\x6c\x6c\x6f"
    let input = Bytes::from_array([..frame[:], ..b"rest"])
    let decoder = @zstd.Decoder(until=EndOfMember)
    let out = FixedArray::make(64, b'\x00')
    let status = decoder.step_into(input, out, end=true)
    assert_true(status is Done)
    assert_eq(Bytes::from_array(out[:decoder.last_produced()]), b"hello")
    assert_eq(input[decoder.last_consumed():], b"rest")
    }

    Decoder implements gaato/codec_bytes's Decode, so code written against that contract (an HTTP client, a command-line tool) takes it as it takes any other decoder there.

    #What it does

    • Decodes every RFC 8878 frame without a dictionary: raw, RLE and compressed blocks, Huffman-coded literals (one or four streams, with or without a new tree), every FSE table mode, and the content checksum (XXH64). A stream may hold several frames, and skippable frames among them, as the zstd tool writes them.
    • Takes input in chunks of any size, down to one byte, and writes output into buffers of any size, under the rules of gaato/codec_bytes.
    • Memory is the window (at most the frame's Window_Size, and no more than its output so far), one compressed block of at most 128 KiB and its literals, plus about 11 KiB of tables. Decoder(max_window_bits=n) refuses frames that need a window over 2^n bytes, before allocating anything; the default 27 (128 MiB) is the reference decoder's. memory_usage() reports the window and the block buffers: at most 2^n bytes plus 256 KiB.
    • Every malformed stream raises gaato/codec's CodecError(kind, message), with the kind Truncated, Corrupt, TrailingData (bytes after the last frame that start no frame), ChecksumMismatch, LimitExceeded (a window over max_window_bits, with resource: Memory; more than max_output from decompress, with resource: Output) or DictionaryRequired (with the frame's dictionary id). It does not abort. In a catch on these functions, CodecError(kind, _) and the kinds match unqualified, with no import; to name the types elsewhere, import gaato/codec_bytes, which re-exports them.
    • A step that raises first hands out the output that comes before the error (answering NeedMoreOutput while it does not fit), and last_consumed() and last_produced() count what that step took and wrote: a corrupt stream gives the same output, then the same error, however it is split. On TrailingData, last_consumed() stops at the end of the stream.
    • The header decisions (frame, block and literals section headers, the window size and the jump table) and the Huffman decoding table built from a tree description are proved against RFC 8878 with moon prove; PROOFS.md lists what is proved and what is trusted.

    Not included: an encoder, and dictionaries (RFC 8878 says the zstd content coding should not use one; a frame that names one raises DictionaryRequired(Some(id))).

    #License

    Apache-2.0. The test frames under testdata/upstream come from facebook/zstd under its BSD license; see NOTICE.

    Status

    using @gaato/codec_bytes { type Status }

    Outcome of one Decoder::step: Done, NeedMoreInput or NeedMoreOutput, as every gaato/codec_bytes decoder answers.

    Decoder

    pub struct Decoder {
    // private fields
    }

    A streaming Zstandard decoder (RFC 8878). Feed it input in chunks of any size with step and collect output in slices of any size; it keeps the window (no more than a frame's Window_Size, and no more than its output so far), one compressed block and fixed-size tables, never the whole input or output.
    impl Decode for Decoder

    Decoder::Decoder

    fn Decoder::Decoder(max_window_bits? : Int, until? :
    Until
    ) -> Decoder

    A decoder at the start of a stream. It refuses, with LimitExceeded({ resource: Memory, .. }), a frame whose Window_Size exceeds 1 << max_window_bits bytes (the window memory it would need). The default 27 is the reference decoder's default limit; RFC 9659 asks HTTP clients to accept 23 (8 MiB). Values are clamped to 10..30.

    until says where the stream ends. EndOfInput, the default, decodes every frame up to the end of the input. EndOfMember stops right after the first frame, a skippable frame included, and consumes nothing past it, so that input[last_consumed():] is what follows.

    Decoder::is_finished

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

    Whether the stream has ended and all its output has been handed out.

    Decoder::last_consumed

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

    Input bytes the latest step accepted: all of them on NeedMoreInput. A step that raised accepted the bytes up to the error; for TrailingData, exactly up to the end of the stream, or none if the stream ended before this step's input.

    Decoder::last_produced

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

    Output bytes the latest step wrote, a step that raised included: the output before the error.

    Decoder::memory_usage

    fn Decoder::memory_usage(self : Decoder) -> Int

    Bytes held in buffers sized by the stream: the window, the compressed block and its literals. The fixed-size tables are not counted. At most (1 << max_window_bits) + 2 * 128 KiB; the window is released when a frame needs a smaller one, so it may drop between frames.

    Decoder::reset

    fn Decoder::reset(self : Decoder) -> Unit

    Starts a new stream with the same options, keeping the tables' memory. The window and the block buffers are released.

    Decoder::step

    fn Decoder::step(self : Decoder, input : BytesView, output : MutArrayView[Byte], end~ : Bool) ->
    Status
    raise
    CodecError

    Decodes from input into output; see last_consumed and last_produced for how much of each it used.

    A stream is one or more frames, and nothing marks its last one, so pass end=true once input holds the last bytes and end=false before: the stream then ends after the frame in progress, and a stream cut short raises Truncated. Without end, the decoder answers NeedMoreInput between frames. Bytes after the last frame that start no frame raise TrailingData. Under until=EndOfMember the stream ends after its first frame, whatever end says.

    An error comes after all the output before it: when output fills first, the step answers NeedMoreOutput and a later one hands out the rest and raises. After an error the decoder raises it again, consuming and writing nothing, until reset.

    Decoder::step_into

    fn Decoder::step_into(self : Decoder, input : BytesView, output : FixedArray[Byte], end~ : Bool) ->
    Status
    raise
    CodecError

    step into a whole FixedArray, with bulk copies.

    decompress

    fn decompress(input : BytesView, max_output? : Int, max_window_bits? : Int) -> Bytes raise
    CodecError

    Decodes a whole Zstandard stream held in memory: one or more frames. max_output caps the decoded size (unlimited by default) and raises LimitExceeded({ resource: Output, .. }) past it; max_window_bits is as for Decoder.

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io