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

  1. Verify the file is really a ZIP (`file x.zip` should say 'Zip archive data')
  2. Re-download/re-export the archive — missing EOCD means truncation at the end
  3. Remove or fix an archive comment if one was recently edited (comment length must equal declared value)
  4. 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

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


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