{"record":{"id":"aae8f61661fcc0b8","repo":"chatboxai/chatbox","slug":"zip-archive-is-truncated","errorCode":null,"errorMessage":"ZIP archive is truncated","messagePattern":"ZIP archive is truncated","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/renderer/packages/backup/zip.ts","lineNumber":174,"sourceCode":"    }\n  } finally {\n    signal?.removeEventListener('abort', onAbort)\n    zip.terminate()\n    notifyProducer(true)\n  }\n}\n\nexport interface ReadZipEntry {\n  path: string\n  data: Uint8Array\n  compressedSize?: number\n  uncompressedSize: number\n}\n\nasync function validateZipEndOfCentralDirectory(file: File) {\n  const minimumRecordSize = 22\n  const maximumCommentSize = 65_535\n  if (file.size < minimumRecordSize) throw new Error('ZIP archive is truncated')\n  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)) {","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/renderer/packages/backup/zip.ts#L156-L192","documentation":"Thrown by validateZipEndOfCentralDirectory when the input file is smaller than 22 bytes — the minimum size of a ZIP End Of Central Directory record. A file that small cannot be a valid ZIP, so it is rejected before any parsing begins.","triggerScenarios":"readZipFileEntries is called with a File whose size < 22 (e.g. an empty file, a 0-byte stub, or a tiny non-zip blob).","commonSituations":"User selects an empty or incompletely-downloaded file; a truncated upload; the wrong file type picked in the dialog.","solutions":["Check file.size >= 22 (and realistically >= a few hundred bytes) before calling readZipFileEntries.","Re-download or re-export the backup.","Show a file-size/type guard in the import UI."],"exampleFix":"// before\nawait readZipFileEntries(file, onEntry, options)\n\n// after\nconst MIN_ZIP = 22\nif (file.size < MIN_ZIP) throw new Error('File is too small to be a ZIP archive')\nawait readZipFileEntries(file, onEntry, options)","handlingStrategy":"validation","validationCode":"const MIN_ZIP_EOCD_BYTES = 22\nfunction isLargeEnoughToBeZip(file: File): boolean {\n  return file.size >= MIN_ZIP_EOCD_BYTES\n}","typeGuard":null,"tryCatchPattern":"try {\n  await readZipFileEntries(file, onEntry, options)\n} catch (error) {\n  if (/truncated/.test((error as Error).message)) notifyUser('Backup file is incomplete')\n  throw error\n}","preventionTips":["Show a minimum-size guard in the import UI.","Confirm downloads completed (compare size to Content-Length) before importing.","Reject 0-byte files at the file picker."],"tags":["zip","validation","file-size"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}