can1357/oh-my-pi · error · ArchiveError

Invalid ${label} Huffman table: prefix collision

Error message

Invalid ${label} Huffman table: prefix collision

What it means

While inserting each symbol's code path into the binary decoding tree, build() finds an interior node already occupied by a symbol (tree.#symbol[node] >= 0) — meaning a previously inserted code is a prefix of the current one. This indicates the length table, though internally consistent by the oversubscription check, does not match canonical assignment invariants (corrupt stream or encoding bug), so decoding would be ambiguous.

Source

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

			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`);
				}
				const bit = (symbolCode >>> bitIndex) & 1;
				let child = bit === 0 ? tree.#zero[node]! : tree.#one[node]!;
				if (child < 0) {
					child = tree.#symbol.length;
					tree.#zero.push(-1);
					tree.#one.push(-1);
					tree.#symbol.push(-1);
					if (bit === 0) tree.#zero[node] = child;
					else tree.#one[node] = child;
				}
				node = child;
			}
			if (tree.#symbol[node]! >= 0 || tree.#zero[node]! >= 0 || tree.#one[node]! >= 0) {
				throw new ArchiveError(`Invalid ${label} Huffman table: duplicate code`);
			}
			tree.#symbol[node] = symbol;
		}

View on GitHub (pinned to 9690622007)

Solutions

  1. Validate the archive externally (unlha -t / CRC check) and re-obtain a clean copy
  2. Ensure the table section is being read from the right bit offset (verify method id and header-skip logic before decompressLhStatic)
  3. If writing an encoder, always assign lengths in canonical symbol order from a standard Huffman construction
  4. Catch ArchiveError; do not retry decoding corrupted bytes
Defensive patterns

Strategy: try-catch

Try / catch

try {
  const out = decompressLhStatic(data, size);
} catch (err) {
  if (err instanceof ArchiveError) return { ok: false, reason: 'corrupt-huffman-table' };
  throw err;
}

Prevention

When it happens

Trigger: Decompressing LZH data whose command/position/temporary table lengths were corrupted after passing the count checks (e.g. bit-rot altering lengths without breaking the Kraft sum), or a custom encoder emitting non-canonical length assignments.

Common situations: Damaged archives, hand-edited or fuzzed table sections, mixing compressed blocks from different variants.

Related errors


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