can1357/oh-my-pi · error · ArchiveError

Invalid ZIP archive: missing ZIP64 end of central directory

Error message

Invalid ZIP archive: missing ZIP64 end of central directory

What it means

readZip64Info is only called when the ZIP needs ZIP64 decoding (the locator was found), yet no candidate location yields a record with the ZIP64 EOCD signature and a consistent extensible-data size ending exactly at the locator. The reader throws this ArchiveError because ZIP64 metadata it was told exists cannot actually be located.

Source

Thrown at packages/utils/src/ar/zip.ts:163

		const record = await source.read(candidate, candidate + ZIP64_EOCD_LENGTH);
		if (record.byteLength !== ZIP64_EOCD_LENGTH || readUInt32LE(record, 0) !== ZIP64_EOCD_SIGNATURE) continue;
		const extensibleSize = readUInt64LE(record, 4);
		if (extensibleSize < 44 || candidate + 12 + extensibleSize !== locatorOffset) continue;
		if (readUInt32LE(record, 16) !== 0 || readUInt32LE(record, 20) !== 0) {
			throw new ArchiveError("Multi-volume ZIP archives are not supported");
		}
		const entriesOnDisk = readUInt64LE(record, 24);
		const entries = readUInt64LE(record, 32);
		if (entriesOnDisk !== entries) throw new ArchiveError("Multi-volume ZIP archives are not supported");
		return {
			entries,
			size: readUInt64LE(record, 40),
			offset: readUInt64LE(record, 48),
			physicalEnd: candidate,
			archiveOffset: 0,
		};
	}
	throw new ArchiveError("Invalid ZIP archive: missing ZIP64 end of central directory");
}

async function locateCentralDirectory(source: ByteSource, info: CentralDirectoryInfo): Promise<number> {
	if (info.entries === 0) return info.offset;
	const candidates = [info.offset];
	const adjacent = info.physicalEnd - info.size;
	if (adjacent !== info.offset) candidates.push(adjacent);
	for (const offset of candidates) {
		if (offset < 0 || offset + 4 > source.size || offset + info.size > source.size) continue;
		const signature = await source.read(offset, offset + 4);
		if (signature.byteLength === 4 && readUInt32LE(signature, 0) === CENTRAL_HEADER_SIGNATURE) return offset;
	}
	throw new ArchiveError("Invalid ZIP archive: central directory is out of bounds or malformed");
}

async function readCentralDirectoryInfo(source: ByteSource, limits: ArchiveLimits): Promise<CentralDirectoryInfo> {
	if (source.size < EOCD_LENGTH) throw new ArchiveError("Invalid ZIP archive: missing end of central directory");
	const tailLength = Math.min(source.size, EOCD_LENGTH + MAX_COMMENT_LENGTH);

View on GitHub (pinned to 9690622007)

Solutions

  1. Re-create the archive with a current zip implementation (Info-ZIP, 7-Zip) that writes ZIP64 correctly
  2. Check for truncation — ZIP64 records are near the end, so a cut file loses them first
  3. If the file has a prepended stub, use a reader path that accounts for the prefix offset, or strip the stub
  4. Validate with `unzip -t`; mismatch there confirms structural damage

Example fix

// before
const zip = await readZip(sfxBytes); // ZIP64 EOCD offset shifted by stub
// after
const raw = await Bun.file("app.sfx").bytes();
const stripped = stripSfxStub(raw); // remove prepended bytes so offsets line up
const zip = await readZip(stripped);
Defensive patterns

Strategy: validation

Validate before calling

// require ZIP64 structures before parsing huge zips
function needsZip64AndHasLocator(bytes: Uint8Array): boolean {
  const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  // find EOCD
  for (let i = bytes.length - 22; i >= Math.max(0, bytes.length - 22 - 65535); i--) {
    if (dv.getUint32(i, true) !== 0x06054b50) continue;
    const entries = dv.getUint16(i + 10, true);
    const cdSize = dv.getUint32(i + 12, true);
    const needs64 = entries === 0xffff || cdSize === 0xffffffff;
    if (!needs64) return false;
    // locator must exist just before EOCD
    const loc = i - 20;
    return loc >= 0 && dv.getUint32(loc, true) === 0x07064b50;
  }
  return false;
}

Try / catch

try {
  const zip = await readZip(bytes);
} catch (err) {
  if (err instanceof ArchiveError && err.message.includes("missing ZIP64 end of central directory")) {
    return rejectUpload("zip64 metadata missing/corrupt — archive may be truncated or stub-prefixed");
  }
  throw err;
}

Prevention

When it happens

Trigger: readZip on an archive with a valid ZIP64 locator but a missing, moved, or corrupted ZIP64 EOCD record; prepended data (self-extractor stubs, concatenated files) shifting offsets so the declared ZIP64 EOCD offset no longer matches.

Common situations: Very large archives (>4GB or >65535 entries) that require ZIP64, whose final records were truncated; SFX stubs prepended after creation; tools that rewrite the EOCD but not the ZIP64 structures.

Related errors


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