Guide

Custom formats

defineCompression takes the metadata and two functions. Register the result and compress and decompress and the tools see it

Two functions and some metadata

Every built-in comes out of defineCompression, and yours can too. Hand it info and a decompress, and a compress if you write the format as well. What comes back is a Compression, the same shape as gzip's family:

ts
interface Compression {
  readonly name: string;
  info(): CompressionInfo;
  compress(input: string | Uint8Array, options?: CompressOptions): Uint8Array;
  decompress(data: Uint8Array, options?: DecompressOptions): { bytes: Uint8Array; details: Details };
}

Run-length, because a puzzle printed counts and bytes

ts
import { compress, decompress, defineCompression, register } from "@agntn/compressions";

const rle = defineCompression({
  info: {
    name: "rle",
    label: "Run-length",
    description: "Each run of one byte as its length and the byte",
    standard: "none",
    containers: [{ name: "rle", label: "Run-length", standard: "none", extensions: [".rle"] }],
    compress: true,
    options: [],
  },
  compress(bytes) {
    const out: number[] = [];
    for (let i = 0, run = 1; i < bytes.length; i += run, run = 1) {
      while (run < 255 && bytes[i + run] === bytes[i]) run++;
      out.push(run, bytes[i]!);
    }
    return Uint8Array.from(out);
  },
  decompress(data, out) {
    if (data.length % 2) throw out.fail("a run needs its length and its byte");
    for (let i = 0; i < data.length; i += 2) out.copyByte(data[i + 1]!, data[i]!);
    return {};
  },
});

register(rle);
compress("rle", "aaaabbb");                     // Uint8Array [4, 97, 3, 98]
decompress("rle", compress("rle", "aaaabbb")).bytes;  // the bytes of "aaaabbb"

What comes free

decompress writes into out, an Output that holds the caller's limit. It has push(byte), append(chunk), copyByte(byte, count) for runs and copy(distance, count) for LZ77 matches, overlap included. Each one checks the limit before it writes, so a bomb in your format stops where gzip's would.

out.fail(message, offset) builds a DecompressError that carries everything written so far. Throw it, and partial: true works on your format the same way it works on the built-ins.

Options get checked before your function runs: unknown names, numbers out of min and max, values outside choices, an option scoped to another container. container always arrives filled in, with the first container as the default. A format without compress throws a clear CompressionError when someone tries to write it.

Where it shows up

register puts it in the registry, replacing any format with the same name. From then on compress, decompress, create, formats and formatInfos see it, and so do the four tools and the CLI in the same process. identify doesn't: it only tries containers it knows how to recognize, and a format of your own has no probe there.