can1357/oh-my-pi · error · ArchiveError

Invalid ARJ local header

Error message

Invalid ARJ local header

What it means

Each ARJ local (file) header must begin with a header-size byte of at least 30 and no larger than the remaining block body. When parseArjBlock returns a non-end block whose first header size byte violates that, the reader concludes the byte stream is not a valid ARJ member sequence — usually meaning the offset walked into garbage, the file is corrupt, or it is not an ARJ archive at all.

Source

Thrown at packages/utils/src/ar/arj.ts:262

	if (mainFirstHeaderSize < 30 || mainFirstHeaderSize > main.bodySize || bytes[main.bodyStart + 6] !== 2) {
		throw new ArchiveError("Invalid ARJ main header");
	}
	const mainFlags = bytes[main.bodyStart + 4]!;
	if ((mainFlags & 0x01) !== 0) throw new ArchiveError("Encrypted ARJ archives are unsupported");
	if ((mainFlags & 0x04) !== 0) throw new ArchiveError("Multi-volume ARJ archives are unsupported");

	const entries: ArchiveIndexEntry[] = [];
	let offset = main.nextOffset;
	let metadataSize = main.metadataSize;
	let parsedCount = 0;
	for (;;) {
		const block = parseArjBlock(bytes, offset, options);
		metadataSize += block.metadataSize;
		assertIndexSize(metadataSize, options.limits, "index");
		if (block.isEnd) break;
		assertEntryCount(++parsedCount, options.limits);
		const firstHeaderSize = bytes[block.bodyStart]!;
		if (firstHeaderSize < 30 || firstHeaderSize > block.bodySize) throw new ArchiveError("Invalid ARJ local header");
		const hostOs = bytes[block.bodyStart + 3]!;
		const flags = bytes[block.bodyStart + 4]!;
		const method = bytes[block.bodyStart + 5]!;
		const fileType = bytes[block.bodyStart + 6]!;
		if ((flags & 0x01) !== 0) throw new ArchiveError("Encrypted ARJ members are unsupported");
		if ((flags & 0x0c) !== 0) throw new ArchiveError("Multi-volume ARJ members are unsupported");
		const packedSize = u32(bytes, block.bodyStart + 12);
		const size = u32(bytes, block.bodyStart + 16);
		const fileCrc = u32(bytes, block.bodyStart + 20);
		const accessMode = u16(bytes, block.bodyStart + 26);
		const filename = readCString(
			bytes,
			block.bodyStart + firstHeaderSize,
			block.bodyStart + block.bodySize,
			"filename",
		);
		readCString(bytes, filename.next, block.bodyStart + block.bodySize, "comment");
		assertArchivePathBytes(

View on GitHub (pinned to 9690622007)

Solutions

  1. Re-obtain or re-download the archive and verify its checksum against the source of truth.
  2. Validate the file is a real ARJ archive (magic 0x60 0xEA and a sane main header) before parsing; reject early with a clearer error.
  3. Try repairing with `arj y` or an external ARJ tool to see if the header table can be recovered.
  4. If the archive is generated by your own code, dump bytes around the failing offset and fix the writer's header-size field.

Example fix

// before
const entries = await readArj(bytes, options); // throws mid-parse on corrupt data
// after
if (!looksLikeArj(bytes)) throw new Error("Not an ARJ archive");
if (!await checksumMatches(bytes, expectedSha256)) throw new Error("Archive corrupted in transit");
const entries = await readArj(bytes, options);
Defensive patterns

Strategy: validation

Validate before calling

import { createHash } from "node:crypto";
function assertArchiveIntact(bytes: Uint8Array, expectedSha256?: string): void {
	if (!looksLikeArj(bytes)) throw new Error("File is not an ARJ archive");
	if (expectedSha256 && createHash("sha256").update(bytes).digest("hex") !== expectedSha256) {
		throw new Error("Archive corrupted in transit");
	}
}

Type guard

function looksLikeArj(bytes: Uint8Array): boolean {
	// ARJ magic: 0x60 0xEA followed by 0x12 0x27 (basic header size 26 + header size field)
	return bytes.length > 4 && bytes[0] === 0x60 && bytes[1] === 0xea;
}

Try / catch

try {
	entries = await readArj(source, options);
} catch (e) {
	if (e instanceof ArchiveError && e.message === "Invalid ARJ local header") {
		logger.error("ARJ stream corrupt at a local header; verify source checksum", { path });
		return { status: "corrupt" };
	}
	throw e;
}

Prevention

When it happens

Trigger: readArj walks local headers via parseArjBlock; a block passes with isEnd=false but bytes[block.bodyStart] (basic header size) is <30 or > block.bodySize — truncated download, corrupted middle bytes, or a non-ARJ file misidentified as ARJ (magic check passed by luck).

Common situations: Incomplete downloads/uploads; bit-rot on old archives; files renamed to .arj that are actually another format; test fixtures hand-assembled incorrectly.

Related errors


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