{"record":{"id":"4265d1aff9e0d016","repo":"can1357/oh-my-pi","slug":"invalid-zip-archive-truncated-central-directory","errorCode":null,"errorMessage":"Invalid ZIP archive: truncated central directory","messagePattern":"Invalid ZIP archive: truncated central directory","errorType":"exception","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/zip.ts","lineNumber":211,"sourceCode":"\tlet offset = readUInt32LE(tail, eocdIndex + 16);\n\tif (\n\t\tdisk !== 0 ||\n\t\tcentralDisk !== 0 ||\n\t\t(entriesOnDisk !== U16_MAX && entries !== U16_MAX && entriesOnDisk !== entries)\n\t) {\n\t\tthrow new ArchiveError(\"Multi-volume ZIP archives are not supported\");\n\t}\n\tconst needsZip64 = entriesOnDisk === U16_MAX || entries === U16_MAX || size === U32_MAX || offset === U32_MAX;\n\tconst zip64 = await readZip64Info(source, tail, tailStart, eocdOffset);\n\tlet physicalEnd = eocdOffset;\n\tif (zip64) {\n\t\t({ entries, size, offset, physicalEnd } = zip64);\n\t} else if (needsZip64) {\n\t\tthrow new ArchiveError(\"Invalid ZIP archive: missing ZIP64 central-directory metadata\");\n\t}\n\tassertEntryCount(entries, limits);\n\tassertIndexSize(size, limits, \"ZIP central directory\");\n\tif (entries > Math.floor(size / 46)) throw new ArchiveError(\"Invalid ZIP archive: truncated central directory\");\n\tconst declaredOffset = offset;\n\tconst info = { entries, size, offset, physicalEnd, archiveOffset: 0 };\n\tinfo.offset = await locateCentralDirectory(source, info);\n\tinfo.archiveOffset = info.offset - declaredOffset;\n\tcheckedEnd(info.offset, info.size, source.size, \"central directory\");\n\treturn info;\n}\n\nfunction filetimeToMs(bytes: Uint8Array, offset: number): number | undefined {\n\tlet value = 0n;\n\tfor (let index = 7; index >= 0; index--) value = (value << 8n) | BigInt(bytes[offset + index]!);\n\tconst milliseconds = value / 10_000n - WINDOWS_EPOCH_FILETIME_MS;\n\tconst number = Number(milliseconds);\n\treturn Number.isSafeInteger(number) ? number : undefined;\n}\n\nfunction parseNtfsMtime(data: Uint8Array): number | undefined {\n\tif (data.byteLength < 4) return undefined;","sourceCodeStart":193,"sourceCodeEnd":229,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/zip.ts#L193-L229","documentation":"Thrown by readCentralDirectoryInfo when the declared number of central-directory entries exceeds what the declared directory size can physically hold (each central header is at least 46 bytes). The EOCD/ZIP64 metadata is internally inconsistent — the directory would have to be truncated to contain that many entries.","triggerScenarios":"Calling zip info/read on an archive where the entry count or central-directory size fields were corrupted or written incorrectly, e.g. count > floor(size/46).","commonSituations":"Bit-level corruption of a large archive (flipped bytes in the EOCD); a buggy writer updating one field but not the other; a truncated transfer that happened to preserve a valid-looking EOCD elsewhere in the tail.","solutions":["Test the archive with `unzip -t archive.zip` to confirm external agreement on corruption.","Repair with `zip -FF broken.zip --out fixed.zip`.","Re-download or regenerate the archive from its source files.","Verify integrity at transfer time (checksum the archive after download) so corruption is caught before reading."],"exampleFix":"// before: trusting the transfer implicitly\nawait download(url, path);\nconst zip = readZip(path);\n// after: verify checksums before parsing\nawait download(url, path);\nconst ok = await Bun.password; // pseudo — verify via sha256\nif (sha256(await Bun.file(path).arrayBuffer()) !== expectedSha) throw new Error(\"archive corrupted in transit\");\nconst zip = readZip(path);","handlingStrategy":"validation","validationCode":"// Cheap structural sanity check before parsing: EOCD signature in the last 64KB.\nconst tail = new Uint8Array(await Bun.file(path).slice(-65557).arrayBuffer());\nconst eo = findLastIndex4LE(tail, 0x06054b50); // scan for PK\\x05\\x06\nif (eo < 0) throw new Error(\"no EOCD signature; archive is corrupt or not a zip\");","typeGuard":null,"tryCatchPattern":"try {\n  const zip = readZip(path);\n} catch (err) {\n  if (err instanceof ArchiveError && err.message.includes(\"truncated central directory\")) {\n    // EOCD fields are internally inconsistent -> treat as corrupt input\n    await $`zip -FF ${path} --out fixed.zip`.nothrow();\n    return readZip(\"fixed.zip\");\n  }\n  throw err;\n}","preventionTips":["Verify archive checksums after any transfer to catch bit corruption.","Treat repeated structural-zip failures from one producer as a writer bug and switch tools.","Don't hand-edit zip binary fields; always re-zip via a standard tool.","Keep a repair step (`zip -FF`) in ingestion pipelines for untrusted archives."],"tags":["zip","archive","corruption","central-directory"],"backgroundTag":"zip-central-directory-truncated","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}