{"record":{"id":"774b612d349a8c19","repo":"can1357/oh-my-pi","slug":"invalid-zip-archive-missing-end-of-central-direct","errorCode":null,"errorMessage":"Invalid ZIP archive: missing end of central directory","messagePattern":"Invalid ZIP archive: missing end of central directory","errorType":"exception","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/zip.ts","lineNumber":117,"sourceCode":"}\n\nfunction checkedEnd(start: number, size: number, archiveSize: number, what: string): number {\n\tif (!Number.isSafeInteger(start) || !Number.isSafeInteger(size) || start < 0 || size < 0) {\n\t\tthrow new ArchiveError(`Invalid ZIP archive: ${what} has an invalid range`);\n\t}\n\tconst end = start + size;\n\tif (!Number.isSafeInteger(end) || end > archiveSize) {\n\t\tthrow new ArchiveError(`Invalid ZIP archive: ${what} exceeds archive size`);\n\t}\n\treturn end;\n}\n\nfunction findEocd(tail: Uint8Array): number {\n\tfor (let offset = tail.byteLength - EOCD_LENGTH; offset >= 0; offset--) {\n\t\tif (readUInt32LE(tail, offset) !== EOCD_SIGNATURE) continue;\n\t\tif (offset + EOCD_LENGTH + readUInt16LE(tail, offset + 20) === tail.byteLength) return offset;\n\t}\n\tthrow new ArchiveError(\"Invalid ZIP archive: missing end of central directory\");\n}\n\nasync function readZip64Info(\n\tsource: ByteSource,\n\ttail: Uint8Array,\n\ttailStart: number,\n\teocdOffset: number,\n): Promise<CentralDirectoryInfo | undefined> {\n\tconst locatorOffset = eocdOffset - ZIP64_LOCATOR_LENGTH;\n\tif (locatorOffset < 0) return undefined;\n\tconst locator =\n\t\tlocatorOffset >= tailStart\n\t\t\t? tail.subarray(locatorOffset - tailStart, locatorOffset - tailStart + ZIP64_LOCATOR_LENGTH)\n\t\t\t: await source.read(locatorOffset, eocdOffset);\n\tif (locator.byteLength !== ZIP64_LOCATOR_LENGTH || readUInt32LE(locator, 0) !== ZIP64_LOCATOR_SIGNATURE) {\n\t\treturn undefined;\n\t}\n\tif (readUInt32LE(locator, 4) !== 0 || readUInt32LE(locator, 16) !== 1) {","sourceCodeStart":99,"sourceCodeEnd":135,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/zip.ts#L99-L135","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Verify the file is really a ZIP (`file x.zip` should say 'Zip archive data')","Re-download/re-export the archive — missing EOCD means truncation at the end","Remove or fix an archive comment if one was recently edited (comment length must equal declared value)","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"],"exampleFix":"// before\nconst zip = await readZip(bytes); // throws on truncated tail\n// after\nconst tail = new TextDecoder().decode(bytes.slice(-64));\nif (!bytes.slice(-64).some((_, i) => readUInt32LE(bytes, i) === 0x06054b50)) {\n  throw new Error(\"download incomplete: EOCD missing, retrying\");\n}\nconst zip = await readZip(bytes);","handlingStrategy":"validation","validationCode":"function hasEocd(bytes: Uint8Array): boolean {\n  const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n  const min = Math.max(0, bytes.length - 22 - 65535);\n  for (let i = bytes.length - 22; i >= min; i--) {\n    if (dv.getUint32(i, true) !== 0x06054b50) continue;\n    if (i + 22 + dv.getUint16(i + 20, true) === bytes.length) return true;\n  }\n  return false;\n}\nif (!hasEocd(bytes)) throw new Error(\"refusing to parse: ZIP is truncated or not a zip\");","typeGuard":null,"tryCatchPattern":"try {\n  const zip = await readZip(bytes);\n} catch (err) {\n  if (err instanceof ArchiveError && err.message.includes(\"missing end of central directory\")) {\n    // distinguish \"not a zip\" from \"truncated zip\" for better UX\n    const isZip = bytes.length > 4 && bytes[0] === 0x50 && bytes[1] === 0x4b;\n    return rejectUpload(isZip ? \"incomplete download\" : \"not a zip file\");\n  }\n  throw err;\n}","preventionTips":["Verify the magic bytes at the start AND a valid EOCD at the end before parsing","For downloads, confirm completion (content-length match) before parsing","Strip SFX stubs or handle prefix offsets before passing tails to the parser"],"tags":["zip","archive","eocd","corrupt-archive"],"backgroundTag":"zip-eocd-missing","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}