can1357/oh-my-pi · error · ArchiveError

Invalid ${label} Huffman table: oversubscribed codes

Error message

Invalid ${label} Huffman table: oversubscribed codes

What it means

CanonicalHuffman.build() validates a Huffman code-length table read from an LZH/LHA archive stream before constructing the canonical decoding tree. The first code at a given bit length (code = previous*2) plus the number of symbols assigned that length exceeds the total number of distinct codes possible at that length (2^length), meaning the table is mathematically inconsistent and no valid canonical Huffman code can exist. The library throws ArchiveError to reject a corrupt or hostile archive instead of building a broken tree.

Source

Thrown at packages/utils/src/ar/lzh.ts:81

	static build(lengths: Uint8Array, symbolCount: number, label: string): CanonicalHuffman {
		const counts = new Uint32Array(17);
		let maximumLength = 0;
		for (let symbol = 0; symbol < symbolCount; symbol++) {
			const length = lengths[symbol]!;
			if (length > 16) throw new ArchiveError(`Invalid ${label} Huffman table: code is too long`);
			if (length !== 0) {
				counts[length]++;
				maximumLength = Math.max(maximumLength, length);
			}
		}
		if (maximumLength === 0) throw new ArchiveError(`Invalid ${label} Huffman table: no symbols`);

		const nextCodes = new Uint32Array(17);
		let code = 0;
		for (let length = 1; length <= 16; length++) {
			code = (code + counts[length - 1]!) * 2;
			if (code + counts[length]! > 2 ** length) {
				throw new ArchiveError(`Invalid ${label} Huffman table: oversubscribed codes`);
			}
			nextCodes[length] = code;
		}
		if (nextCodes[maximumLength]! + counts[maximumLength]! !== 2 ** maximumLength) {
			throw new ArchiveError(`Invalid ${label} Huffman table: incomplete codes`);
		}

		const tree = new CanonicalHuffman(label);
		for (let symbol = 0; symbol < symbolCount; symbol++) {
			const length = lengths[symbol]!;
			if (length === 0) continue;
			const symbolCode = nextCodes[length]!;
			nextCodes[length] = symbolCode + 1;
			let node = 0;
			for (let bitIndex = length - 1; bitIndex >= 0; bitIndex--) {
				if (tree.#symbol[node]! >= 0) {
					throw new ArchiveError(`Invalid ${label} Huffman table: prefix collision`);
				}

View on GitHub (pinned to 9690622007)

Solutions

  1. Verify the archive with its checksum/unlha -t or re-extract from a known-good source
  2. Check that the compressed data starts at the correct offset (method id LH5/LH6/LH7 + header size), not shifted by header misparse
  3. If producing archives with your own encoder, recompute canonical code lengths from actual symbol frequencies and ensure Kraft sum equals 1
  4. Catch ArchiveError and surface 'archive is corrupted' to the user rather than retrying

Example fix

// before: trusting raw lengths read from a custom encoder
lengths = encodeFrequencies(freqs); // may be oversubscribed
// after: build a proper canonical length table
const { lengths } = packageLengths(limitedLengthHuffman(freqs, 16)); // ensures sum(counts[l]/2^l) <= 1
Defensive patterns

Strategy: try-catch

Validate before calling

// No pre-call API exists: table bytes come from the archive stream.
// Pre-validate the file before decompressing:
const stat = await Bun.file(archivePath).stat?.();
if (!stat || stat.size < 24) throw new Error('archive too small to be valid LZH');

Type guard

function isLzhMethod(method: string): boolean {
  return method === '-lh0-' || method === '-lh5-' || method === '-lh6-' || method === '-lh7-';
}

Try / catch

try {
  const out = decompressLhStatic(compressed, originalSize);
} catch (err) {
  if (err instanceof ArchiveError) {
    throw new Error(`LZH archive table is corrupt: ${err.message}`);
  }
  throw err;
}

Prevention

When it happens

Trigger: Decompressing an LH5/LH6/LH7-format stream (decompressLhStatic / readTemporaryTree / readPositionTree) whose header-encoded code-length counts are corrupted, truncated mid-table, or hand-crafted so counts[length] overshoots 2^length for some length 1..16.

Common situations: Corrupted .lzh/.lha files (bad download, disk damage), archives concatenated or byte-shifted so the bit reader parses garbage lengths, fuzz-crafted inputs, or writing a custom LH-compressor that emits invalid length distributions.

Related errors


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