can1357/oh-my-pi · error · ArchiveError

Invalid LZH level-2 extended header chain

Error message

Invalid LZH level-2 extended header chain

What it means

After walking the level-2 extended-header chain, the cursor must land exactly on the header end: extensions tile the space contiguously, terminated by a zero size word. A cursor left short of headerEnd means the chain is malformed (overlapping/incorrect sizes that left unexplained trailing bytes).

Source

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

		let extensionCount = 0;
		while (cursor + 2 <= headerEnd) {
			const extensionSize = u16(bytes, cursor);
			if (extensionSize === 0) {
				cursor += 2;
				break;
			}
			if (extensionSize < 3 || cursor + extensionSize > headerEnd)
				throw new ArchiveError("Invalid LZH extended header size");
			if (++extensionCount > 65_535) throw new ArchiveError("Invalid LZH archive: too many extended headers");
			const type = bytes[cursor + 2]!;
			const data = bytes.subarray(cursor + 3, cursor + extensionSize);
			if (type === 0x01 || type === 0x02 || type === 0x44 || type === 0x45) {
				assertArchivePathBytes(data.byteLength, "member path", options.limits.maxPathBytes);
			}
			processExtendedHeader(type, data, cursor + 3, fields);
			cursor += extensionSize;
		}
		if (cursor !== headerEnd) throw new ArchiveError("Invalid LZH level-2 extended header chain");
		dataStart = headerEnd;
		packedSize = fields.packedSize ?? packedSize;
	}
	if (level === 1 && osId === 0x20 && method === "-lh7-") {
		throw new ArchiveError("LZH uses the incompatible LHARK -lh7- variant");
	}

	size = fields.size ?? size;
	mtimeMs = fields.mtimeMs ?? mtimeMs;
	const mode = fields.mode;
	if (fields.commonCrc !== undefined) {
		const relative = fields.commonCrcOffset! - offset;
		if (crc16WithZeroRange(bytes.subarray(offset, dataStart), relative, relative + 2) !== fields.commonCrc) {
			throw new ArchiveError("Invalid LZH common header CRC-16");
		}
	}
	const filename = fields.unicodeFilename ?? fields.filename ?? legacyFilename ?? "";
	const directory = fields.unicodeDirectory ?? fields.directory ?? "";

View on GitHub (pinned to 9690622007)

Solutions

  1. Re-extract or re-download the archive and validate against a known checksum.
  2. Repack the archive with a conformant tool (lha/lhasa) so the extended-header chain is regenerated correctly.
  3. If the file came from a legacy source, try opening it with a reference tool first to confirm whether it is valid LZH at all.
  4. If processing untrusted input, catch ArchiveError and reject the file.
Defensive patterns

Strategy: validation

Validate before calling

const bytes = await readAllBytes(source);
if (!sniffLzh(bytes)) throw new Error("not LZH");
if ((await sha256File(path)) !== expectedSha256) throw new Error("archive integrity check failed");

Try / catch

try {
  const entries = await readLzh(source, options);
} catch (err) {
  if (err instanceof ArchiveError && err.message === "Invalid LZH level-2 extended header chain") {
    throw new Error("LZH header chain malformed — repack with a standard LHA tool");
  }
  throw err;
}

Prevention

When it happens

Trigger: readLzh() on a level-2 archive where extended headers do not exactly fill the header region — e.g. extension sizes that skip bytes or a missing zero terminator within headerEnd.

Common situations: Archives written by buggy or non-conformant LZH implementations; partially corrupted files; hand-edited binaries.

Related errors


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