can1357/oh-my-pi · error · ArchiveError

Invalid ZIP archive: central directory is out of bounds or m

Error message

Invalid ZIP archive: central directory is out of bounds or malformed

What it means

Thrown by locateCentralDirectory after the end-of-central-directory (EOCD) record was found but the central directory it points to could not be located. The reader tries the declared offset and the offset computed from physicalEnd minus size; both fail bounds checks or lack the central-header signature 0x02014b50. This means the EOCD metadata does not describe where the central directory actually lives in the file.

Source

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

			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);
	const tailStart = source.size - tailLength;
	const tail = await source.read(tailStart, source.size);
	if (tail.byteLength !== tailLength)
		throw new ArchiveError("Invalid ZIP archive: truncated end of central directory");
	const eocdIndex = findEocd(tail);
	const eocdOffset = tailStart + eocdIndex;
	const disk = readUInt16LE(tail, eocdIndex + 4);
	const centralDisk = readUInt16LE(tail, eocdIndex + 6);
	const entriesOnDisk = readUInt16LE(tail, eocdIndex + 8);
	let entries = readUInt16LE(tail, eocdIndex + 10);
	let size = readUInt32LE(tail, eocdIndex + 12);
	let offset = readUInt32LE(tail, eocdIndex + 16);
	if (

View on GitHub (pinned to 9690622007)

Solutions

  1. Verify the file is complete: re-download or re-copy the .zip and compare its size/hash with the source.
  2. Open the file with `unzip -t archive.zip` (or `zipinfo`) to confirm it is structurally valid outside this library.
  3. If the zip is embedded (SFX/inner blob), extract or slice the actual zip payload before passing it in, so EOCD offsets match the byte source.
  4. Repair the archive with `zip -FF broken.zip --out fixed.zip`, then retry.
  5. If you control archive creation, ensure the writing tool finalizes the central directory correctly (e.g. close the zip writer before reading).

Example fix

// before: reading a truncated download directly
const data = await Bun.file(partialPath).arrayBuffer();
const archive = readZip(memoryByteSource(new Uint8Array(data)));
// after: verify completeness first
const expected = await getExpectedSize(url);
if (fileSize !== expected) throw new Error(`incomplete download: ${fileSize}/${expected}`);
const archive = readZip(memoryByteSource(new Uint8Array(await Bun.file(fullPath).arrayBuffer())));
Defensive patterns

Strategy: validation

Validate before calling

import * as fs from "node:fs/promises";
// A zip must be big enough to hold at least an EOCD, and standard tools should agree it's valid.
const size = (await fs.stat(path)).size;
if (size < 22) throw new Error(`not a plausible zip (${size} bytes)`);
// optionally: verify size matches the upstream content-length you expect
if (expectedSize != null && size !== expectedSize) throw new Error(`incomplete file: ${size}/${expectedSize}`);

Try / catch

try {
  const zip = readZip(memoryByteSource(bytes));
  // use zip
} catch (err) {
  if (err instanceof ArchiveError && err.message.includes("central directory is out of bounds")) {
    // recover: re-download or repair with `zip -FF`
  } else throw err;
}

Prevention

When it happens

Trigger: Calling zip info/read APIs on a file whose EOCD declares a central-directory offset+size that lands outside the file, or points at bytes not starting with the central-file-header signature. Caused by corruption, truncation, or prepended/appended data (e.g. a self-extracting stub or concatenated archives) with no matching EOCD fixup.

Common situations: A partially downloaded or truncated .zip; a zip embedded inside another file (self-extracting executables, Office/Java archives) where the EOCD offsets are relative to an inner blob; byte-level corruption; tools that rewrote the file without updating the central directory offset.

Understand the failure class

Related errors


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