{"record":{"id":"c5922ac0bc1b69b1","repo":"can1357/oh-my-pi","slug":"archive-sparse-metadata-is-truncated","errorCode":null,"errorMessage":"Archive sparse metadata is truncated","messagePattern":"Archive sparse metadata is truncated","errorType":"exception","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/tar.ts","lineNumber":506,"sourceCode":"\t\tif (longName !== undefined) name = longName;\n\t\tif (longLink !== undefined) linkName = longLink;\n\t\tconst paxPath = paxAttribute(globalPax, localPax, \"path\");\n\t\tif (paxPath !== undefined) name = paxPath;\n\t\tconst paxLinkPath = paxAttribute(globalPax, localPax, \"linkpath\");\n\t\tif (paxLinkPath !== undefined) linkName = paxLinkPath;\n\t\tconst paxSize = paxAttribute(globalPax, localPax, \"size\");\n\t\tif (paxSize !== undefined) size = parsePaxSize(paxSize, \"member size\");\n\t\tconst paxSparseName = paxAttribute(globalPax, localPax, \"GNU.sparse.name\");\n\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;","sourceCodeStart":488,"sourceCodeEnd":524,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/tar.ts#L488-L524","documentation":"Thrown while parsing a GNU sparse member (type flag 'S'): when the sparse-map 'isextended' byte is set, the header is followed by continuation blocks listing sparse extents, and this error fires when the buffer ends before the continuation chain completes. It means the archive's sparse metadata is cut off mid-chain, so the member's real data offset can't be located. Like the other truncation guards, it indicates incomplete archive bytes rather than bad options.","triggerScenarios":"Parsing a tar created with GNU `tar -S` (sparse) where the buffer ends inside the sparse-extension block chain — e.g. a truncated download or single volume of a sparse multi-volume archive.","commonSituations":"Sparse backups of VM disks, database files, or docker layer blobs (large files with holes) that were incompletely transferred; cutting a sparse tar mid-file with head/dd.","solutions":["Re-transfer the archive and verify size/hash against the source.","Confirm the decompression step completed (gzip/brotli EOF) before buffering for parse.","Extract with GNU tar first (`tar -xSf`) or repack non-sparsely (`tar -cf`) if you must hand the bytes to this parser.","Do not truncate/split sparse tar archives by byte count; keep the whole file."],"exampleFix":"// before: buffer may be a partial chunk\nconst buf = chunks[0];\nconst entries = readTarEntriesFromBuffer(buf, options);\n// after: only parse the fully received archive\nconst buf = new Uint8Array(await Bun.file(path).arrayBuffer());\nconst entries = readTarEntriesFromBuffer(buf, options);","handlingStrategy":"validation","validationCode":"// require the whole file (sparse tars must be complete) before parsing\nconst stat = await fs.stat(archivePath);\nif (stat.size < (expectedSize ?? 0)) throw new Error(`sparse archive incomplete: ${stat.size} < ${expectedSize}`);\nconst bytes = new Uint8Array(await Bun.file(archivePath).arrayBuffer());\nconst entries = readTarEntriesFromBuffer(bytes, { limits });","typeGuard":null,"tryCatchPattern":"try {\n  const entries = readTarEntriesFromBuffer(bytes, { limits });\n} catch (err) {\n  if (err instanceof ArchiveError && /sparse metadata is truncated/.test(err.message)) {\n    throw new Error('Sparse tar archive is incomplete; re-transfer without truncating');\n  }\n  throw err;\n}","preventionTips":["Avoid byte-level truncation of sparse tars (head/dd/split).","Verify checksums before parsing sparse archives.","Prefer transferring with resumable protocols (rsync, ranged HTTP) so partial files are detected.","If only metadata is needed, consider repacking non-sparsely upstream."],"tags":["tar","gnu-sparse","truncation","archive"],"backgroundTag":"truncated-archive","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}