Sign in

    brotli

    A streaming Brotli (RFC 7932) decoder and encoder in pure MoonBit: arbitrary input chunks, bounded output slices, memory bounded by the window.

    brotli
    compression
    decompression
    encoder
    rfc7932
    streaming
    Download zip
    Author
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    10 hours ago
    Downloads
    258

    #gaato/brotli

    A streaming Brotli (RFC 7932) decoder and encoder in pure MoonBit. Native, JS, Wasm and Wasm GC.

    moon add gaato/brotli

    One shot:

    ///|
    test "decompress a whole stream" {
    let compressed = b"\x0b\x02\x80hello\x03" // brotli.compress(b"hello")
    let text = @brotli.decompress(compressed, max_output=16 * 1024 * 1024)
    assert_eq(text, b"hello")
    }

    ///|
    test "compress and decompress" {
    let text = b"hello, hello, hello, brotli"
    let compressed = @brotli.compress(text)
    assert_eq(@brotli.decompress(compressed), text)
    }

    Streaming: feed input as it arrives, take output in slices of the size you choose.

    ///|
    fn decode_all(chunks : Array[Bytes], sink : (BytesView) -> Unit) -> Unit raise {
    let decoder = @brotli.Decoder()
    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"\x0b\x02", b"\x80hel", b"lo\x03"], bytes => {
    decoded.write_bytes(bytes.to_owned())
    })
    assert_eq(decoded.to_bytes(), b"hello")
    }

    Encoding works the same way. Flush makes the output so far decode to the input so far, say to send one part of an HTTP response; Finish ends the stream.

    ///|
    fn encode_parts(parts : Array[Bytes], sink : (BytesView) -> Unit) -> Unit {
    let encoder = @brotli.Encoder()
    let out = FixedArray::make(64 * 1024, b'\x00')
    for i, part in parts {
    let action : @brotli.EncodeAction = if i == parts.length() - 1 {
    Finish
    } else {
    Flush
    }
    let mut input = part[:]
    for ;; {
    let status = encoder.step_into(input, out, action~)
    input = input[encoder.last_consumed():]
    sink(Bytes::from_array(out[:encoder.last_produced()]))
    if status is (Done | NeedMoreInput) {
    break
    }
    }
    }
    }

    ///|
    test "encode a stream part by part" {
    let encoded = Buffer()
    encode_parts([b"<html>", b"<body>hello</body>", b"</html>"], bytes => {
    encoded.write_bytes(bytes.to_owned())
    })
    let decoded = @brotli.decompress(encoded.to_bytes())
    assert_eq(decoded, b"<html><body>hello</body></html>")
    }

    #What it does

    • Decodes every RFC 7932 stream, including the static dictionary and its 121 transforms.
    • 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 16 MiB, and no more than the output so far) plus tables of at most 1.3 MiB. Decoder(max_window_bits=n) refuses streams that need a window over 2^n bytes.
    • Every malformed stream raises BrotliError(kind, message): Truncated, Corrupt, TrailingData (bytes after the stream), WindowTooLarge or OutputLimitExceeded. It does not abort.
    • Encodes with greedy LZ77 (lazy by one byte) and one prefix code per meta-block: about google/brotli's quality 4, and close to gzip -6. Its streams decode with google/brotli's decoder (scripts/crosscheck.py).
    • The encoder's memory is its window (Encoder(window_bits=n), 22 by default: 4 MiB, growing with the input to at most twice what it has taken, and up to a quarter more before it slides) plus a 64 KiB meta-block, its output and about 1 MiB of fixed tables.

    Not included: higher-quality encoding (context modeling, block splitting, static dictionary references), and the non-RFC large-window format.

    #License

    Apache-2.0. The static dictionary in src/static_data.mbt comes from google/brotli under the MIT license; see LICENSE-dictionary.

    #gaato/brotli

    A streaming Brotli (RFC 7932) decoder and encoder in pure MoonBit. Native, JS, Wasm and Wasm GC.

    moon add gaato/brotli

    One shot:

    ///|
    test "decompress a whole stream" {
    let compressed = b"\x0b\x02\x80hello\x03" // brotli.compress(b"hello")
    let text = @brotli.decompress(compressed, max_output=16 * 1024 * 1024)
    assert_eq(text, b"hello")
    }

    ///|
    test "compress and decompress" {
    let text = b"hello, hello, hello, brotli"
    let compressed = @brotli.compress(text)
    assert_eq(@brotli.decompress(compressed), text)
    }

    Streaming: feed input as it arrives, take output in slices of the size you choose.

    ///|
    fn decode_all(chunks : Array[Bytes], sink : (BytesView) -> Unit) -> Unit raise {
    let decoder = @brotli.Decoder()
    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"\x0b\x02", b"\x80hel", b"lo\x03"], bytes => {
    decoded.write_bytes(bytes.to_owned())
    })
    assert_eq(decoded.to_bytes(), b"hello")
    }

    Encoding works the same way. Flush makes the output so far decode to the input so far, say to send one part of an HTTP response; Finish ends the stream.

    ///|
    fn encode_parts(parts : Array[Bytes], sink : (BytesView) -> Unit) -> Unit {
    let encoder = @brotli.Encoder()
    let out = FixedArray::make(64 * 1024, b'\x00')
    for i, part in parts {
    let action : @brotli.EncodeAction = if i == parts.length() - 1 {
    Finish
    } else {
    Flush
    }
    let mut input = part[:]
    for ;; {
    let status = encoder.step_into(input, out, action~)
    input = input[encoder.last_consumed():]
    sink(Bytes::from_array(out[:encoder.last_produced()]))
    if status is (Done | NeedMoreInput) {
    break
    }
    }
    }
    }

    ///|
    test "encode a stream part by part" {
    let encoded = Buffer()
    encode_parts([b"<html>", b"<body>hello</body>", b"</html>"], bytes => {
    encoded.write_bytes(bytes.to_owned())
    })
    let decoded = @brotli.decompress(encoded.to_bytes())
    assert_eq(decoded, b"<html><body>hello</body></html>")
    }

    #What it does

    • Decodes every RFC 7932 stream, including the static dictionary and its 121 transforms.
    • 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 16 MiB, and no more than the output so far) plus tables of at most 1.3 MiB. Decoder(max_window_bits=n) refuses streams that need a window over 2^n bytes.
    • Every malformed stream raises BrotliError(kind, message): Truncated, Corrupt, TrailingData (bytes after the stream), WindowTooLarge or OutputLimitExceeded. It does not abort.
    • Encodes with greedy LZ77 (lazy by one byte) and one prefix code per meta-block: about google/brotli's quality 4, and close to gzip -6. Its streams decode with google/brotli's decoder (scripts/crosscheck.py).
    • The encoder's memory is its window (Encoder(window_bits=n), 22 by default: 4 MiB, growing with the input to at most twice what it has taken, and up to a quarter more before it slides) plus a 64 KiB meta-block, its output and about 1 MiB of fixed tables.

    Not included: higher-quality encoding (context modeling, block splitting, static dictionary references), and the non-RFC large-window format.

    #License

    Apache-2.0. The static dictionary in src/static_data.mbt comes from google/brotli under the MIT license; see LICENSE-dictionary.

    BrotliError

    pub suberror BrotliError {
    BrotliError(BrotliErrorKind, String)
    } derive(
    Debug
    )

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

    BrotliErrorKind

    pub(all) enum BrotliErrorKind {
    Truncated
    Corrupt
    TrailingData
    WindowTooLarge
    OutputLimitExceeded
    } derive(Eq,
    Debug
    )

    Why a stream was refused.

    BrotliErrorKind::equal

    BrotliErrorKind::not_equal

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

    Decoder

    pub struct Decoder {
    // private fields
    }

    A streaming Brotli decoder (RFC 7932). Feed it input in chunks of any size with step and collect output in slices of any size; it keeps the sliding window (at most 16 MiB, and no more than the output so far) plus 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 stream whose window exceeds 1 << max_window_bits bytes (the window memory it would need); the default 24 accepts every RFC 7932 stream. Values are clamped to 10..24.

    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 is released.

    Decoder::step

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

    Decodes from input into output; see last_consumed and last_produced for how much of each it used. Pass end=true once input holds the last bytes of the stream, so that a stream cut short raises Truncated instead of asking for more. Bytes after the end of the stream raise TrailingData. 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 BrotliError

    step into a whole FixedArray, with bulk copies.

    EncodeAction

    pub(all) enum EncodeAction {
    Continue
    Flush
    Finish
    } derive(Eq,
    Debug
    )

    What Encoder::step should do once it has taken all of its input.

    EncodeAction::equal

    EncodeAction::not_equal

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

    Encoder

    pub struct Encoder {
    // private fields
    }

    A streaming Brotli encoder (RFC 7932): greedy LZ77 with one prefix code per category, about the ratio of gzip. Feed it input in chunks of any size with step and collect output in slices of any size. It keeps the window of past input (1 << window_bits bytes, and no more than the input so far) plus at most a meta-block (64 KiB) of input and its output, and fixed-size tables.

    Encoder::Encoder

    fn Encoder::Encoder(window_bits? : Int) -> Encoder

    An encoder at the start of a stream, with a window of 1 << window_bits bytes (clamped to 10..24). The default 22 is the reference encoder's; a decoder needs up to that much memory.

    Encoder::is_finished

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

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

    Encoder::last_consumed

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

    Input bytes the latest step took.

    Encoder::last_produced

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

    Output bytes the latest step wrote.

    Encoder::reset

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

    Starts a new stream, keeping the tables' memory. The input buffer is released.

    Encoder::step

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

    Compresses from input into output; see last_consumed and last_produced for how much of each it used. Input that was not taken (when the output is full) must be passed again, with the same action.

    action applies once all of input is taken: Flush makes the output so far decode to the input so far, Finish ends the stream. Both stay in effect while the step answers NeedMoreOutput. Status: NeedMoreInput once everything is taken and its output handed out (a Flush is then complete), NeedMoreOutput while output waits, Done once the stream has ended and been handed out; after that, steps take no input and answer Done.

    Encoder::step_into

    fn Encoder::step_into(self : Encoder, input : BytesView, output : FixedArray[Byte], action? : EncodeAction) -> Status

    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

    compress

    fn compress(input : BytesView, window_bits? : Int) -> Bytes

    Compresses input into a whole Brotli stream. window_bits is as for Encoder; the window declared is no larger than input needs (and at least 64 KiB), so that decoders reserve no more memory than necessary.

    decompress

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

    Decodes a whole Brotli stream held in memory. 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