{"record":{"id":"1bdf51bb63b05dfb","repo":"chatboxai/chatbox","slug":"zip-archive-central-directory-is-invalid","errorCode":null,"errorMessage":"ZIP archive central directory is invalid","messagePattern":"ZIP archive central directory is invalid","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/renderer/packages/backup/zip.ts","lineNumber":193,"sourceCode":"  const tailSize = Math.min(file.size, minimumRecordSize + maximumCommentSize)\n  const tail = new Uint8Array(await file.slice(file.size - tailSize).arrayBuffer())\n  let recordOffset = -1\n  for (let index = tail.length - minimumRecordSize; index >= 0; index--) {\n    if (tail[index] === 0x50 && tail[index + 1] === 0x4b && tail[index + 2] === 0x05 && tail[index + 3] === 0x06) {\n      const candidateView = new DataView(tail.buffer, tail.byteOffset + index, tail.length - index)\n      const commentLength = candidateView.getUint16(20, true)\n      if (index + minimumRecordSize + commentLength === tail.length) {\n        recordOffset = index\n        break\n      }\n    }\n  }\n  if (recordOffset < 0) throw new Error('ZIP archive is missing its central directory')\n  const view = new DataView(tail.buffer, tail.byteOffset + recordOffset, tail.length - recordOffset)\n  const centralDirectorySize = view.getUint32(12, true)\n  const centralDirectoryOffset = view.getUint32(16, true)\n  if (centralDirectoryOffset + centralDirectorySize > file.size - (tail.length - recordOffset)) {\n    throw new Error('ZIP archive central directory is invalid')\n  }\n}\n\nfunction combineChunks(chunks: Uint8Array[], totalSize: number): Uint8Array {\n  const output = new Uint8Array(totalSize)\n  let offset = 0\n  for (const chunk of chunks) {\n    output.set(chunk, offset)\n    offset += chunk.length\n  }\n  return output\n}\n\nexport async function readZipFileEntries(\n  file: File,\n  onEntry: (entry: ReadZipEntry) => Promise<void> | void,\n  options: ZipReadOptions = {}\n): Promise<void> {","sourceCodeStart":175,"sourceCodeEnd":211,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/renderer/packages/backup/zip.ts#L175-L211","documentation":"Thrown by validateZipEndOfCentralDirectory after it locates the End-of-Central-Directory (EOCD) signature. The EOCD record advertises a central directory byte offset (getUint32 at 16) and size (getUint32 at 12); if their sum exceeds the file's remaining content space (file.size minus the tail region holding the EOCD), the archive's structural metadata is internally inconsistent and the file is rejected before any entry is read.","triggerScenarios":"Reproduces when a .zip is truncated mid-central-directory, when bytes are appended or stripped after creation (e.g. an outer container's footer shifts offsets), when a download finished early, or when the file is not actually a zip but happens to contain an EOCD signature.","commonSituations":"Restoring a backup that was only partially uploaded/downloaded, importing a file exported by a different tool that wrapped the zip, disk-full writes that cut the file short, or manual binary patching that invalidated offsets.","solutions":["Re-export or re-download the backup file and retry; truncation/corruption is the dominant cause.","Verify integrity against the source (checksum/size) before calling readZipFileEntries.","Confirm the file is genuinely a zip (PK\\x03\\x04 local-file header at offset 0), not a sibling format that embedded a zip.","If you control the producer, rewrite it so no bytes are appended after the EOCD."],"exampleFix":"// before: trusting an arbitrary blob\nawait readZipFileEntries(file, onEntry)\n\n// after: pre-flight structural sanity\nconst head = new Uint8Array(await file.slice(0, 4).arrayBuffer())\nif (file.size < 22 || head[0] !== 0x50 || head[1] !== 0x4b) {\n  throw new Error('Not a valid ZIP file')\n}\nawait readZipFileEntries(file, onEntry)","handlingStrategy":"try-catch","validationCode":"// Best-effort pre-flight: real signature + minimum size.\nasync function looksLikeValidZip(file: File): Promise<boolean> {\n  if (file.size < 22) return false\n  const head = new Uint8Array(await file.slice(0, 4).arrayBuffer())\n  return head[0] === 0x50 && head[1] === 0x4b && head[2] === 0x03 && head[3] === 0x04\n}\n\nif (!(await looksLikeValidZip(file))) {\n  throw new Error('File is not a valid ZIP archive')\n}","typeGuard":null,"tryCatchPattern":"try {\n  await readZipFileEntries(file, onEntry, { signal })\n} catch (error) {\n  if (error instanceof Error && /central directory is invalid|missing its central directory|truncated/.test(error.message)) {\n    // structural corruption: do not retry the same bytes; ask for a fresh export\n    reportToUser('The backup file is corrupted. Please re-export and try again.')\n  } else {\n    throw error\n  }\n}","preventionTips":["Always validate the source/export integrity (checksum, expected size) before importing.","Wrap readZipFileEntries in try/catch and treat structural errors as user-facing 're-export needed' messages.","Do not retry the same corrupted bytes; the fix is a fresh file, not a retry."],"tags":["zip","backup","validation","file-corruption","security"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}