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
- Re-obtain or re-download the archive and verify its checksum against the source of truth.
- Validate the file is a real ARJ archive (magic 0x60 0xEA and a sane main header) before parsing; reject early with a clearer error.
- Try repairing with `arj y` or an external ARJ tool to see if the header table can be recovered.
- 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
- Always checksum ARJ files (SHA256) at transfer time and verify before parsing.
- Validate the ARJ magic and main header before indexing so non-ARJ files fail with a clearer error.
- Avoid hand-editing or concatenating ARJ files; regenerate instead.
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
- Invalid ARJ archive: truncated ${what}
- Invalid ARJ ${field}: missing terminator
- Invalid ARJ archive: no members
- Invalid ARJ basic header size
- Invalid ARJ basic header CRC32
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/7661846eb0345368.
Report an issue: GitHub.