Sign in

    deflate

    Streaming DEFLATE, zlib and gzip decoders and encoders (RFC 1951, 1950, 1952) for MoonBit: arbitrary input chunks, bounded output slices, checksums verified, gzip headers and members, zlib preset dictionaries, levels 0 to 9 and sync flush.

    deflate
    inflate
    zlib
    gzip
    decompression
    compression
    rfc1951
    rfc1950
    rfc1952
    streaming
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    2 hours ago
    Downloads
    36

    Dependencies

    #gaato/deflate

    Streaming decoders and encoders for DEFLATE (RFC 1951) and the two formats around it, zlib (RFC 1950) and gzip (RFC 1952), in MoonBit. Native, JS, Wasm and Wasm GC.

    moon add gaato/deflate

    Three packages, one per format; import those you need in your moon.pkg:

    PackageFormatUse it for
    gaato/deflateraw DEFLATEZIP entries, WebSocket permessage-deflate, formats that frame DEFLATE themselves
    gaato/deflate/zlibzlibHTTP's deflate coding, PNG, Discord's zlib-stream
    gaato/deflate/gzipgzip.gz files, HTTP's gzip coding

    One shot:

    ///|
    test "compress and decompress a whole stream of each format" {
    let text = b"hello, hello, hello"
    assert_eq(@deflate.decompress(@deflate.compress(text)), text)
    assert_eq(@zlib.decompress(@zlib.compress(text, level=9)), text)
    assert_eq(@gzip.decompress(@gzip.compress(text, level=1)), text)
    }

    ///|
    test "decompress a whole stream of each format" {
    let raw = b"\xcb\x48\xcd\xc9\xc9\x07\x00"
    assert_eq(@deflate.decompress(raw), b"hello")
    let zlib = b"\x78\x9c\xcb\x48\xcd\xc9\xc9\x07\x00\x06\x2c\x02\x15"
    assert_eq(@zlib.decompress(zlib, max_output=16 * 1024 * 1024), b"hello")
    let gzip = b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\x03\xcb\x48\xcd\xc9\xc9\x07\x00\x86\xa6\x10\x36\x05\x00\x00\x00"
    assert_eq(@gzip.decompress(gzip), b"hello")
    }

    Streaming: feed input as it arrives, take output in slices of the size you choose. Every step says whether its input is the last: end=false before, end=true with the last input, so that a stream cut short raises Truncated instead of ending quietly. Status is gaato/codec_bytes's, re-exported by each package: match Done, NeedMoreInput and NeedMoreOutput unqualified, or write @gzip.Status::Done.

    ///|
    fn gunzip(chunks : Array[Bytes], sink : (BytesView) -> Unit) -> Unit raise {
    let decoder = @gzip.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 gzip stream chunk by chunk" {
    let decoded = Buffer()
    gunzip(
    [
    b"\x1f\x8b\x08\x00\x00\x00\x00\x00", b"\x02\x03\xcb\x48\xcd\xc9\xc9\x07", b"\x00\x86\xa6\x10\x36\x05\x00\x00\x00",
    ],
    bytes => decoded.write_bytes(bytes.to_owned()),
    )
    assert_eq(decoded.to_bytes(), b"hello")
    }

    Encoding works the same way, with an action instead of end: Continue takes input, Flush makes the output so far decode to the input so far while the stream goes on, and Finish ends the stream. EncodeAction is re-exported too.

    ///|
    /// Compresses each message of a connection into one zlib stream, as
    /// Discord's `zlib-stream` does: every message ends in a sync flush. Returns
    /// the bytes to send for each message.
    fn compress_messages(messages : Array[Bytes]) -> Array[Bytes] {
    let encoder = @zlib.Encoder()
    let out = FixedArray::make(64 * 1024, b'\x00')
    messages.map(message => {
    let part = Buffer()
    let mut input = message[:]
    for ;; {
    let status = encoder.step_into(input, out, action=Flush)
    input = input[encoder.last_consumed():]
    part.write_bytes(Bytes::from_array(out[:encoder.last_produced()]))
    if status is NeedMoreInput {
    break
    }
    }
    part.to_bytes()
    })
    }

    ///|
    test "each message decodes on its own, without the end of the stream" {
    let messages = [b"{\"op\":10}", b"{\"op\":11}"]
    let decoder = @zlib.Decoder()
    let out = FixedArray::make(64 * 1024, b'\x00')
    for i, part in compress_messages(messages) {
    assert_true(part[part.length() - 4:] is [0x00, 0x00, 0xff, 0xff])
    assert_true(decoder.step_into(part, out, end=false) is NeedMoreInput)
    assert_eq(Bytes::from_array(out[:decoder.last_produced()]), messages[i])
    }
    }

    level goes from 0 to 9, as zlib's and the gzip command's do: 0 stores the input in stored blocks, 1 is the fastest, 9 the smallest, and 6, the default, a balance; levels outside are clamped. Flush is zlib's sync flush (Z_SYNC_FLUSH): it ends with an empty stored block, 00 00 ff ff, and a flush with nothing new since the last adds nothing. A flush ends the current block and costs at least the 5 bytes of the empty one, so flush where a reader needs the data, not after every write; matches still reach back across it. The output does not depend on how the input and the output are split, only on the input, the options and where the flushes fall.

    Each Decoder implements gaato/codec_bytes's Decode, and each Encoder its Encode, so code written against that contract (an HTTP client, a command-line tool) takes any of them as it takes the other codecs of codecs.mbt.

    #gzip

    A gzip stream may hold several members, as cat a.gz b.gz makes, and nothing marks the last one. By default the decoder reads every member, as the gzip command does. After a member, bytes that begin another (1f 8b) are read as one; anything else, zero padding included, raises TrailingData. A decoder that skipped zeros would also skip the members after them.

    header() gives the current member's header: the extra field, the name and the comment as the bytes hold them (RFC 1952 writes names in ISO 8859-1; they are not converted), and the time, flags and OS. until=EndOfMember stops right after the first member and consumes nothing past it, so that you can decide yourself what may follow:

    ///|
    test "one member, then what follows it" {
    let gz = b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\x03\xcb\x48\xcd\xc9\xc9\x07\x00\x86\xa6\x10\x36\x05\x00\x00\x00"
    let input = Bytes::from_array([..gz, 0, 0, 0, 0])
    let decoder = @gzip.Decoder(until=EndOfMember)
    let out = FixedArray::make(64, b'\x00')
    assert_true(decoder.step_into(input, out, end=true) is Done)
    assert_eq(Bytes::from_array(out[:decoder.last_produced()]), b"hello")
    assert_eq(input[decoder.last_consumed():], b"\x00\x00\x00\x00")
    let header = decoder.header().unwrap()
    assert_eq(header.os, b'\x03')
    assert_true(header.name is None)
    }

    A header's extra field, name and comment may hold at most max_header_bytes between them, 65,536 by default; a header that holds more raises LimitExceeded({ resource: Memory, .. }), rather than being kept whole or cut short.

    A decoder that has no use for them, as an HTTP client usually has none, passes keep_header=false: it skips the extra field, the name and the comment whatever their size, holds none of their bytes, and still checks the header CRC. header() then gives the time, flags and OS, with the other fields None, and max_header_bytes does not apply. @gzip.decompress skips them this way, so a large header never stops it.

    ///|
    test "skip the header's fields" {
    // A member named "a.txt" (FNAME).
    let gz = b"\x1f\x8b\x08\x08\x00\x00\x00\x00\x00\x03a.txt\x00\xcb\x48\xcd\xc9\xc9\x07\x00\x86\xa6\x10\x36\x05\x00\x00\x00"
    let decoder = @gzip.Decoder(keep_header=false)
    let out = FixedArray::make(64, b'\x00')
    assert_true(decoder.step_into(gz, out, end=true) is Done)
    assert_eq(Bytes::from_array(out[:decoder.last_produced()]), b"hello")
    assert_true(decoder.header() is Some({ name: None, os: b'\x03', .. }))
    assert_eq(@gzip.decompress(gz), b"hello")
    }

    @gzip.Encoder and @gzip.compress write one member. Without a header, it has no name, comment or extra field, MTIME 0 and OS 255 (both unknown), so that the same input gives the same bytes on every target and every day, and XFL as zlib sets it: 4 at levels 0 and 1, 2 at level 9, 0 otherwise. A Header you give is written as it is, but it must be writable (Header::is_writable): an extra field of at most 65,535 bytes, and no zero byte in the name or the comment, which a zero ends. Passing one that is not is a programming error, and aborts.

    ///|
    test "a member with a name" {
    let header : @gzip.Header = {
    text: true,
    mtime: 1700000000,
    xfl: b'\x00',
    os: b'\x03',
    extra: None,
    name: Some(b"notes.txt"),
    comment: None,
    }
    assert_true(header.is_writable())
    let gz = @gzip.compress(b"hello", header~)
    let decoder = @gzip.Decoder()
    let out = FixedArray::make(64, b'\x00')
    assert_true(decoder.step_into(gz, out, end=true) is Done)
    assert_eq(decoder.header(), Some(header))
    // The default header: MTIME 0, XFL 0 at level 6, OS 255.
    assert_eq(
    @gzip.compress(b"hello")[:10],
    b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff",
    )
    }

    #zlib

    A zlib stream that was compressed with a preset dictionary names it by its Adler-32, which @zlib.dictionary_id computes. Without it the decoder raises DictionaryRequired(Some(id)), and with another one DictionaryMismatch; make a new decoder with the right dictionary and start over. A stream that names no dictionary ignores the one you give. A decoder keeps a view of the dictionary's last 32 KiB, not a copy, so the Bytes it is a view of stay alive as long as the decoder; the same goes for @deflate.Decoder(dictionary=...).

    ///|
    test "a stream that needs a dictionary" {
    let dictionary = b"hello, dictionary"
    // zlib.deflateSync("hello", { dictionary }) in Node.
    let stream = b"\x78\xbb\x39\x4e\x06\x97\xcb\x00\x09\x00\x00\x06\x2c\x02\x15"
    let needed = try @zlib.decompress(stream) catch {
    CodecError(DictionaryRequired(id), _) => id
    _ => None
    } noraise {
    _ => None
    }
    assert_eq(needed, Some(@zlib.dictionary_id(dictionary)))
    assert_eq(@zlib.decompress(stream, dictionary~), b"hello")
    }

    An encoder given a dictionary names it in the header, and its stream's matches may reach back into it; the same goes for @deflate.Encoder(dictionary=...), whose stream does not name it. Its header's FLEVEL says which kind of level made it, as zlib's does, so @zlib.compress writes the same header bytes as zlib: 78 9c at the default level.

    ///|
    test "compress with a dictionary" {
    let dictionary = b"{\"op\":0,\"t\":\"MESSAGE_CREATE\",\"d\":{\"content\":\""
    let message = b"{\"op\":0,\"t\":\"MESSAGE_CREATE\",\"d\":{\"content\":\"hi\"}}"
    let stream = @zlib.compress(message, dictionary~)
    assert_eq(@zlib.decompress(stream, dictionary~), message)
    assert_true(stream.length() < @zlib.compress(message).length())
    }

    @zlib.is_header(cmf, flg) tells a zlib header from the start of raw DEFLATE, for HTTP's deflate coding, which is zlib but which some servers send raw. It is a heuristic, though a strong one.

    #A stream that does not end

    Discord's zlib-stream gateway compression is one zlib stream per connection that never ends; each message ends in a sync flush (00 00 ff ff), and a message may come in several WebSocket frames. Keep one decoder for the connection, made with until=EndOfMember, and pass every frame with end=false, since no frame is the last:

    • NeedMoreInput, with the frame consumed, is the answer to every frame. Everything the frame determines has been written, so after a frame that ends in 00 00 ff ff the whole message is out. The decoder does not know where messages end; the suffix does.
    • NeedMoreOutput: the slice is full. Call again with the rest of the frame, frame[last_consumed():].
    • Done: the stream ended, which a gateway stream never does. until=EndOfMember makes the decoder report this at once; under the default, it would wait for what follows and raise TrailingData on the next frame.

    When the connection closes, drop the decoder or reset it for the next one, rather than passing end=true, which raises Truncated: the stream was cut, not ended. However long the connection, the decoder holds its window and tables, never the messages so far.

    ///|
    /// Inflates the frames of one connection, and returns its messages.
    fn inflate_messages(frames : Array[Bytes]) -> Array[Bytes] raise {
    let decoder = @zlib.Decoder(until=EndOfMember)
    let out = FixedArray::make(64 * 1024, b'\x00')
    let message = Buffer()
    let messages = []
    for frame in frames {
    let mut input = frame[:]
    for ;; {
    let status = decoder.step_into(input, out, end=false)
    input = input[decoder.last_consumed():]
    message.write_bytes(Bytes::from_array(out[:decoder.last_produced()]))
    match status {
    NeedMoreInput => break
    NeedMoreOutput => ()
    Done => fail("the stream ended")
    }
    }
    if frame is [.., 0x00, 0x00, 0xff, 0xff] {
    messages.push(message.to_bytes())
    message.reset()
    }
    }
    messages
    }

    ///|
    test "a message split across frames comes out once its flush arrives" {
    let messages = [b"{\"op\":10}", b"{\"op\":0,\"t\":\"READY\"}"]
    let parts = compress_messages(messages)
    // The second message in two frames.
    let second = parts[1]
    let frames = [parts[0], second[:3].to_owned(), second[3:].to_owned()]
    assert_eq(inflate_messages(frames), messages)
    }

    #What it does

    • Decodes every DEFLATE block (stored, fixed and dynamic Huffman codes) with a 32 KiB window, and preset dictionaries for raw DEFLATE and zlib. It refuses what zlib refuses, with zlib's kind of error: reserved block types and codes, over-subscribed and incomplete codes, distances before the start of the output, and the reserved header fields and values of zlib and gzip. Crafted streams and thousands of corrupted ones are checked against zlib's answers.
    • Checks every checksum: zlib's Adler-32, gzip's CRC-32 and length, and the header CRC when a gzip header has one. Output comes out as soon as it is decoded, before the checksum that covers it, so ChecksumMismatch can follow output you already have; decompress returns only checked output.
    • Never reads past the end of a stream: after Done under until=EndOfMember, what follows is input[last_consumed():], whatever the input splits. Raw DEFLATE and zlib streams cannot be concatenated, so under the default until=EndOfInput anything after one raises TrailingData.
    • 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 and the decoding tables, 46,592 bytes with a dictionary or without (a dictionary is your bytes, held by reference), plus a gzip header's fields; memory_usage() reports it.
    • A step that raises first writes the output that comes before the error, and last_consumed() and last_produced() say what it took and wrote, so a stream with bytes after it gives all its output, then TrailingData, with last_consumed() at its end: whether it came in one step or many. A malformed stream gives the same output before the same kind of error however its input and output are split.
    • Every malformed stream raises gaato/codec's CodecError(kind, message), with the kind Truncated, Corrupt, ChecksumMismatch, TrailingData, DictionaryRequired, DictionaryMismatch or LimitExceeded (a gzip header over max_header_bytes, with resource: Memory; more than max_output from decompress, with resource: Output). An error inside the DEFLATE data of a zlib or gzip stream keeps its kind. 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.
    • Encodes with libdeflate's levels: 0 stores the input; 1 finds matches in a hash table; 2 to 4 take the longest match at each position, and 5 to 9 look one or two positions ahead for a better one, over hash chains. Each block takes the fixed Huffman code, a dynamic one or no code (stored), whichever is smallest, and ends where the data changes (libdeflate's block splitting) or after 64 KiB. Inputs of a few dozen bytes or less are stored, as libdeflate stores them. With the same input, no dictionary and no flush, the streams are the bytes that libdeflate 1.24 writes when its blocks are limited to 64 KiB. They decode with zlib and the gzip command, whole and cut after every flush (scripts/crosscheck.mbtx, run in CI), and the same input gives the same bytes on every target, however it is split. An encoder never raises.
    • An encoder's memory is fixed by its level: 542,516 bytes at the default level (the 32 KiB window, a 64 KiB block and what the parser looks ahead, the block's output, the match tables and the block's sequences), 361,948 at level 1 and 131,077 at level 0, with a dictionary or without, and for gzip its header's bytes; memory_usage() reports it.
    • The zlib and gzip header checks, Adler-32's sums (which never overflow), the decoder's verdict on a code's lengths, and the canonical codes the encoder writes are proved against RFC 1950, RFC 1951 and RFC 1952 with moon prove; PROOFS.md lists what is proved and what is trusted.

    #Compression ratio

    Sizes in bytes of raw DEFLATE streams for six files of the Silesia corpus, against zlib 1.3.2.1 at the same levels:

    FileSizehere -1zlib -1here -6zlib -6here -9zlib -9
    dickens10,192,4464,217,5254,585,6123,864,1853,871,6283,816,7623,854,729
    mozilla51,220,48020,192,96120,577,22018,807,65319,089,11818,716,68119,044,390
    nci33,553,4453,837,5774,624,5913,102,9633,200,1822,935,6682,987,995
    ooffice6,152,1923,275,1243,290,5263,073,9993,097,2883,068,7263,092,920
    samba21,606,4005,866,5176,329,4495,338,8625,451,3995,272,2995,402,704
    xml5,345,280887,708965,242686,486687,991651,148658,899
    total128,070,24338,277,41240,372,64034,874,14835,397,60634,461,28435,041,637

    Level 1 compresses about 5 % more than zlib's level 1, and levels 6 and 9 about 1.5 % more than zlib's: every file comes out smaller, at level 6 by 0.2 % (dickens, xml) to 3.0 % (nci). On the Canterbury corpus the eleven files come to 722,392 bytes at level 6, against zlib's 730,888. Random bytes go into stored blocks, 5 bytes more for each 65,535; a long run of one byte costs a few bytes for each 64 KiB block. moon run deflate/scripts/ratio.mbtx -- DIR measures the sizes and the speed on a directory of files.

    #Known limits

    • Levels stop at 9: libdeflate's levels 10 to 12, whose near-optimal parsing comes out about 3 % smaller than level 9 at several times the time, are not here, and higher levels are clamped to 9.
    • A flush is a sync flush: zlib's full flush, which also forgets the window, is not offered.

    #License

    Apache-2.0.

    #gaato/deflate

    Streaming decoders and encoders for DEFLATE (RFC 1951) and the two formats around it, zlib (RFC 1950) and gzip (RFC 1952), in MoonBit. Native, JS, Wasm and Wasm GC.

    moon add gaato/deflate

    Three packages, one per format; import those you need in your moon.pkg:

    PackageFormatUse it for
    gaato/deflateraw DEFLATEZIP entries, WebSocket permessage-deflate, formats that frame DEFLATE themselves
    gaato/deflate/zlibzlibHTTP's deflate coding, PNG, Discord's zlib-stream
    gaato/deflate/gzipgzip.gz files, HTTP's gzip coding

    One shot:

    ///|
    test "compress and decompress a whole stream of each format" {
    let text = b"hello, hello, hello"
    assert_eq(@deflate.decompress(@deflate.compress(text)), text)
    assert_eq(@zlib.decompress(@zlib.compress(text, level=9)), text)
    assert_eq(@gzip.decompress(@gzip.compress(text, level=1)), text)
    }

    ///|
    test "decompress a whole stream of each format" {
    let raw = b"\xcb\x48\xcd\xc9\xc9\x07\x00"
    assert_eq(@deflate.decompress(raw), b"hello")
    let zlib = b"\x78\x9c\xcb\x48\xcd\xc9\xc9\x07\x00\x06\x2c\x02\x15"
    assert_eq(@zlib.decompress(zlib, max_output=16 * 1024 * 1024), b"hello")
    let gzip = b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\x03\xcb\x48\xcd\xc9\xc9\x07\x00\x86\xa6\x10\x36\x05\x00\x00\x00"
    assert_eq(@gzip.decompress(gzip), b"hello")
    }

    Streaming: feed input as it arrives, take output in slices of the size you choose. Every step says whether its input is the last: end=false before, end=true with the last input, so that a stream cut short raises Truncated instead of ending quietly. Status is gaato/codec_bytes's, re-exported by each package: match Done, NeedMoreInput and NeedMoreOutput unqualified, or write @gzip.Status::Done.

    ///|
    fn gunzip(chunks : Array[Bytes], sink : (BytesView) -> Unit) -> Unit raise {
    let decoder = @gzip.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 gzip stream chunk by chunk" {
    let decoded = Buffer()
    gunzip(
    [
    b"\x1f\x8b\x08\x00\x00\x00\x00\x00", b"\x02\x03\xcb\x48\xcd\xc9\xc9\x07", b"\x00\x86\xa6\x10\x36\x05\x00\x00\x00",
    ],
    bytes => decoded.write_bytes(bytes.to_owned()),
    )
    assert_eq(decoded.to_bytes(), b"hello")
    }

    Encoding works the same way, with an action instead of end: Continue takes input, Flush makes the output so far decode to the input so far while the stream goes on, and Finish ends the stream. EncodeAction is re-exported too.

    ///|
    /// Compresses each message of a connection into one zlib stream, as
    /// Discord's `zlib-stream` does: every message ends in a sync flush. Returns
    /// the bytes to send for each message.
    fn compress_messages(messages : Array[Bytes]) -> Array[Bytes] {
    let encoder = @zlib.Encoder()
    let out = FixedArray::make(64 * 1024, b'\x00')
    messages.map(message => {
    let part = Buffer()
    let mut input = message[:]
    for ;; {
    let status = encoder.step_into(input, out, action=Flush)
    input = input[encoder.last_consumed():]
    part.write_bytes(Bytes::from_array(out[:encoder.last_produced()]))
    if status is NeedMoreInput {
    break
    }
    }
    part.to_bytes()
    })
    }

    ///|
    test "each message decodes on its own, without the end of the stream" {
    let messages = [b"{\"op\":10}", b"{\"op\":11}"]
    let decoder = @zlib.Decoder()
    let out = FixedArray::make(64 * 1024, b'\x00')
    for i, part in compress_messages(messages) {
    assert_true(part[part.length() - 4:] is [0x00, 0x00, 0xff, 0xff])
    assert_true(decoder.step_into(part, out, end=false) is NeedMoreInput)
    assert_eq(Bytes::from_array(out[:decoder.last_produced()]), messages[i])
    }
    }

    level goes from 0 to 9, as zlib's and the gzip command's do: 0 stores the input in stored blocks, 1 is the fastest, 9 the smallest, and 6, the default, a balance; levels outside are clamped. Flush is zlib's sync flush (Z_SYNC_FLUSH): it ends with an empty stored block, 00 00 ff ff, and a flush with nothing new since the last adds nothing. A flush ends the current block and costs at least the 5 bytes of the empty one, so flush where a reader needs the data, not after every write; matches still reach back across it. The output does not depend on how the input and the output are split, only on the input, the options and where the flushes fall.

    Each Decoder implements gaato/codec_bytes's Decode, and each Encoder its Encode, so code written against that contract (an HTTP client, a command-line tool) takes any of them as it takes the other codecs of codecs.mbt.

    #gzip

    A gzip stream may hold several members, as cat a.gz b.gz makes, and nothing marks the last one. By default the decoder reads every member, as the gzip command does. After a member, bytes that begin another (1f 8b) are read as one; anything else, zero padding included, raises TrailingData. A decoder that skipped zeros would also skip the members after them.

    header() gives the current member's header: the extra field, the name and the comment as the bytes hold them (RFC 1952 writes names in ISO 8859-1; they are not converted), and the time, flags and OS. until=EndOfMember stops right after the first member and consumes nothing past it, so that you can decide yourself what may follow:

    ///|
    test "one member, then what follows it" {
    let gz = b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\x03\xcb\x48\xcd\xc9\xc9\x07\x00\x86\xa6\x10\x36\x05\x00\x00\x00"
    let input = Bytes::from_array([..gz, 0, 0, 0, 0])
    let decoder = @gzip.Decoder(until=EndOfMember)
    let out = FixedArray::make(64, b'\x00')
    assert_true(decoder.step_into(input, out, end=true) is Done)
    assert_eq(Bytes::from_array(out[:decoder.last_produced()]), b"hello")
    assert_eq(input[decoder.last_consumed():], b"\x00\x00\x00\x00")
    let header = decoder.header().unwrap()
    assert_eq(header.os, b'\x03')
    assert_true(header.name is None)
    }

    A header's extra field, name and comment may hold at most max_header_bytes between them, 65,536 by default; a header that holds more raises LimitExceeded({ resource: Memory, .. }), rather than being kept whole or cut short.

    A decoder that has no use for them, as an HTTP client usually has none, passes keep_header=false: it skips the extra field, the name and the comment whatever their size, holds none of their bytes, and still checks the header CRC. header() then gives the time, flags and OS, with the other fields None, and max_header_bytes does not apply. @gzip.decompress skips them this way, so a large header never stops it.

    ///|
    test "skip the header's fields" {
    // A member named "a.txt" (FNAME).
    let gz = b"\x1f\x8b\x08\x08\x00\x00\x00\x00\x00\x03a.txt\x00\xcb\x48\xcd\xc9\xc9\x07\x00\x86\xa6\x10\x36\x05\x00\x00\x00"
    let decoder = @gzip.Decoder(keep_header=false)
    let out = FixedArray::make(64, b'\x00')
    assert_true(decoder.step_into(gz, out, end=true) is Done)
    assert_eq(Bytes::from_array(out[:decoder.last_produced()]), b"hello")
    assert_true(decoder.header() is Some({ name: None, os: b'\x03', .. }))
    assert_eq(@gzip.decompress(gz), b"hello")
    }

    @gzip.Encoder and @gzip.compress write one member. Without a header, it has no name, comment or extra field, MTIME 0 and OS 255 (both unknown), so that the same input gives the same bytes on every target and every day, and XFL as zlib sets it: 4 at levels 0 and 1, 2 at level 9, 0 otherwise. A Header you give is written as it is, but it must be writable (Header::is_writable): an extra field of at most 65,535 bytes, and no zero byte in the name or the comment, which a zero ends. Passing one that is not is a programming error, and aborts.

    ///|
    test "a member with a name" {
    let header : @gzip.Header = {
    text: true,
    mtime: 1700000000,
    xfl: b'\x00',
    os: b'\x03',
    extra: None,
    name: Some(b"notes.txt"),
    comment: None,
    }
    assert_true(header.is_writable())
    let gz = @gzip.compress(b"hello", header~)
    let decoder = @gzip.Decoder()
    let out = FixedArray::make(64, b'\x00')
    assert_true(decoder.step_into(gz, out, end=true) is Done)
    assert_eq(decoder.header(), Some(header))
    // The default header: MTIME 0, XFL 0 at level 6, OS 255.
    assert_eq(
    @gzip.compress(b"hello")[:10],
    b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff",
    )
    }

    #zlib

    A zlib stream that was compressed with a preset dictionary names it by its Adler-32, which @zlib.dictionary_id computes. Without it the decoder raises DictionaryRequired(Some(id)), and with another one DictionaryMismatch; make a new decoder with the right dictionary and start over. A stream that names no dictionary ignores the one you give. A decoder keeps a view of the dictionary's last 32 KiB, not a copy, so the Bytes it is a view of stay alive as long as the decoder; the same goes for @deflate.Decoder(dictionary=...).

    ///|
    test "a stream that needs a dictionary" {
    let dictionary = b"hello, dictionary"
    // zlib.deflateSync("hello", { dictionary }) in Node.
    let stream = b"\x78\xbb\x39\x4e\x06\x97\xcb\x00\x09\x00\x00\x06\x2c\x02\x15"
    let needed = try @zlib.decompress(stream) catch {
    CodecError(DictionaryRequired(id), _) => id
    _ => None
    } noraise {
    _ => None
    }
    assert_eq(needed, Some(@zlib.dictionary_id(dictionary)))
    assert_eq(@zlib.decompress(stream, dictionary~), b"hello")
    }

    An encoder given a dictionary names it in the header, and its stream's matches may reach back into it; the same goes for @deflate.Encoder(dictionary=...), whose stream does not name it. Its header's FLEVEL says which kind of level made it, as zlib's does, so @zlib.compress writes the same header bytes as zlib: 78 9c at the default level.

    ///|
    test "compress with a dictionary" {
    let dictionary = b"{\"op\":0,\"t\":\"MESSAGE_CREATE\",\"d\":{\"content\":\""
    let message = b"{\"op\":0,\"t\":\"MESSAGE_CREATE\",\"d\":{\"content\":\"hi\"}}"
    let stream = @zlib.compress(message, dictionary~)
    assert_eq(@zlib.decompress(stream, dictionary~), message)
    assert_true(stream.length() < @zlib.compress(message).length())
    }

    @zlib.is_header(cmf, flg) tells a zlib header from the start of raw DEFLATE, for HTTP's deflate coding, which is zlib but which some servers send raw. It is a heuristic, though a strong one.

    #A stream that does not end

    Discord's zlib-stream gateway compression is one zlib stream per connection that never ends; each message ends in a sync flush (00 00 ff ff), and a message may come in several WebSocket frames. Keep one decoder for the connection, made with until=EndOfMember, and pass every frame with end=false, since no frame is the last:

    • NeedMoreInput, with the frame consumed, is the answer to every frame. Everything the frame determines has been written, so after a frame that ends in 00 00 ff ff the whole message is out. The decoder does not know where messages end; the suffix does.
    • NeedMoreOutput: the slice is full. Call again with the rest of the frame, frame[last_consumed():].
    • Done: the stream ended, which a gateway stream never does. until=EndOfMember makes the decoder report this at once; under the default, it would wait for what follows and raise TrailingData on the next frame.

    When the connection closes, drop the decoder or reset it for the next one, rather than passing end=true, which raises Truncated: the stream was cut, not ended. However long the connection, the decoder holds its window and tables, never the messages so far.

    ///|
    /// Inflates the frames of one connection, and returns its messages.
    fn inflate_messages(frames : Array[Bytes]) -> Array[Bytes] raise {
    let decoder = @zlib.Decoder(until=EndOfMember)
    let out = FixedArray::make(64 * 1024, b'\x00')
    let message = Buffer()
    let messages = []
    for frame in frames {
    let mut input = frame[:]
    for ;; {
    let status = decoder.step_into(input, out, end=false)
    input = input[decoder.last_consumed():]
    message.write_bytes(Bytes::from_array(out[:decoder.last_produced()]))
    match status {
    NeedMoreInput => break
    NeedMoreOutput => ()
    Done => fail("the stream ended")
    }
    }
    if frame is [.., 0x00, 0x00, 0xff, 0xff] {
    messages.push(message.to_bytes())
    message.reset()
    }
    }
    messages
    }

    ///|
    test "a message split across frames comes out once its flush arrives" {
    let messages = [b"{\"op\":10}", b"{\"op\":0,\"t\":\"READY\"}"]
    let parts = compress_messages(messages)
    // The second message in two frames.
    let second = parts[1]
    let frames = [parts[0], second[:3].to_owned(), second[3:].to_owned()]
    assert_eq(inflate_messages(frames), messages)
    }

    #What it does

    • Decodes every DEFLATE block (stored, fixed and dynamic Huffman codes) with a 32 KiB window, and preset dictionaries for raw DEFLATE and zlib. It refuses what zlib refuses, with zlib's kind of error: reserved block types and codes, over-subscribed and incomplete codes, distances before the start of the output, and the reserved header fields and values of zlib and gzip. Crafted streams and thousands of corrupted ones are checked against zlib's answers.
    • Checks every checksum: zlib's Adler-32, gzip's CRC-32 and length, and the header CRC when a gzip header has one. Output comes out as soon as it is decoded, before the checksum that covers it, so ChecksumMismatch can follow output you already have; decompress returns only checked output.
    • Never reads past the end of a stream: after Done under until=EndOfMember, what follows is input[last_consumed():], whatever the input splits. Raw DEFLATE and zlib streams cannot be concatenated, so under the default until=EndOfInput anything after one raises TrailingData.
    • 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 and the decoding tables, 46,592 bytes with a dictionary or without (a dictionary is your bytes, held by reference), plus a gzip header's fields; memory_usage() reports it.
    • A step that raises first writes the output that comes before the error, and last_consumed() and last_produced() say what it took and wrote, so a stream with bytes after it gives all its output, then TrailingData, with last_consumed() at its end: whether it came in one step or many. A malformed stream gives the same output before the same kind of error however its input and output are split.
    • Every malformed stream raises gaato/codec's CodecError(kind, message), with the kind Truncated, Corrupt, ChecksumMismatch, TrailingData, DictionaryRequired, DictionaryMismatch or LimitExceeded (a gzip header over max_header_bytes, with resource: Memory; more than max_output from decompress, with resource: Output). An error inside the DEFLATE data of a zlib or gzip stream keeps its kind. 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.
    • Encodes with libdeflate's levels: 0 stores the input; 1 finds matches in a hash table; 2 to 4 take the longest match at each position, and 5 to 9 look one or two positions ahead for a better one, over hash chains. Each block takes the fixed Huffman code, a dynamic one or no code (stored), whichever is smallest, and ends where the data changes (libdeflate's block splitting) or after 64 KiB. Inputs of a few dozen bytes or less are stored, as libdeflate stores them. With the same input, no dictionary and no flush, the streams are the bytes that libdeflate 1.24 writes when its blocks are limited to 64 KiB. They decode with zlib and the gzip command, whole and cut after every flush (scripts/crosscheck.mbtx, run in CI), and the same input gives the same bytes on every target, however it is split. An encoder never raises.
    • An encoder's memory is fixed by its level: 542,516 bytes at the default level (the 32 KiB window, a 64 KiB block and what the parser looks ahead, the block's output, the match tables and the block's sequences), 361,948 at level 1 and 131,077 at level 0, with a dictionary or without, and for gzip its header's bytes; memory_usage() reports it.
    • The zlib and gzip header checks, Adler-32's sums (which never overflow), the decoder's verdict on a code's lengths, and the canonical codes the encoder writes are proved against RFC 1950, RFC 1951 and RFC 1952 with moon prove; PROOFS.md lists what is proved and what is trusted.

    #Compression ratio

    Sizes in bytes of raw DEFLATE streams for six files of the Silesia corpus, against zlib 1.3.2.1 at the same levels:

    FileSizehere -1zlib -1here -6zlib -6here -9zlib -9
    dickens10,192,4464,217,5254,585,6123,864,1853,871,6283,816,7623,854,729
    mozilla51,220,48020,192,96120,577,22018,807,65319,089,11818,716,68119,044,390
    nci33,553,4453,837,5774,624,5913,102,9633,200,1822,935,6682,987,995
    ooffice6,152,1923,275,1243,290,5263,073,9993,097,2883,068,7263,092,920
    samba21,606,4005,866,5176,329,4495,338,8625,451,3995,272,2995,402,704
    xml5,345,280887,708965,242686,486687,991651,148658,899
    total128,070,24338,277,41240,372,64034,874,14835,397,60634,461,28435,041,637

    Level 1 compresses about 5 % more than zlib's level 1, and levels 6 and 9 about 1.5 % more than zlib's: every file comes out smaller, at level 6 by 0.2 % (dickens, xml) to 3.0 % (nci). On the Canterbury corpus the eleven files come to 722,392 bytes at level 6, against zlib's 730,888. Random bytes go into stored blocks, 5 bytes more for each 65,535; a long run of one byte costs a few bytes for each 64 KiB block. moon run deflate/scripts/ratio.mbtx -- DIR measures the sizes and the speed on a directory of files.

    #Known limits

    • Levels stop at 9: libdeflate's levels 10 to 12, whose near-optimal parsing comes out about 3 % smaller than level 9 at several times the time, are not here, and higher levels are clamped to 9.
    • A flush is a sync flush: zlib's full flush, which also forgets the window, is not offered.

    #License

    Apache-2.0.

    EncodeAction

    Outcome of one Decoder::step or Encoder::step: Done, NeedMoreInput or NeedMoreOutput, and what an encoder's step is told to do: Continue, Flush or Finish, as every gaato/codec_bytes codec answers and is told.

    Status

    using @gaato/codec_bytes { type Status }

    Outcome of one Decoder::step or Encoder::step: Done, NeedMoreInput or NeedMoreOutput, and what an encoder's step is told to do: Continue, Flush or Finish, as every gaato/codec_bytes codec answers and is told.

    Decoder

    pub struct Decoder {
    // private fields
    }

    A streaming raw DEFLATE decoder (RFC 1951). Feed it input in chunks of any size with step and collect output in slices of any size; it holds the 32 KiB window and the Huffman tables of the current block, never the whole input or output.
    impl Decode for Decoder

    Decoder::Decoder

    fn Decoder::Decoder(dictionary? : BytesView, until? :
    Until
    ) -> Decoder

    A decoder at the start of a stream.

    dictionary is a preset dictionary: the stream's matches may reach back into its last 32 KiB as if it had been decoded just before the stream. A zlib stream names the one it needs; raw DEFLATE leaves it to the caller. The decoder keeps a view of the dictionary's last 32 KiB, not a copy, so the bytes the view belongs to stay alive as long as the decoder.

    until says where the stream ends. EndOfInput, the default, decodes the stream up to the end of the input, and bytes after its final block raise TrailingData. EndOfMember stops right after the final block and consumes nothing past it, so that input[last_consumed():] is what follows: a format that embeds DEFLATE (zlib, gzip) reads its trailer from there.

    Decoder::is_finished

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

    Whether the stream has ended: its final block is decoded and all its output has been handed out. Under EndOfInput this can be true before Done, which waits for end.

    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 for the stream: the 32 KiB window, the Huffman tables of a block and the scratch space they are built with, and the bytes kept aside for a unit cut short. All of it is allocated when the decoder is made, so this is the same from the first step to the last, with a dictionary or without (see RESOURCE_BOUNDS.md). A dictionary is the caller's bytes, kept by reference, and not counted.

    Decoder::reset

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

    Starts a new stream with the same options: the same dictionary, and the same until.

    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.

    Pass end=true once input holds the last bytes and end=false before: a stream cut short then raises Truncated. Under until=EndOfInput a stream whose final block has ended answers NeedMoreInput until end, and bytes after it raise TrailingData; under until=EndOfMember it answers Done right after the final block, whatever end says. 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 raw DEFLATE encoder (RFC 1951). Feed it input in chunks of any size with step and collect output in slices of any size; it holds the 32 KiB window, a block of input and its output, and fixed-size match tables, never the whole input or output.
    impl Encode for Encoder

    Encoder::Encoder

    fn Encoder::Encoder(level? : Int, dictionary? : BytesView) -> Encoder

    An encoder at the start of a stream.

    level trades speed for size as zlib's levels do: 0 writes the input in stored blocks, 1 is the fastest compression, 9 the smallest, and 6, the default, a balance. Levels outside 0 to 9 are clamped. The levels are libdeflate's.

    dictionary is a preset dictionary: the stream's matches may reach back into its last 32 KiB as if it had come just before the stream, and the decoder must be given the same one. The encoder keeps a view of the dictionary's last 32 KiB, not a copy, so the bytes the view belongs to stay alive as long as the encoder.

    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 held for the stream, the same from the first step to the last: the input buffer (the 32 KiB window, a block of up to 64 KiB and the 6 KiB the parser looks ahead), the output of a block, the match finder's tables and the block's sequences, counts and codes. 542,516 bytes at levels 5 to 9, 542,172 at 2 to 4, 361,948 at 1 and 131,077 at 0, with a dictionary or without. A dictionary is the caller's bytes, kept by reference, and not counted.

    Encoder::reset

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

    Starts a new stream with the same options: the same level and the same dictionary.

    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 is a sync flush, as zlib's Z_SYNC_FLUSH: it compresses everything taken and ends with an empty stored block (00 00 ff ff), so that the output so far decodes to the input so far, and the stream goes on; Finish ends the stream. A flush with nothing new since the last one adds nothing. 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.

    compress

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

    Compresses input into a whole raw DEFLATE stream, at level (0 to 9, clamped; 6 by default) and with the preset dictionary if given, as Encoder does.

    decompress

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

    Decodes a whole raw DEFLATE stream held in memory. max_output caps the decoded size (unlimited by default) and raises LimitExceeded({ resource: Output, .. }) past it; bytes after the final block raise TrailingData.

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io