can1357/oh-my-pi · error · ArchiveError

Invalid ${label} Huffman table: incomplete codes

Error message

Invalid ${label} Huffman table: incomplete codes

What it means

After the length loop, CanonicalHuffman.build() requires the canonical codes to exactly fill the code space: firstCode[maximumLength] + counts[maximumLength] === 2^maximumLength. If the table is under-subscribed (unused code space remains), the tree would contain paths that decode to nothing, so the library rejects it. This catches archives whose declared code lengths do not form a complete Huffman code.

Source

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

			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`);
				}
				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);

View on GitHub (pinned to 9690622007)

Solutions

  1. Re-extract or re-download the archive; compare file size and CRC
  2. Confirm you are using the matching decompress routine for the archive's method byte (LH5 vs LH6 vs LH7 differ in table sizes)
  3. If you are the encoder, emit lengths for every nonzero-frequency symbol so the code space is exactly filled
  4. Catch ArchiveError and report corruption instead of attempting partial decode

Example fix

// before: emitting only symbols actually used, leaving space unfilled
writeLengths(freqs.filter(f => f > 0));
// after: use a complete-code canonical assignment
writeLengths(canonicalCompleteLengths(freqs, 16)); // guarantees Kraft sum == 1
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate the archive externally before decoding:
// unlha t archive.lzh  (exit 0 => structurally sound)

Try / catch

try {
  const out = decompressLhStatic(data, size);
} catch (err) {
  if (err instanceof ArchiveError && err.message.includes('incomplete codes')) {
    throw new Error('archive code-length table is incomplete; file is corrupt');
  }
  throw err;
}

Prevention

When it happens

Trigger: Decompressing an LZH stream where the encoded symbol-count/length distribution is short by symbols — e.g. a truncated table section, an encoder that wrote fewer lengths than it declared, or corruption zeroing out trailing lengths in readTemporaryTree/readCommandTree/readPositionTree.

Common situations: Bit-flipped or truncated .lzh downloads, archives produced by buggy third-party compressors, mixing LH-format variants (LH5 table parsed as LH7), or fuzzing.

Related errors


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