can1357/oh-my-pi · error · ArchiveError

Invalid LZH -lzs- data: match exceeds declared size

Error message

Invalid LZH -lzs- data: match exceeds declared size

What it means

In -lzs- mode, a match is an 11-bit history position plus 4-bit length; if the resulting length exceeds remaining output space, the library refuses to write past outSize.

Source

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

	const output = new Uint8Array(outSize);
	const history = new Uint8Array(2048);
	history.fill(0x20);
	let historyPosition = 2048 - 17;
	let outputPosition = 0;
	const emit = (value: number): void => {
		if (outputPosition >= outSize) throw new ArchiveError("Invalid LZH -lzs- data: output exceeds declared size");
		output[outputPosition++] = value;
		history[historyPosition] = value;
		historyPosition = (historyPosition + 1) & 2047;
	};
	while (outputPosition < outSize) {
		if (reader.read(1) !== 0) {
			emit(reader.read(8));
		} else {
			const position = reader.read(11);
			const length = reader.read(4) + 2;
			if (length > outSize - outputPosition)
				throw new ArchiveError("Invalid LZH -lzs- data: match exceeds declared size");
			for (let index = 0; index < length; index++) emit(history[(position + index) & 2047]!);
		}
	}
	reader.assertZeroPadding();
	return output;
}

const ZERO_CRC16_BYTES = new Uint8Array(2);

function crc16WithZeroRange(bytes: Uint8Array, zeroStart: number, zeroEnd: number): number {
	if (zeroStart < 0) return crc16Arc(bytes);
	let value = crc16Arc(bytes.subarray(0, zeroStart));
	value = crc16Arc(ZERO_CRC16_BYTES.subarray(0, zeroEnd - zeroStart), value);
	return crc16Arc(bytes.subarray(zeroEnd), value);
}

function u16(bytes: Uint8Array, offset: number): number {
	return bytes[offset]! | (bytes[offset + 1]! << 8);

View on GitHub (pinned to 9690622007)

Solutions

  1. Verify the member's declared size matches the data (check CRC)
  2. Re-obtain the archive
  3. Catch ArchiveError for untrusted input and skip the entry
Defensive patterns

Strategy: try-catch

Validate before calling

if (packed.length < 2) throw new Error("-lzs- stream too short");
if (!Number.isSafeInteger(outSize) || outSize <= 0) throw new Error("bad outSize");

Try / catch

try {
  const out = decompressLzs(packed, outSize);
} catch (err) {
  if (err instanceof ArchiveError && err.message.includes("match exceeds")) return null;
  throw err;
}

Prevention

When it happens

Trigger: decompressLzs decodes a match whose length (4-bit+2) exceeds outSize - outputPosition — corrupt bitstream, or outSize smaller than the real uncompressed size.

Common situations: Wrong size field parsed from header; archives truncated/damaged in transit; fuzzed inputs.

Related errors


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