can1357/oh-my-pi · error · ArchiveError

Invalid CAB archive: invalid LZX Huffman code length

Error message

Invalid CAB archive: invalid LZX Huffman code length

What it means

LZX Huffman code lengths are at most 16 bits; the LzxHuffmanTable constructor validates every length read from the block header's code-length trees. A value above 16 can only come from a corrupt or hostile bitstream (valid encodings can't produce it via readCodeLengths deltas... but a direct call with a bad lengths array can), so the decoder refuses to build the table.

Source

Thrown at packages/utils/src/ar/codecs/lzx.ts:72

		const b0 = this.readByte();
		const b1 = this.readByte();
		const b2 = this.readByte();
		const b3 = this.readByte();
		return (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)) >>> 0;
	}
}

class LzxHuffmanTable {
	readonly #counts = new Uint32Array(17);
	readonly #firstCodes = new Uint32Array(17);
	readonly #firstSymbols = new Uint32Array(17);
	readonly #symbols: Uint16Array;
	readonly empty: boolean;

	constructor(lengths: Uint8Array, allowEmpty = false) {
		let symbolCount = 0;
		for (const length of lengths) {
			if (length > 16) throw new ArchiveError("Invalid CAB archive: invalid LZX Huffman code length");
			if (length !== 0) {
				this.#counts[length]++;
				symbolCount++;
			}
		}
		this.empty = symbolCount === 0;
		if (this.empty && !allowEmpty) {
			throw new ArchiveError("Invalid CAB archive: empty LZX Huffman tree");
		}

		let code = 0;
		let symbolOffset = 0;
		for (let length = 1; length <= 16; length++) {
			code = (code + this.#counts[length - 1]!) * 2;
			if (code + this.#counts[length]! > 2 ** length) {
				throw new ArchiveError("Invalid CAB archive: oversubscribed LZX Huffman tree");
			}
			this.#firstCodes[length] = code;

View on GitHub (pinned to 9690622007)

Solutions

  1. If calling LzxHuffmanTable yourself, clamp/validate lengths to 0..16 before construction.
  2. If decoding CABs normally, treat the archive as corrupt: validate with cabextract/7z and re-obtain the file.
  3. Check for stream desync — once a prior frame/block decode fails mid-way, abandon that decoder; create a fresh LzxDecoder per folder.
  4. Verify readCodeLengths inputs (first/last ranges) match the LZX spec (main tree split at 256, length tree of 249).

Example fix

// before
const table = new LzxHuffmanTable(rawLengths); // rawLengths may contain 17+
// after
const lengths = Uint8Array.from(rawLengths, (n) => n & 0x0f); // or validate explicitly
if (lengths.some((n) => n > 16)) throw new Error("bad lengths");
const table = new LzxHuffmanTable(lengths);
Defensive patterns

Strategy: validation

Validate before calling

if (Array.prototype.some.call(lengths, (n) => n > 16)) {
  throw new Error("Huffman code length exceeds 16 bits — stream is corrupt");
}

Type guard

function hasValidLzxLengths(lengths: Uint8Array): boolean {
  for (let i = 0; i < lengths.byteLength; i++) {
    if (lengths[i]! > 16) return false;
  }
  return true;
}

Try / catch

try {
  return decoder.decompressFrame(frameBytes, outputSize);
} catch (e) {
  if (e instanceof Error && e.message.includes("invalid LZX Huffman code length")) {
    throw new Error("Block header decoded impossible code lengths — abandon this decoder (state is desynced) and reject the archive");
  }
  throw e;
}

Prevention

When it happens

Trigger: Constructing LzxHuffmanTable directly (or via a corrupted readCodeLengths path) with a Uint8Array containing an entry > 16: feeding pre-tree 4-bit values into the wrong array, a caller-supplied lengths buffer that was never mod-17 reduced, or bit desync corrupting stored lengths.

Common situations: Custom CAB/LZX tooling that builds Huffman tables from unvalidated decoded lengths; fuzzed archives; reusing the decoder after a previous desync so subsequent block headers decode to garbage lengths.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/607a0042abdfbd74. Report an issue: GitHub.