can1357/oh-my-pi · error · ArchiveError

Invalid CAB archive: invalid LZX Huffman symbol

Error message

Invalid CAB archive: invalid LZX Huffman symbol

What it means

Thrown by LzxHuffmanTable.decode after consuming up to 16 bits without the accumulated code matching any entry in the tree. The bitstream contains a code prefix that no symbol was assigned, which cannot happen with a valid encoder — it indicates corrupt or misaligned data.

Source

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

		const next = this.#firstSymbols.slice();
		for (let symbol = 0; symbol < lengths.byteLength; symbol++) {
			const length = lengths[symbol]!;
			if (length !== 0) this.#symbols[next[length]!] = symbol;
			next[length]!++;
		}
	}

	decode(reader: LzxBitReader): number {
		if (this.empty) throw new ArchiveError("Invalid CAB archive: LZX stream uses an empty Huffman tree");
		let code = 0;
		for (let length = 1; length <= 16; length++) {
			code = code * 2 + reader.readBits(1);
			const relative = code - this.#firstCodes[length]!;
			if (relative >= 0 && relative < this.#counts[length]!) {
				return this.#symbols[this.#firstSymbols[length]! + relative]!;
			}
		}
		throw new ArchiveError("Invalid CAB archive: invalid LZX Huffman symbol");
	}
}

function readCodeLengths(reader: LzxBitReader, lengths: Uint8Array, first: number, last: number): void {
	const pretreeLengths = new Uint8Array(20);
	for (let index = 0; index < pretreeLengths.byteLength; index++) pretreeLengths[index] = reader.readBits(4);
	const pretree = new LzxHuffmanTable(pretreeLengths);
	let index = first;
	while (index < last) {
		const symbol = pretree.decode(reader);
		if (symbol === 17 || symbol === 18) {
			const run = reader.readBits(symbol === 17 ? 4 : 5) + (symbol === 17 ? 4 : 20);
			if (index + run > last) throw new ArchiveError("Invalid CAB archive: LZX code-length run exceeds its tree");
			lengths.fill(0, index, index + run);
			index += run;
			continue;
		}
		if (symbol === 19) {

View on GitHub (pinned to 9690622007)

Solutions

  1. Check that each decompressFrame call receives exactly one CFDATA block's compressed bytes, in order, with matching outputSize.
  2. Verify the CAB file's integrity and re-download.
  3. Recompress the archive with a conformant tool.
  4. Make sure no earlier error path left the decoder mid-frame; a fresh LzxDecoder must be created per folder and reused for the whole folder only.

Example fix

// before
for (const block of blocks) decoder.decompressFrame(block.compressed, block.uncompressedSize)
// after
let decoder: LzxDecoder | null = null
for (const block of blocks) {
  decoder ??= new LzxDecoder(folder.windowBits)
  decoder.decompressFrame(block.compressed, block.uncompressedSize) // same decoder per folder, exact block bytes
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure frame boundaries are exact before decoding:
if (!Number.isInteger(size) || size <= 0 || size > 32768) throw new Error('bad frame size')
if (bytes.byteLength === 0) throw new Error('empty frame data')

Try / catch

try {
  const frame = decoder.decompressFrame(blockData, blockOutSize)
} catch (err) {
  if (err instanceof ArchiveError && err.message.includes('invalid LZX Huffman symbol')) {
    throw new Error('CAB data is corrupt or frames were fed out of order')
  }
  throw err
}

Prevention

When it happens

Trigger: Decoding any LZX symbol (main, length, aligned, or pretree) when the reader is bit-desynchronized or the data is corrupt, so the next bits do not form a valid code prefix.

Common situations: Truncated or bit-rotted CAB downloads, wrong slice boundaries passed to decompressFrame (frames must be fed in order with exact CFDATA uncompressed-block sizes), decoding non-CAB LZX data with this CAB-specific decoder.

Related errors


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