Docs
What it’s for
snappy is Snappy written in Mojo: the raw block format Parquet and Avro compress with, and the sNaPpY framing format with a CRC-32C per chunk. parquet reads and writes SNAPPY pages with it and avro uses it for the snappy codec. Unlike the other codecs it has no C library and no shim, so there is nothing to build or load.
Install
A new project needs Modular’s channel, pixi’s build preview and the compiler before the tin:
pixi init app -c https://conda.modular.com/max -c conda-forge && cd app
sed -i.bak 's/^\[workspace\]/[workspace]\npreview = ["pixi-build"]/' pixi.toml
pixi add "mojo-compiler==1.2.0"
pixi shelf add snappy-mojo # no shelf dependencies, no C library
Quick start
Compress a page as a raw block and as a framed stream, and decompress both:
from snappy import (
compress, compress_framed, crc32c, decompress, decompress_framed,
uncompressed_length,
)
def main() raises:
var page = List[UInt8]()
for i in range(100_000):
page.append(UInt8(i % 7))
# Raw block, as in Parquet and Avro: the length leads the block.
var packed = compress(Span(page))
print("raw:", len(packed), "bytes for", uncompressed_length(Span(packed)))
print(decompress(Span(packed)) == page)
# Framing format: sNaPpY stream, CRC-32C checked per 64 KiB chunk.
var stream = compress_framed(Span(page))
print("framed:", len(stream), "bytes,", decompress_framed(Span(stream)) == page)
print(hex(crc32c("123456789".as_bytes()))) # 0xe3069283
API
Everything is exported from snappy.
Raw blocks, which begin with the decoded length as a varint:
compress(data: Span[UInt8, _]) -> List[UInt8]: one block; it never fails.decompress(data: Span[UInt8, _]) raises -> List[UInt8]: one block, sized from its own header.decompress_into[origin: Origin[mut=True], //](data: Span[UInt8, _], dst: Span[UInt8, origin]) raises -> Int: into a buffer of at leastuncompressed_length(data)bytes; returns the bytes written.uncompressed_length(data: Span[UInt8, _]) raises -> Int: the decoded length, read from the header without decompressing.max_compressed_length(n: Int) -> Int: the worst case,32 + n + n / 6, as in the C++ reference.validate(data: Span[UInt8, _]) -> Bool: whetherdatadecompresses without error; it decodes the whole block to find out.
Framing format:
compress_framed(data: Span[UInt8, _]) -> List[UInt8]anddecompress_framed(data: Span[UInt8, _]) raises -> List[UInt8]: a whole stream, in 64 KiB chunks, every checksum verified.FramedWriter():write(mut self, data: Span[UInt8, _])as often as you like, thenfinish(deinit self) -> List[UInt8].FramedReader(data: Span[UInt8, _]):read_chunk(mut self) raises -> Optional[List[UInt8]]returns each chunk’s bytes, then None.
CRC-32C: crc32c(data: Span[UInt8, _], seed: UInt32 = 0) -> UInt32, the incremental Crc32c with update and finish, and the framing format’s mask_crc32c and unmask_crc32c.
Errors and limits
- Corrupt input raises
snappy: <what>:truncated varint,invalid copy offset,length mismatch,destination buffer too small. The framing reader addssnappy: crc32c mismatch,bad stream identifierandunsupported unskippable chunk type; padding and skippable chunks are passed over. - The compressor is a single-pass greedy matcher with no lazy matching. Its element encoding follows the C++ reference, but its output isn’t guaranteed to match libsnappy’s byte for byte.
FramedReadercopies its whole input when you construct it; it is incremental on the output side only.
Tests
pixi run test checks the CRC-32C check value, the spec’s edge cases (long literals, overlapping copies), 64 KiB boundaries, 1 MiB round trips, corrupt input and bad checksums, and decodes a block produced by python-snappy and baked in as a constant. CI runs it on Linux and macOS with Mojo 1.2.0 and nightly.
The full reference is the README on GitHub.