zstd

tape
import zstd from "zstd";

let packed: string = fs.read_file("level1.pak.zst") or { return 1; };
var data: collections.Vec_u8 = zstd.decompress(packed, 256 << 20) or {
    io.println("not good zstd: {err}");
    return 1;
};
// data.slice() is what the .zst file holds

zstd reads Zstandard (RFC 8878): .zst files, asset packs, Content-Encoding: zstd bodies, and the supercompressed levels of KTX2 textures (which texture decodes with it). Every frame the RFC defines decodes:

  • raw, RLE and compressed blocks, any window size;
  • Huffman-coded literals, FSE-coded sequences, repeated tables and trees;
  • the content checksum, checked whenever a frame has one;
  • several frames one after the other, and skippable frames (skipped).

Decompression is strict: anything that isn’t valid zstd is an error, including bytes left over after the last frame. A frame made with a dictionary is refused with Unsupported. zstd decompresses only; it has no encoder. It is a pure module, separate from compress and brotli.

Errors

tape
pub enum ZstdError : u8 {
    BadData = 1: "not valid zstd data";
    Truncated = 2: "zstd data ends early";
    TooLarge = 3: "decompressed data over the limit";
    Checksum = 4: "zstd content checksum does not match";
    Unsupported = 5: "a zstd frame that needs a dictionary";
}
  • BadData: the bytes are not zstd. Causes include a broken table or Huffman tree, a match reaching before the start of the data, a frame that produces a different size than its header says, or bytes after the end.
  • Truncated: the data ends inside a frame (or is empty).
  • TooLarge: the output would pass the limit you gave.
  • Checksum: a frame decoded, but its content checksum differs: the data was damaged.
  • Unsupported: the frame needs a dictionary.

Functions

decompress

tape
pub fn decompress(data: []const u8, max_out: i64) -> collections.Vec_u8 or ZstdError

Decompresses every frame in data into one byte vector. max_out is the most output you accept: a small input that would expand to gigabytes stops with TooLarge at that size, and the vector never grows past it. Traps when max_out is negative.

decompress_into

tape
pub fn decompress_into(data: []const u8, out: []u8) -> i64 or ZstdError

Decompresses into your buffer and returns how many bytes it wrote. Use it when you know the size, as a texture’s level index tells it. Output that does not fit is TooLarge.

tape
var pixels: [65536]u8;
let n: i64 = zstd.decompress_into(level_bytes, pixels[0..65536]) or { return 1; };

content_size

tape
pub fn content_size(data: []const u8) -> ?i64

The size the first frame says it decompresses to, read from its header without decoding anything; null when the header doesn’t say (a frame written as a stream) or data is not zstd. A frame can lie: the size is checked when you decompress.

tape
let n: i64 = zstd.content_size(packed) or { 1 << 20 };

Memory and speed

Each call allocates about 150 KiB for its tables and one block’s literals, and frees it before returning; decompress also allocates its result, at the frame’s content size when the header gives one. Natively on an x64 desktop, 4 MB of text, JSON and binary records decompress in about 10 ms.

Last modified: