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.
Dependencies
moon add gaato/deflate| Package | Format | Use it for |
|---|---|---|
| gaato/deflate | raw DEFLATE | ZIP entries, WebSocket permessage-deflate, formats that frame DEFLATE themselves |
| gaato/deflate/zlib | zlib | HTTP's deflate coding, PNG, Discord's zlib-stream |
| gaato/deflate/gzip | gzip | .gz files, HTTP's gzip coding |
///|
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")
}///|
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")
}///|
/// 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])
}
}///|
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)
}///|
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")
}///|
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",
)
}///|
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")
}///|
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())
}///|
/// 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)
}| File | Size | here -1 | zlib -1 | here -6 | zlib -6 | here -9 | zlib -9 |
|---|---|---|---|---|---|---|---|
| dickens | 10,192,446 | 4,217,525 | 4,585,612 | 3,864,185 | 3,871,628 | 3,816,762 | 3,854,729 |
| mozilla | 51,220,480 | 20,192,961 | 20,577,220 | 18,807,653 | 19,089,118 | 18,716,681 | 19,044,390 |
| nci | 33,553,445 | 3,837,577 | 4,624,591 | 3,102,963 | 3,200,182 | 2,935,668 | 2,987,995 |
| ooffice | 6,152,192 | 3,275,124 | 3,290,526 | 3,073,999 | 3,097,288 | 3,068,726 | 3,092,920 |
| samba | 21,606,400 | 5,866,517 | 6,329,449 | 5,338,862 | 5,451,399 | 5,272,299 | 5,402,704 |
| xml | 5,345,280 | 887,708 | 965,242 | 686,486 | 687,991 | 651,148 | 658,899 |
| total | 128,070,243 | 38,277,412 | 40,372,640 | 34,874,148 | 35,397,606 | 34,461,284 | 35,041,637 |
moon add gaato/deflate| Package | Format | Use it for |
|---|---|---|
| gaato/deflate | raw DEFLATE | ZIP entries, WebSocket permessage-deflate, formats that frame DEFLATE themselves |
| gaato/deflate/zlib | zlib | HTTP's deflate coding, PNG, Discord's zlib-stream |
| gaato/deflate/gzip | gzip | .gz files, HTTP's gzip coding |
///|
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")
}///|
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")
}///|
/// 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])
}
}///|
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)
}///|
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")
}///|
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",
)
}///|
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")
}///|
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())
}///|
/// 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)
}| File | Size | here -1 | zlib -1 | here -6 | zlib -6 | here -9 | zlib -9 |
|---|---|---|---|---|---|---|---|
| dickens | 10,192,446 | 4,217,525 | 4,585,612 | 3,864,185 | 3,871,628 | 3,816,762 | 3,854,729 |
| mozilla | 51,220,480 | 20,192,961 | 20,577,220 | 18,807,653 | 19,089,118 | 18,716,681 | 19,044,390 |
| nci | 33,553,445 | 3,837,577 | 4,624,591 | 3,102,963 | 3,200,182 | 2,935,668 | 2,987,995 |
| ooffice | 6,152,192 | 3,275,124 | 3,290,526 | 3,073,999 | 3,097,288 | 3,068,726 | 3,092,920 |
| samba | 21,606,400 | 5,866,517 | 6,329,449 | 5,338,862 | 5,451,399 | 5,272,299 | 5,402,704 |
| xml | 5,345,280 | 887,708 | 965,242 | 686,486 | 687,991 | 651,148 | 658,899 |
| total | 128,070,243 | 38,277,412 | 40,372,640 | 34,874,148 | 35,397,606 | 34,461,284 | 35,041,637 |
pub struct Decoder {
// private fields
}fn Decoder::step(self : Decoder, input : BytesView, output : MutArrayView[Byte], end~ : Bool) -> Status raise CodecErrorfn Decoder::step_into(self : Decoder, input : BytesView, output : FixedArray[Byte], end~ : Bool) -> Status raise CodecErrorpub struct Encoder {
// private fields
}fn step(self : Encoder, input : BytesView, output : MutArrayView[Byte], action~ : EncodeAction) -> Status raise CodecErrorfn Encoder::step(self : Encoder, input : BytesView, output : MutArrayView[Byte], action~ : EncodeAction) -> Statusfn Encoder::step_into(self : Encoder, input : BytesView, output : FixedArray[Byte], action~ : EncodeAction) -> Statusfn compress(input : BytesView, level? : Int, dictionary? : BytesView) -> BytesInstall
Download zipStreaming 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.
Dependencies