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.4.1
    License
    Apache-2.0
    Last updated
    4 hours ago
    Downloads
    421

    Dependencies

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

    Decoder and Encoder implement Decode and Encode from gaato/codec_bytes, the contract the codecs of codecs.mbt share, so code written once against it drives them. Errors are gaato/codec's CodecError(kind, message), the same for every codec there; gaato/codec_bytes re-exports it, so that one import names it.

    Brotli has no framing, so an input holds one stream. A decoder made with until=EndOfMember stops right after the stream and leaves what follows unread:

    ///|
    test "stop at the end of the stream" {
    let input = b"\x0b\x02\x80hello\x03" + b"what follows"
    let decoder = @brotli.Decoder(until=EndOfMember)
    let out = FixedArray::make(64, b'\x00')
    let status = decoder.step_into(input, out, end=false)
    assert_eq(status, Done)
    assert_eq(Bytes::from_array(out[:decoder.last_produced()]), b"hello")
    assert_eq(input[decoder.last_consumed():], b"what follows")
    }

    #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, under the rules of gaato/codec_bytes.
    • Memory is the window (at most 16 MiB, and no more than the output so far) plus tables of at most 1.3 MiB; memory_usage() reports it. Decoder(max_window_bits=n) refuses streams that need a window over 2^n - 16 bytes with LimitExceeded, saying how much the stream needs.
    • Every malformed stream raises CodecError(kind, message) with Truncated, Corrupt or TrailingData (bytes after the stream); decompress(max_output=n) raises LimitExceeded past n bytes. It does not abort. 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.
    • 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.mbtx).
    • 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.
    • The rules the decoder applies to what it reads (the window, the meta-block lengths, block switches, distances and the inverse move-to-front), where dictionary words are written, and the canonical codes the encoder writes are proved against RFC 7932 with moon prove; PROOFS.md lists what is proved and what is trusted.

    Not included: higher-quality encoding (context modeling, block splitting, static dictionary references), and Shared Brotli (RFC 9841): its shared dictionaries, its large window and its framing format. A large-window stream raises Corrupt.

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

    Decoder and Encoder implement Decode and Encode from gaato/codec_bytes, the contract the codecs of codecs.mbt share, so code written once against it drives them. Errors are gaato/codec's CodecError(kind, message), the same for every codec there; gaato/codec_bytes re-exports it, so that one import names it.

    Brotli has no framing, so an input holds one stream. A decoder made with until=EndOfMember stops right after the stream and leaves what follows unread:

    ///|
    test "stop at the end of the stream" {
    let input = b"\x0b\x02\x80hello\x03" + b"what follows"
    let decoder = @brotli.Decoder(until=EndOfMember)
    let out = FixedArray::make(64, b'\x00')
    let status = decoder.step_into(input, out, end=false)
    assert_eq(status, Done)
    assert_eq(Bytes::from_array(out[:decoder.last_produced()]), b"hello")
    assert_eq(input[decoder.last_consumed():], b"what follows")
    }

    #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, under the rules of gaato/codec_bytes.
    • Memory is the window (at most 16 MiB, and no more than the output so far) plus tables of at most 1.3 MiB; memory_usage() reports it. Decoder(max_window_bits=n) refuses streams that need a window over 2^n - 16 bytes with LimitExceeded, saying how much the stream needs.
    • Every malformed stream raises CodecError(kind, message) with Truncated, Corrupt or TrailingData (bytes after the stream); decompress(max_output=n) raises LimitExceeded past n bytes. It does not abort. 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.
    • 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.mbtx).
    • 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.
    • The rules the decoder applies to what it reads (the window, the meta-block lengths, block switches, distances and the inverse move-to-front), where dictionary words are written, and the canonical codes the encoder writes are proved against RFC 7932 with moon prove; PROOFS.md lists what is proved and what is trusted.

    Not included: higher-quality encoding (context modeling, block splitting, static dictionary references), and Shared Brotli (RFC 9841): its shared dictionaries, its large window and its framing format. A large-window stream raises Corrupt.

    #License

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

    EncodeAction

    Status and EncodeAction are gaato/codec_bytes's: a step answers Done, NeedMoreInput or NeedMoreOutput, and an encoder's step is told to Continue, Flush or Finish. Errors are gaato/codec's CodecError(kind, message).

    Status

    using @gaato/codec_bytes { type Status }

    Status and EncodeAction are gaato/codec_bytes's: a step answers Done, NeedMoreInput or NeedMoreOutput, and an encoder's step is told to Continue, Flush or Finish. Errors are gaato/codec's CodecError(kind, message).

    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.
    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 a stream whose window exceeds what max_window_bits allows: LimitExceeded with the Memory resource, where needed is the stream's window and limit the largest one allowed, both (1 << WBITS) - 16 bytes (RFC 7932 section 9.1). The default 24 accepts every RFC 7932 stream. Values are clamped to 10..24.

    Brotli has no framing: an input holds one stream. With until at its default EndOfInput, bytes after the stream raise TrailingData; with EndOfMember, step answers Done right after the stream's last meta-block and consumes nothing past it, so input[last_consumed():] is what follows the stream.

    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 the decoder holds in buffers sized by the stream: the window, and the prefix code tables of the largest meta-block so far. At most 1 << max_window_bits plus 1,343,596 bytes of tables (see RESOURCE_BOUNDS.md). reset releases the window and keeps the tables.

    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
    CodecError

    Decodes from input into output; see last_consumed and last_produced for how much of each it used. end says whether input holds the last bytes there are: then a stream cut short raises Truncated instead of asking for more. With EndOfInput, Done comes once the stream has ended, end is true and every byte is consumed; bytes after the end of the stream raise TrailingData. With EndOfMember, Done comes once the stream has ended, and the bytes after it are left unconsumed.

    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.

    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.
    impl Encode for Encoder

    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::memory_usage

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

    Bytes the encoder holds in buffers that grow with the stream: the input buffer (the window and at most a meta-block not encoded yet) and the output waiting for the caller. At most (1 << window_bits) - 16, plus max(1 << (window_bits - 2), 64 KiB), plus 64 KiB, plus 131,200 bytes (see RESOURCE_BOUNDS.md). reset releases the input buffer.

    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: Continue only takes input, Flush compresses everything buffered and pads to a byte boundary, so that the output so far decodes to the input so far, and 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.

    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
    CodecError

    Decodes a whole Brotli stream held in memory. max_output caps the decoded size (unlimited by default): past it, decompress raises LimitExceeded with the Output resource and max_output as the limit. max_window_bits is as for Decoder.

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io