can1357/oh-my-pi · error · ArchiveError

Invalid LZH archive header

Error message

Invalid LZH archive header

What it means

sniffLzh() checks the opening bytes look like an LZH member header: at least 22 bytes, the method string starting with '-l' at offset 2 and ending '-' at offset 6, a valid compression-method pattern, and header level <= 2. Files that fail are not LZH at all, so the reader refuses them before any parsing.

Source

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

	const method = String.fromCharCode(...bytes.subarray(2, 7));
	return LHA_METHOD_PATTERN.test(method) && bytes[20]! <= 2;
}

/** Index an LZH/LHA archive and lazily decode its members from the bounded archive buffer. */
export const readLzh: FormatReader = async (
	source: ByteSource,
	options: FormatReadOptions,
): Promise<ArchiveIndexEntry[]> => {
	assertInMemorySize(source.size, options.limits);
	let bytes: Uint8Array;
	try {
		bytes = await readAllBytes(source);
	} catch (error) {
		if (error instanceof ArchiveError) throw error;
		throw new ArchiveError(`Unable to read LZH archive: ${error instanceof Error ? error.message : String(error)}`);
	}
	if (bytes.byteLength !== source.size) throw new ArchiveError("Invalid LZH archive: truncated data");
	if (!sniffLzh(bytes)) throw new ArchiveError("Invalid LZH archive header");
	const entries: ArchiveIndexEntry[] = [];
	let offset = 0;
	let parsedCount = 0;
	let metadataSize = 0;
	while (offset < bytes.byteLength && bytes[offset] !== 0) {
		const header = parseLzhHeader(bytes, offset, options);
		metadataSize += header.dataStart - offset;
		assertIndexSize(metadataSize, options.limits, "index");
		assertEntryCount(++parsedCount, options.limits);
		if (header.nextOffset <= offset) throw new ArchiveError("Invalid LZH archive: header did not advance");
		offset = header.nextOffset;
		if (!header.path) continue;
		const isDirectory = header.method === "-lhd-";
		if (isDirectory && header.mode !== undefined && (header.mode & 0xf000) === 0xa000) {
			const separator = header.path.indexOf("|");
			if (separator < 1) throw new ArchiveError(`Invalid LZH symbolic link '${header.path}'`);
			const path = normalizeArchiveEntryPath(header.path.slice(0, separator));
			const targetPath = normalizeArchiveEntryPath(header.path.slice(separator + 1));

View on GitHub (pinned to 9690622007)

Solutions

  1. Confirm the file's actual type (file(1) command or magic bytes) and use the correct format reader.
  2. If it is a self-extracting or wrapped LHA (BinHex, MacBinary), strip the wrapper first.
  3. Re-download if the 'archive' is actually an HTML/text error page.
  4. Call sniffLzh() yourself (exported) to gate before invoking readLzh.

Example fix

// before
const entries = await readLzh(source, options);
// after
const bytes = await readAllBytes(source);
if (!sniffLzh(bytes)) throw new Error("not an LZH archive — check the file type");
const entries = await readLzh(source, options);
Defensive patterns

Strategy: type-guard

Validate before calling

const bytes = await readAllBytes(source);
if (!sniffLzh(bytes)) throw new Error("not an LZH archive — check the file type with `file`");

Type guard

function looksLikeLzh(buf: Uint8Array): boolean {
  return sniffLzh(buf);
}

Try / catch

try {
  const entries = await readLzh(source, options);
} catch (err) {
  if (err instanceof ArchiveError && err.message === "Invalid LZH archive header") {
    throw new Error("input is not LZH — detect the real format (zip/gzip/7z?) and use the right reader");
  }
  throw err;
}

Prevention

When it happens

Trigger: readLzh() called on bytes that are not an LZH/LHA archive: a different archive format (zip, gzip, 7z), a plain text/binary file, or an empty file renamed to .lzh.

Common situations: Wrong format passed by mistake; server returned an HTML error page saved as .lzh; MacBinary/BinHex wrapper around an LHA file; file extension renamed without conversion.

Related errors


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