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.2.0
    License
    Apache-2.0
    Last updated
    2 hours ago
    Downloads
    37

    #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 say end=true with the last input.

    ///|
    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")
    }

    #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; the same step / Status shape as moonbit-community/flate.
    • 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.
    • Every malformed stream raises ZstdError(kind, message): Truncated, Corrupt (including bytes after the last frame), ChecksumMismatch, WindowTooLarge, DictionaryUnsupported or OutputLimitExceeded. It does not abort.

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

    #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 say end=true with the last input.

    ///|
    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")
    }

    #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; the same step / Status shape as moonbit-community/flate.
    • 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.
    • Every malformed stream raises ZstdError(kind, message): Truncated, Corrupt (including bytes after the last frame), ChecksumMismatch, WindowTooLarge, DictionaryUnsupported or OutputLimitExceeded. It does not abort.

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

    #License

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

    ZstdError

    pub suberror ZstdError {
    ZstdError(ZstdErrorKind, String)
    } derive(
    Debug
    )

    The error every decoding failure raises: a kind and a message.

    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.

    Decoder::Decoder

    fn Decoder::Decoder(max_window_bits? : Int) -> Decoder

    A decoder at the start of a stream. It refuses, with WindowTooLarge, 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.

    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. Zero after a step that raised.

    Decoder::last_produced

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

    Output bytes the latest step wrote. Zero after a step that raised.

    Decoder::reset

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

    Starts a new stream, 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 ZstdError

    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: 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 Corrupt. After an error the decoder raises it again until reset.

    Decoder::step_into

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

    step into a whole FixedArray, with bulk copies.

    Status

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

    Outcome of one Decoder::step, as in moonbit-community/flate.

    Status::equal

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

    Status::not_equal

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

    Status::to_repr

    ZstdErrorKind

    pub(all) enum ZstdErrorKind {
    Truncated
    Corrupt
    ChecksumMismatch
    WindowTooLarge
    DictionaryUnsupported
    OutputLimitExceeded
    } derive(Eq,
    Debug
    )

    Why a stream was refused.

    ZstdErrorKind::equal

    ZstdErrorKind::not_equal

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

    decompress

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

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

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io