{"record":{"id":"339cfba3adc057dc","repo":"can1357/oh-my-pi","slug":"archive-member-formatarchivepathforerror-name","errorCode":null,"errorMessage":"Archive member '${formatArchivePathForError(name)}' is truncated","messagePattern":"Archive member '(.+?)' is truncated","errorType":"exception","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/tar.ts","lineNumber":515,"sourceCode":"\t\tif (paxSparseName !== undefined) name = paxSparseName;\n\t\tlet displaySize = size;\n\t\tconst paxSparseRealSize = paxAttribute(globalPax, localPax, \"GNU.sparse.realsize\");\n\t\tif (paxSparseRealSize !== undefined) displaySize = parsePaxSize(paxSparseRealSize, \"sparse real size\");\n\t\tconst sparse = typeFlag === \"S\" || paxDeclaresSparse(globalPax, localPax);\n\t\tif (typeFlag === \"S\" && buffer[headerOffset + GNU_SPARSE_ISEXTENDED_OFFSET] === 1) {\n\t\t\tlet extended = true;\n\t\t\twhile (extended) {\n\t\t\t\tif (offset + BLOCK_SIZE > buffer.byteLength) {\n\t\t\t\t\tthrow new ArchiveError(\"Archive sparse metadata is truncated\");\n\t\t\t\t}\n\t\t\t\textended = buffer[offset + GNU_SPARSE_CONT_ISEXTENDED_OFFSET] === 1;\n\t\t\t\toffset += BLOCK_SIZE;\n\t\t\t}\n\t\t}\n\t\tconst dataOffset = offset;\n\t\tconst memberDataBlocks = paddedSize(size);\n\t\tif (memberDataBlocks > buffer.byteLength - dataOffset) {\n\t\t\tthrow new ArchiveError(`Archive member '${formatArchivePathForError(name)}' is truncated`);\n\t\t}\n\t\toffset += memberDataBlocks;\n\t\tlongName = undefined;\n\t\tlongLink = undefined;\n\t\tlocalPax = undefined;\n\n\t\tconst isDirectory = typeFlag === \"5\" || name.endsWith(\"/\");\n\t\tconst normalizedPath = normalizeArchiveEntryPath(name);\n\t\tif (!normalizedPath) continue;\n\t\tassertArchivePathString(normalizedPath, \"member path\", limits.maxPathBytes);\n\t\tconst scaledMtime = mtime * 1000;\n\t\tconst mtimeMs = mtime !== 0 && Number.isSafeInteger(scaledMtime) ? scaledMtime : undefined;\n\t\tif (isDirectory) {\n\t\t\taddEntry({ path: normalizedPath, isDirectory: true, size: 0, mtimeMs, mode });\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeFlag === \"1\" || typeFlag === \"2\") {\n\t\t\tconst kind = typeFlag === \"1\" ? \"hard link\" : \"symlink\";","sourceCodeStart":497,"sourceCodeEnd":533,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/tar.ts#L497-L533","documentation":"Thrown after all extended-name/PAX/sparse metadata has been consumed, when the final member's padded data size (memberDataBlocks) extends past the end of the buffer. Unlike the generic truncation check at line 456 (which runs before metadata handling), this check uses the size after PAX 'size' record overrides, so it specifically catches members whose true size — possibly corrected by a PAX header — overruns the buffer. It names the offending member path to pinpoint the corruption.","triggerScenarios":"Calling readTarEntriesFromBuffer on a buffer where a member's data (per header size or PAX size record) is not fully present — truncated download, PAX size header disagreeing with actual bytes, or reading one slice of a split archive.","commonSituations":"Downloads interrupted mid-file; tar files modified by tools that rewrote size fields; extracting only the first volume; build caches that wrote a tar without flushing the final member.","solutions":["Re-download/re-copy the archive and verify checksum against the publisher.","Check the archive with `tar -tf file.tar` — if GNU tar also complains, the file is corrupt upstream.","Ensure the full decompressed stream was captured (stream not ended early, gunzip not killed by SIGPIPE).","If a PAX-producing tool wrote wrong size records, regenerate the archive with a different tar implementation.","Catch ArchiveError and treat it as a corrupt-archive condition (fail fast; do not retry parsing the same bytes)."],"exampleFix":"// before\nconst buf = await readSomeBytes(fd, partialLength);\nreturn readTarEntriesFromBuffer(buf, options);\n// after\nconst stat = await fs.stat(path);\nconst expected = manifest['archive.tar'].size;\nif (stat.size !== expected) throw new Error(`archive incomplete: ${stat.size}/${expected} bytes`);\nreturn readTarEntriesFromBuffer(new Uint8Array(await Bun.file(path).arrayBuffer()), options);","handlingStrategy":"try-catch","validationCode":"// sanity-check that the buffer plausibly terminates with zero blocks before parsing\nfunction hasTerminator(bytes) {\n  const tail = bytes.subarray(Math.max(0, bytes.byteLength - 1024));\n  return tail.length >= 1024 && tail.every((b) => b === 0);\n}\nif (!hasTerminator(bytes)) throw new Error('archive appears cut short');","typeGuard":null,"tryCatchPattern":"try {\n  const entries = readTarEntriesFromBuffer(bytes, { limits });\n} catch (err) {\n  if (err instanceof ArchiveError) {\n    const member = /'([^']+)'/.exec(err.message)?.[1];\n    throw new Error(`Corrupt archive: member ${member ?? '?'} data is missing — re-fetch the archive`);\n  }\n  throw err;\n}","preventionTips":["Capture the entire decompressed stream (await full read; never kill the pipe early).","Validate archive hash/size against the publisher before parsing.","Don't edit tar size fields or hand-splice member bytes.","Log the member name from the error message to identify the corrupt producer."],"tags":["tar","truncation","pax","archive"],"backgroundTag":"truncated-archive","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}