{"record":{"id":"dfe759a7f712bef9","repo":"can1357/oh-my-pi","slug":"invalid-zip-archive-central-directory-is-out-of-b","errorCode":null,"errorMessage":"Invalid ZIP archive: central directory is out of bounds or malformed","messagePattern":"Invalid ZIP archive: central directory is out of bounds or malformed","errorType":"exception","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/zip.ts","lineNumber":176,"sourceCode":"\t\t\toffset: readUInt64LE(record, 48),\n\t\t\tphysicalEnd: candidate,\n\t\t\tarchiveOffset: 0,\n\t\t};\n\t}\n\tthrow new ArchiveError(\"Invalid ZIP archive: missing ZIP64 end of central directory\");\n}\n\nasync function locateCentralDirectory(source: ByteSource, info: CentralDirectoryInfo): Promise<number> {\n\tif (info.entries === 0) return info.offset;\n\tconst candidates = [info.offset];\n\tconst adjacent = info.physicalEnd - info.size;\n\tif (adjacent !== info.offset) candidates.push(adjacent);\n\tfor (const offset of candidates) {\n\t\tif (offset < 0 || offset + 4 > source.size || offset + info.size > source.size) continue;\n\t\tconst signature = await source.read(offset, offset + 4);\n\t\tif (signature.byteLength === 4 && readUInt32LE(signature, 0) === CENTRAL_HEADER_SIGNATURE) return offset;\n\t}\n\tthrow new ArchiveError(\"Invalid ZIP archive: central directory is out of bounds or malformed\");\n}\n\nasync function readCentralDirectoryInfo(source: ByteSource, limits: ArchiveLimits): Promise<CentralDirectoryInfo> {\n\tif (source.size < EOCD_LENGTH) throw new ArchiveError(\"Invalid ZIP archive: missing end of central directory\");\n\tconst tailLength = Math.min(source.size, EOCD_LENGTH + MAX_COMMENT_LENGTH);\n\tconst tailStart = source.size - tailLength;\n\tconst tail = await source.read(tailStart, source.size);\n\tif (tail.byteLength !== tailLength)\n\t\tthrow new ArchiveError(\"Invalid ZIP archive: truncated end of central directory\");\n\tconst eocdIndex = findEocd(tail);\n\tconst eocdOffset = tailStart + eocdIndex;\n\tconst disk = readUInt16LE(tail, eocdIndex + 4);\n\tconst centralDisk = readUInt16LE(tail, eocdIndex + 6);\n\tconst entriesOnDisk = readUInt16LE(tail, eocdIndex + 8);\n\tlet entries = readUInt16LE(tail, eocdIndex + 10);\n\tlet size = readUInt32LE(tail, eocdIndex + 12);\n\tlet offset = readUInt32LE(tail, eocdIndex + 16);\n\tif (","sourceCodeStart":158,"sourceCodeEnd":194,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/zip.ts#L158-L194","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before: reading a truncated download directly\nconst data = await Bun.file(partialPath).arrayBuffer();\nconst archive = readZip(memoryByteSource(new Uint8Array(data)));\n// after: verify completeness first\nconst expected = await getExpectedSize(url);\nif (fileSize !== expected) throw new Error(`incomplete download: ${fileSize}/${expected}`);\nconst archive = readZip(memoryByteSource(new Uint8Array(await Bun.file(fullPath).arrayBuffer())));","handlingStrategy":"validation","validationCode":"import * as fs from \"node:fs/promises\";\n// A zip must be big enough to hold at least an EOCD, and standard tools should agree it's valid.\nconst size = (await fs.stat(path)).size;\nif (size < 22) throw new Error(`not a plausible zip (${size} bytes)`);\n// optionally: verify size matches the upstream content-length you expect\nif (expectedSize != null && size !== expectedSize) throw new Error(`incomplete file: ${size}/${expectedSize}`);","typeGuard":null,"tryCatchPattern":"try {\n  const zip = readZip(memoryByteSource(bytes));\n  // use zip\n} catch (err) {\n  if (err instanceof ArchiveError && err.message.includes(\"central directory is out of bounds\")) {\n    // recover: re-download or repair with `zip -FF`\n  } else throw err;\n}","preventionTips":["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."],"tags":["zip","archive","corruption","central-directory"],"backgroundTag":"zip-central-directory-invalid","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}