can1357/oh-my-pi · error · ArchiveError

Invalid LZH Unicode path header: odd UTF-16 length

Error message

Invalid LZH Unicode path header: odd UTF-16 length

What it means

LZH level-1/2 headers can carry Unicode (UTF-16LE) filename/directory extended headers (types 0x44/0x45). This library requires those payloads to contain a whole number of UTF-16 code units; an odd byte count means the archive is malformed, so decodeUtf16 throws before decoding.

Source

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

	if (
		!Number.isSafeInteger(start) ||
		!Number.isSafeInteger(end) ||
		start < 0 ||
		end < start ||
		end > bytes.byteLength
	) {
		throw new ArchiveError(`Invalid LZH archive: truncated ${what}`);
	}
}

function decodeLegacy(bytes: Uint8Array): string {
	let end = bytes.indexOf(0);
	if (end < 0) end = bytes.byteLength;
	return LEGACY_DECODER.decode(bytes.subarray(0, end));
}

function decodeUtf16(bytes: Uint8Array): string {
	if ((bytes.byteLength & 1) !== 0) throw new ArchiveError("Invalid LZH Unicode path header: odd UTF-16 length");
	let end = bytes.byteLength;
	while (end >= 2 && bytes[end - 1] === 0 && bytes[end - 2] === 0) end -= 2;
	return UTF16LE_DECODER.decode(bytes.subarray(0, end));
}

function dosTimeToMs(value: number): number | undefined {
	if (value === 0) return undefined;
	const year = 1980 + ((value >>> 25) & 0x7f);
	const month = (value >>> 21) & 0x0f;
	const day = (value >>> 16) & 0x1f;
	const hour = (value >>> 11) & 0x1f;
	const minute = (value >>> 5) & 0x3f;
	const second = (value & 0x1f) * 2;
	if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59) return undefined;
	return Date.UTC(year, month - 1, day, hour, minute, second);
}

interface LzhExtendedFields {

View on GitHub (pinned to 9690622007)

Solutions

  1. Verify the archive with its original source (re-download or re-extract) — odd-length UTF-16 payloads indicate corruption, not a recoverable path
  2. Try extracting the same archive with a tolerant tool (e.g. 7-Zip or LHA) to confirm which member is damaged; if only one member is bad, skip/repair it upstream
  3. If you generate archives, fix the writer so the 0x44/0x45 extended-header size includes the 2-byte size trailer and yields an even data length
  4. As a workaround, strip the Unicode extended headers from the archive (convert paths to the legacy 0x01/0x02 headers)

Example fix

null
Defensive patterns

Strategy: try-catch

Validate before calling

// UTF-16 extended-header data must have even byte length
function isValidUtf16HeaderData(data: Uint8Array): boolean {
	return (data.byteLength & 1) === 0;
}

Type guard

null

Try / catch

import { ArchiveError } from "@oh-my-pi/pi-utils/ar/error";
try {
	const entries = await readArchive(lzhBytes);
} catch (err) {
	if (err instanceof ArchiveError && err.message.includes("odd UTF-16 length")) {
		// archive has corrupt Unicode path headers — treat as damaged, request a fresh copy
	}
	throw err;
}

Prevention

When it happens

Trigger: Reading an LZH archive whose member header contains a 0x44 (Unicode filename) or 0x45 (Unicode directory) extended header whose data section has an odd number of bytes — e.g. the header size field was corrupted, truncated, or written by a tool that computed the extension length incorrectly.

Common situations: Corrupted or partially downloaded .lzh/.lha files; archives produced by non-conformant or buggy Japanese archivers (e.g. old UNLHA32-compatible tools) that miscount extended-header sizes; hand-patched headers.

Related errors


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