can1357/oh-my-pi · error · ArchiveError
Invalid ZIP archive: missing end of central directory
Error message
Invalid ZIP archive: missing end of central directory
What it means
findEocd scans the tail of the buffer for the end-of-central-directory (EOCD) record, whose signature must be followed by a comment of exactly the declared length. If no candidate matches, the reader concludes the EOCD is absent and throws this ArchiveError — the file is not a readable ZIP as far as this library is concerned.
Source
Thrown at packages/utils/src/ar/zip.ts:117
}
function checkedEnd(start: number, size: number, archiveSize: number, what: string): number {
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(size) || start < 0 || size < 0) {
throw new ArchiveError(`Invalid ZIP archive: ${what} has an invalid range`);
}
const end = start + size;
if (!Number.isSafeInteger(end) || end > archiveSize) {
throw new ArchiveError(`Invalid ZIP archive: ${what} exceeds archive size`);
}
return end;
}
function findEocd(tail: Uint8Array): number {
for (let offset = tail.byteLength - EOCD_LENGTH; offset >= 0; offset--) {
if (readUInt32LE(tail, offset) !== EOCD_SIGNATURE) continue;
if (offset + EOCD_LENGTH + readUInt16LE(tail, offset + 20) === tail.byteLength) return offset;
}
throw new ArchiveError("Invalid ZIP archive: missing end of central directory");
}
async function readZip64Info(
source: ByteSource,
tail: Uint8Array,
tailStart: number,
eocdOffset: number,
): Promise<CentralDirectoryInfo | undefined> {
const locatorOffset = eocdOffset - ZIP64_LOCATOR_LENGTH;
if (locatorOffset < 0) return undefined;
const locator =
locatorOffset >= tailStart
? tail.subarray(locatorOffset - tailStart, locatorOffset - tailStart + ZIP64_LOCATOR_LENGTH)
: await source.read(locatorOffset, eocdOffset);
if (locator.byteLength !== ZIP64_LOCATOR_LENGTH || readUInt32LE(locator, 0) !== ZIP64_LOCATOR_SIGNATURE) {
return undefined;
}
if (readUInt32LE(locator, 4) !== 0 || readUInt32LE(locator, 16) !== 1) {View on GitHub (pinned to 9690622007)
Solutions
- Verify the file is really a ZIP (`file x.zip` should say 'Zip archive data')
- Re-download/re-export the archive — missing EOCD means truncation at the end
- Remove or fix an archive comment if one was recently edited (comment length must equal declared value)
- Prepend/strip handling: if the file has a prefix (e.g. self-extractor stub), ensure the tail slice given to the reader still contains the full EOCD
Example fix
// before
const zip = await readZip(bytes); // throws on truncated tail
// after
const tail = new TextDecoder().decode(bytes.slice(-64));
if (!bytes.slice(-64).some((_, i) => readUInt32LE(bytes, i) === 0x06054b50)) {
throw new Error("download incomplete: EOCD missing, retrying");
}
const zip = await readZip(bytes); Defensive patterns
Strategy: validation
Validate before calling
function hasEocd(bytes: Uint8Array): boolean {
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const min = Math.max(0, bytes.length - 22 - 65535);
for (let i = bytes.length - 22; i >= min; i--) {
if (dv.getUint32(i, true) !== 0x06054b50) continue;
if (i + 22 + dv.getUint16(i + 20, true) === bytes.length) return true;
}
return false;
}
if (!hasEocd(bytes)) throw new Error("refusing to parse: ZIP is truncated or not a zip"); Try / catch
try {
const zip = await readZip(bytes);
} catch (err) {
if (err instanceof ArchiveError && err.message.includes("missing end of central directory")) {
// distinguish "not a zip" from "truncated zip" for better UX
const isZip = bytes.length > 4 && bytes[0] === 0x50 && bytes[1] === 0x4b;
return rejectUpload(isZip ? "incomplete download" : "not a zip file");
}
throw err;
} Prevention
- Verify the magic bytes at the start AND a valid EOCD at the end before parsing
- For downloads, confirm completion (content-length match) before parsing
- Strip SFX stubs or handle prefix offsets before passing tails to the parser
When it happens
Trigger: readZip on: (1) a file that is not a ZIP at all, (2) a truncated ZIP missing its EOCD, (3) a ZIP whose comment length field doesn't match the actual trailing bytes, (4) a self-extracting/multi-part file where the tail was stripped or prepended bytes shift EOCD parsing.
Common situations: Renaming a non-zip file to .zip; download managers that cut the last bytes; archives with comments edited by tools that mis-set the comment-length field; concatenated spans missing the final record.
Related errors
- Invalid ZIP archive: ${what} has an invalid range
- Invalid ZIP archive: missing ZIP64 end of central directory
- Invalid CPIO archive: ${field} is not a valid base-${radix}
- Invalid ZIP archive: ${what} exceeds archive size
- Invalid ZIP archive: central directory is out of bounds or m
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/774b612d349a8c19.
Report an issue: GitHub.