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
- Verify the file is complete: re-download or re-copy the .zip and compare its size/hash with the source.
- Open the file with `unzip -t archive.zip` (or `zipinfo`) to confirm it is structurally valid outside this library.
- 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.
- Repair the archive with `zip -FF broken.zip --out fixed.zip`, then retry.
- 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
- Always confirm downloads are complete (compare content-length/size) before reading archives.
- For embedded zips (SFX, inner blobs), slice the actual payload so EOCD offsets are relative to the byte source you pass.
- Checksum archives after transfer to catch corruption early.
- Never read a zip while its producer is still writing it.
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Invalid ZIP archive: truncated central directory
- Invalid ZIP archive: missing ZIP64 central-directory metadat
- Invalid ZIP archive: malformed NTFS extra field
- Invalid ZIP archive: truncated extra-field header
- Invalid ZIP archive: malformed extra field
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/dfe759a7f712bef9.
Report an issue: GitHub.