{"record":{"id":"06ef64d008ae85a0","repo":"can1357/oh-my-pi","slug":"archive-member-data-is-truncated","errorCode":null,"errorMessage":"Archive member data is truncated","messagePattern":"Archive member data is truncated","errorType":"exception","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/tar.ts","lineNumber":456,"sourceCode":"\t\t\tsawTerminator = true;\n\t\t\tbreak;\n\t\t}\n\t\tif (!checksumMatches(buffer, offset)) throw new ArchiveError(\"Invalid or corrupt tar archive header\");\n\t\tconst headerOffset = offset;\n\t\tconst typeFlag = String.fromCharCode(buffer[headerOffset + TYPEFLAG_OFFSET] || 0x30);\n\t\tlet size = readTarSize(buffer, headerOffset + SIZE_OFFSET);\n\t\tlet name = readTarString(buffer, headerOffset + NAME_OFFSET, NAME_LENGTH);\n\t\tif (isUstarHeader(buffer, headerOffset)) {\n\t\t\tconst prefix = readTarString(buffer, headerOffset + PREFIX_OFFSET, PREFIX_LENGTH);\n\t\t\tif (prefix) name = `${prefix}/${name}`;\n\t\t}\n\t\tlet linkName = readTarString(buffer, headerOffset + LINKNAME_OFFSET, LINKNAME_LENGTH);\n\t\tconst mtime = readTarNumeric(buffer, headerOffset + MTIME_OFFSET, MTIME_LENGTH);\n\t\tconst rawMode = readTarNumeric(buffer, headerOffset + MODE_OFFSET, MODE_LENGTH);\n\t\tconst mode = Number.isSafeInteger(rawMode) && rawMode >= 0 ? rawMode : undefined;\n\t\toffset += BLOCK_SIZE;\n\t\tconst dataBlocks = paddedSize(size);\n\t\tif (dataBlocks > buffer.byteLength - offset) throw new ArchiveError(\"Archive member data is truncated\");\n\t\tconst data = buffer.subarray(offset, offset + size);\n\n\t\tif (typeFlag === \"L\") {\n\t\t\tassertIndexSize(data.byteLength, limits, \"GNU long-name metadata\");\n\t\t\tlongName = readMetadataPath(data, \"GNU long path\", limits);\n\t\t\toffset += dataBlocks;\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeFlag === \"K\") {\n\t\t\tassertIndexSize(data.byteLength, limits, \"GNU long-link metadata\");\n\t\t\tlongLink = readMetadataPath(data, \"GNU long link target\", limits);\n\t\t\toffset += dataBlocks;\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeFlag === \"N\") {\n\t\t\tapplyOldGnuNameRecords(data, entries, pendingLinks, limits);\n\t\t\toffset += dataBlocks;\n\t\t\tcontinue;","sourceCodeStart":438,"sourceCodeEnd":474,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/tar.ts#L438-L474","documentation":"Thrown by readTarEntriesFromBuffer in packages/utils/src/ar/tar.ts after parsing a tar header when the member's declared size (padded to block alignment) extends past the end of the buffer. The header claims more data blocks than the byte array actually contains, so the library refuses to index the entry rather than return partial data. This is a data-integrity guard: it means the archive bytes are incomplete, not a bug in the caller's options.","triggerScenarios":"Passing a Uint8Array/Buffer to readTarEntriesFromBuffer (or a stream that readTar fully buffered) where a member header's octal/PAX size field points beyond buffer.byteLength — e.g. a partially downloaded .tar, a mid-file truncation, or reading a multi-volume tar with only one volume's bytes.","commonSituations":"Interrupted downloads (gzip finished but tar tail missing), splitting a tar file with `head -c`, appending to a tar while it is still being written, or downloading from an S3/HTTP range request that cut the file short.","solutions":["Re-download or re-copy the archive and compare file sizes/checksums against the source.","Extract the underlying file completely into memory before calling (check that the decompressed stream reached EOF, not just that read() returned).","Validate the tar ends with the expected two zero blocks; a buffer ending mid-member is truncated.","If the source is a multi-volume or split tar, concatenate all parts (cat part1 part2 ...) before parsing.","Wrap the call in try-catch on ArchiveError and surface 'archive is incomplete/corrupt' to the user instead of retrying parse."],"exampleFix":"// before: parse whatever bytes arrived\nconst buf = new Uint8Array(await partialResponse.arrayBuffer());\nconst entries = readTarEntriesFromBuffer(buf, options);\n// after: verify completeness first\nconst buf = new Uint8Array(await response.arrayBuffer());\nif (response.headers.get('content-length') && buf.byteLength !== Number(response.headers.get('content-length'))) {\n  throw new Error('download incomplete, not a parse error');\n}\nconst entries = readTarEntriesFromBuffer(buf, options);","handlingStrategy":"validation","validationCode":"// verify archive completeness before parsing\nconst bytes = new Uint8Array(await Bun.file(archivePath).arrayBuffer());\nif (bytes.byteLength < 1024 || bytes.byteLength % 512 !== 0) {\n  throw new Error(`archive size ${bytes.byteLength} looks truncated`);\n}\nconst entries = readTarEntriesFromBuffer(bytes, { limits });","typeGuard":null,"tryCatchPattern":"try {\n  const entries = readTarEntriesFromBuffer(bytes, { limits });\n} catch (err) {\n  if (err instanceof ArchiveError && /truncated/i.test(err.message)) {\n    throw new Error('Archive is incomplete or corrupt; re-download it');\n  }\n  throw err;\n}","preventionTips":["Compare downloaded size against Content-Length or a published checksum before parsing.","Wait for the decompression stream to fully end before buffering tar bytes.","Never split tar archives with head/dd/tail; use multi-archive tooling instead.","Fail fast on ArchiveError — the bytes are corrupt, retrying parse cannot succeed."],"tags":["tar","archive","truncation","data-integrity"],"backgroundTag":"truncated-archive","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}