can1357/oh-my-pi · error · ArchiveError

Invalid LZH level-${level} header size

Error message

Invalid LZH level-${level} header size

What it means

Thrown when a level-0 or level-1 LZH header's declared length byte is smaller than the minimum structurally required size (22 bytes for level 0, 25 for level 1). The header cannot possibly contain the mandatory fields, so parsing aborts.

Source

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

function parseLzhHeader(bytes: Uint8Array, offset: number, options: FormatReadOptions): ParsedLzhHeader {
	assertRange(bytes, offset, offset + 22, "header");
	const level = bytes[offset + 20]!;
	if (level > 2) throw new ArchiveError(`Unsupported LZH header level ${level}`);
	const method = String.fromCharCode(...bytes.subarray(offset + 2, offset + 7));
	if (!LHA_METHOD_PATTERN.test(method)) throw new ArchiveError(`Invalid LZH compression method '${method}'`);
	let packedSize = u32(bytes, offset + 7);
	let size = u32(bytes, offset + 11);
	let crc = 0;
	let legacyFilename: string | undefined;
	let mtimeMs: number | undefined;
	let osId = 0;
	let dataStart: number;
	const fields: LzhExtendedFields = {};

	if (level < 2) {
		const headerLength = bytes[offset]!;
		const minimum = level === 0 ? 22 : 25;
		if (headerLength < minimum) throw new ArchiveError(`Invalid LZH level-${level} header size`);
		const baseEnd = offset + headerLength + 2;
		assertRange(bytes, offset, baseEnd, "header");
		let sum = 0;
		for (let index = offset + 2; index < baseEnd; index++) sum = (sum + bytes[index]!) & 0xff;
		if (sum !== bytes[offset + 1]) throw new ArchiveError("Invalid LZH header checksum");
		const nameLength = bytes[offset + 21]!;
		if (22 + nameLength + 2 > headerLength + 2) throw new ArchiveError("Invalid LZH filename length");
		assertArchivePathBytes(nameLength, "member path", options.limits.maxPathBytes);
		legacyFilename = decodeLegacy(bytes.subarray(offset + 22, offset + 22 + nameLength));
		crc = u16(bytes, offset + 22 + nameLength);
		mtimeMs = dosTimeToMs(u32(bytes, offset + 15));
		if (level === 0) {
			const extendedStart = offset + 24 + nameLength;
			if (baseEnd - extendedStart >= 12) {
				const extended = bytes.subarray(extendedStart, baseEnd);
				if ((extended[0] === 0x55 || extended[0] === 0x4b) && extended[1] === 0) {
					osId = extended[0]!;
					fields.mtimeMs = u32(extended, 2) * 1000;

View on GitHub (pinned to 9690622007)

Solutions

  1. Replace the archive with an intact copy and verify its size/checksum
  2. Repack the source files into a fresh LHA archive
  3. If processing untrusted input, catch ArchiveError and reject the file

Example fix

// before: trusting any byte blob as a member
for (const member of lzhMembers(bytes)) { ... }
// after: guard the read
try {
  for (const member of lzhMembers(bytes)) { ... }
} catch (err) {
  if (err instanceof ArchiveError) throw new Error(`corrupt LZH: ${err.message}`);
  throw err;
}
Defensive patterns

Strategy: validation

Validate before calling

if (bytes.byteLength < 24) throw new Error('too small to be an LZH member');

Try / catch

try { parse(); } catch (err) { if (err instanceof ArchiveError && /header size/.test(err.message)) skip(err); else throw err; }

Prevention

When it happens

Trigger: Reading an archive with a truncated or corrupt header-length byte, or a forged/minimized header written by a broken tool.

Common situations: Partially downloaded .lzh files, archives damaged in storage, or fuzzed/malicious inputs.

Related errors


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