can1357/oh-my-pi · error · ArchiveError

Invalid ${label} compressed data: history distance is out of

Error message

Invalid ${label} compressed data: history distance is out of range

What it means

A match's distance (back-reference offset) must be smaller than both the dictionary size and the bytes already emitted; otherwise it would read before the start of the output. This indicates corrupt position data.

Source

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

		}
		blockRemaining--;
		const symbol = commands!.decode(reader);
		if (symbol < 256) {
			output[outputPosition++] = symbol;
			continue;
		}
		const length = symbol - 256 + 3;
		if (length > outSize - outputPosition) {
			throw new ArchiveError(`Invalid ${label} compressed data: match exceeds declared size`);
		}
		const positionCode = positions!.decode(reader);
		let distance = positionCode;
		if (positionCode > 1) {
			const lowBitCount = positionCode - 1;
			distance = 2 ** lowBitCount + reader.read(lowBitCount);
		}
		if (distance >= dictionarySize || distance >= outputPosition) {
			throw new ArchiveError(`Invalid ${label} compressed data: history distance is out of range`);
		}
		let sourcePosition = outputPosition - distance - 1;
		for (let index = 0; index < length; index++) output[outputPosition++] = output[sourcePosition++]!;
	}
	if (blockRemaining !== 0) throw new ArchiveError(`Invalid ${label} compressed data: block exceeds declared size`);
	reader.assertZeroPadding();
	return output;
}

function decompressLzs(packed: Uint8Array, outSize: number): Uint8Array {
	const reader = new MsbBitReader(packed, "LZH -lzs-");
	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");

View on GitHub (pinned to 9690622007)

Solutions

  1. Pass the correct dictionarySize for the method variant
  2. Ensure decompression starts at the true beginning of the member's compressed data
  3. Re-validate/re-download the archive
  4. Catch ArchiveError for untrusted input

Example fix

// before
decompressLhStatic(packed, outSize, 5, 256, "LZH -lh5-"); // dictionary 4KB
// after
// -lh6-/-lh7- use larger dictionaries
decompressLhStatic(packed, outSize, 6, 8192, "LZH -lh6-");
Defensive patterns

Strategy: try-catch

Validate before calling

// dictionarySize must match the method; check it is a power-of-two-ish sane value
if (dictionarySize <= 0 || !Number.isSafeInteger(dictionarySize)) throw new Error("bad dictionary size");

Try / catch

try {
  const out = decompressLhStatic(packed, outSize, positionBits, positionSymbols, label);
} catch (err) {
  if (err instanceof ArchiveError && err.message.includes("distance")) return null;
  throw err;
}

Prevention

When it happens

Trigger: decompressLhStatic decodes a position symbol plus low bits whose distance >= dictionarySize or >= bytes written so far — corrupt stream, wrong dictionarySize for the method, or output starting mid-stream so outputPosition is too small for early references.

Common situations: Wrong dictionary size parameter (e.g. 4KB for -lh5- data that needs larger); misparsed extended headers shifting stream start; fuzzed archives.

Related errors


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