can1357/oh-my-pi · error · ArchiveError

Invalid ${label} command Huffman table size

Error message

Invalid ${label} command Huffman table size

What it means

readCommandTree() reads a 9-bit encodedCount for the command table (510 symbols for LH5/LH6, larger spaces handled by the caller). A count above symbolCount cannot occur in a valid stream, so the library throws ArchiveError before allocating/reading the length array. This protects against corrupt headers and prevents out-of-range table parsing.

Source

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

	if (encodedCount > symbolCount) throw new ArchiveError(`Invalid ${label} temporary Huffman table size`);
	const lengths = new Uint8Array(symbolCount);
	let index = 0;
	while (index < encodedCount) {
		lengths[index++] = readCodeLength(reader, label);
		if (index === 3) {
			const skipped = reader.read(2);
			if (index + skipped > encodedCount) throw new ArchiveError(`Invalid ${label} temporary Huffman table`);
			index += skipped;
		}
	}
	return CanonicalHuffman.build(lengths, symbolCount, label);
}

function readCommandTree(reader: MsbBitReader, temporary: CanonicalHuffman, label: string): CanonicalHuffman {
	const symbolCount = 510;
	const encodedCount = reader.read(9);
	if (encodedCount === 0) return CanonicalHuffman.single(reader.read(9), symbolCount, label);
	if (encodedCount > symbolCount) throw new ArchiveError(`Invalid ${label} command Huffman table size`);
	const lengths = new Uint8Array(symbolCount);
	let index = 0;
	while (index < encodedCount) {
		const code = temporary.decode(reader);
		if (code <= 2) {
			const skipped = code === 0 ? 1 : code === 1 ? reader.read(4) + 3 : reader.read(9) + 20;
			if (index + skipped > encodedCount) throw new ArchiveError(`Invalid ${label} command Huffman table`);
			index += skipped;
		} else {
			lengths[index++] = code - 2;
		}
	}
	return CanonicalHuffman.build(lengths, symbolCount, label);
}

function readPositionTree(
	reader: MsbBitReader,
	positionBits: number,

View on GitHub (pinned to 9690622007)

Solutions

  1. Check the archive's method byte and use the matching table size / decompressor path
  2. Recompute the compressed-data start offset (header parse, extended-header skip) before decompressLhStatic
  3. Validate the file externally and restore a clean copy if checks fail
  4. Catch ArchiveError and report a corrupt or unsupported archive; retrying is pointless

Example fix

// before: single fixed-size path for all variants
const commandTree = readCommandTree(reader, tempTree, 'command');
// after: route by declared method
if (method === 'LH7') { /* 510+ symbol path */ } else { const commandTree = readCommandTree(reader, tempTree, 'command'); }
Defensive patterns

Strategy: try-catch

Validate before calling

// Verify offsets and method before decompressing:
if (!isSupportedLhMethod(method)) throw new Error(`unsupported method: ${method}`);
if (dataStart + declaredCompressedSize > data.length) throw new Error('truncated archive');

Type guard

function isSupportedLhMethod(method: string): boolean {
  return ['LH4', 'LH5', 'LH6', 'LH7'].includes(method);
}

Try / catch

try {
  const out = decompressLhStatic(data.subarray(dataStart), originalSize);
} catch (err) {
  if (err instanceof ArchiveError && err.message.includes('table size')) {
    throw new Error('command table header corrupt or LH-variant mismatch');
  }
  throw err;
}

Prevention

When it happens

Trigger: decompressLhStatic on a stream whose 9-bit command count field is corrupted or misread — e.g. parsing an LH7 block with an LH5-sized reader, byte-offset errors from a bad LHA header, or fuzzed inputs with counts > 510.

Common situations: Truncated or damaged .lzh files, variant mismatches (LH4/LH5/LH6/LH7 confusion), misparsed extended headers shifting the data start, fuzzing.

Related errors


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