radlang — Compression (compress)

The compress namespace provides gzip and zlib(deflate) stream compression, plus ZIP archive create/extract. It is a compiler-intrinsic namespace — available everywhere with no import — backed by a vendored, statically-linked miniz (no external zlib dependency).

For language fundamentals see the Overview. The HTTP server uses this same codec to gzip responses automatically; see HTTP.


Overview

MethodReturnsDescription
compress.gzip(string\|byte[] input [, map meta])byte[]Compress to a gzip stream (RFC 1952)
compress.gunzip(byte[] data)string | byte[]Decompress a gzip stream
compress.deflate(string\|byte[] input)byte[]Compress to a zlib stream (RFC 1950)
compress.inflate(byte[] data)string | byte[]Decompress a zlib stream
compress.gzipInfo(byte[] data)stringRead a gzip stream’s metadata as JSON
compress.unzip(string\|byte[] src, string destDir)intExtract a ZIP archive to a directory; returns file count
compress.zip(string destPath, string[] files)intCreate a ZIP archive from files; returns count added

gzip/gunzip produce and consume the gzip container (magic header + CRC-32 + size trailer) that HTTP labels Content-Encoding: gzip. deflate/inflate use the zlib container HTTP labels Content-Encoding: deflate.


Text and binary (input + output)

Compression accepts either a string or a byte[] — the same method handles both:

byte[] a = compress.gzip("some text")        // from a string
byte[] b = compress.gzip(fs.readBytesSync("photo.raw"))   // from bytes

Decompression’s output type is chosen by the declaration target — declare the result string for text, or byte[] for binary-safe bytes:

string text = compress.gunzip(a)   // text (NUL-terminated)
byte[] raw  = compress.gunzip(b)   // raw bytes, safe for binary payloads

Prefer the byte[] form for any payload that may contain NUL bytes (images, audio, serialized binary) — the string form is meant for text and stops at the first NUL.

Round-trip a string:

fn main() {
    string original = "the quick brown fox jumps over the lazy dog"
    byte[] packed = compress.gzip(original)
    string restored = compress.gunzip(packed)
    sys.output(restored)   // the quick brown fox jumps over the lazy dog
}

Persist a compressed payload with the fs byte APIs:

fn main() {
    byte[] packed = compress.gzip(report)
    fs.writeBytesSync("report.json.gz", packed)

    byte[] onDisk = fs.readBytesSync("report.json.gz")
    string report = compress.gunzip(onDisk)
}

deflate / inflate behave the same over the zlib container.


gzip metadata

By default gzip writes a minimal, deterministic header (no timestamp, no filename) so the same input always yields the same bytes — useful for caching and ETags. Pass an optional metadata map to embed a filename, modification time, or comment in the gzip header:

byte[] gz = compress.gzip(body, {
    "name": "report.json",
    "mtime": "1730400000",   // unix seconds
    "comment": "nightly export"
})

Read a gzip stream’s metadata back as a JSON object with gzipInfo (missing fields come back as "" / 0):

string info = compress.gzipInfo(gz)
// {"name":"report.json","mtime":1730400000,"comment":"nightly export"}

// Parse it into a struct you define, or navigate it dynamically with json.*
type GzipMeta { string name, long mtime, string comment }
GzipMeta m = json.parse<GzipMeta>(info)
sys.output(m.name)   // report.json

deflate (zlib) has no metadata slot, so there’s nothing to set there.


ZIP archives

Create a ZIP from a list of file paths (each stored under its basename), and extract an archive to a directory. Both zip and unzip return the number of files processed.

fn main() {
    int added = compress.zip("bundle.zip", ["a.txt", "assets/logo.png"])
    // bundle.zip now contains "a.txt" and "logo.png"

    int files = compress.unzip("bundle.zip", "out/")
    sys.output(files)   // 2
}

unzip accepts the archive as a path or as byte[], so you can extract one you already hold in memory (e.g. a download):

byte[] archive = http.getSync("https://example.com/pkg.zip").body   // (bytes)
int files = compress.unzip(archive, "pkg/")

Extraction is hardened against zip-slip: entries with absolute paths or .. components are skipped rather than written outside destDir. Parent directories are created as needed.


Notes

  • Compression level is the balanced default (zlib level 6); it is not currently configurable.
  • Decompression is size-safe: a malformed stream yields an empty result rather than crashing.
  • HTTP is automatic. You rarely need compress for web responses — the server gzips eligible bodies and the client transparently inflates them. Reach for this namespace to compress files, cache entries, message payloads, or to pack/unpack archives outside the HTTP path.
  • ZIP scope. The archive API is intentionally minimal (extract-all and create-from-paths). In-memory per-entry read/listing and richer writing are a planned enhancement.