{"record":{"id":"4e45f7ba008cbd57","repo":"can1357/oh-my-pi","slug":"invalid-archive-truncated-data","errorCode":null,"errorMessage":"Invalid archive: truncated data","messagePattern":"Invalid archive: truncated data","errorType":"exception","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/source.ts","lineNumber":25,"sourceCode":" * only read when a member is actually extracted.\n */\nexport interface ByteSource {\n\treadonly size: number;\n\tread(start: number, end: number): Promise<Uint8Array>;\n}\n\n/** Reject a nonsensical `[start, end)` range before any read. */\nexport function assertValidRange(start: number, end: number): void {\n\tif (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start) {\n\t\tthrow new ArchiveError(\"Invalid archive range\");\n\t}\n}\n\n/** Read an exact in-memory range, throwing (not clamping) when it runs past the buffer. */\nexport function readMemoryRange(buffer: Uint8Array, start: number, end: number): Uint8Array {\n\tassertValidRange(start, end);\n\tif (end > buffer.byteLength) {\n\t\tthrow new ArchiveError(\"Invalid archive: truncated data\");\n\t}\n\treturn buffer.subarray(start, end);\n}\n\n/** Wrap borrowed bytes as a {@link ByteSource}. */\nexport function memoryByteSource(buffer: Uint8Array): ByteSource {\n\treturn {\n\t\tsize: buffer.byteLength,\n\t\tasync read(start, end) {\n\t\t\treturn readMemoryRange(buffer, start, end);\n\t\t},\n\t};\n}\n\n/** Lazily read ranges of a file on disk as a {@link ByteSource}. */\nexport function fileByteSource(filePath: string): ByteSource {\n\tconst file = Bun.file(filePath);\n\tconst size = file.size;","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/source.ts#L7-L43","documentation":"`readMemoryRange` performs an exact in-memory read: if the requested end exceeds the buffer's byte length, the archive is truncated relative to what its own metadata claims, so it throws instead of silently clamping to a short slice. This protects parsers from operating on short reads that would produce garbage structures.","triggerScenarios":"Calling `readMemoryRange`/`read` (via `memoryByteSource`) with end > buffer.byteLength — e.g. an archive header declares an entry at offset X with size Y but the actual buffer is shorter, or the caller computed a range past EOF.","commonSituations":"Parsing a truncated download or copy where the file was cut short; archive header offsets from a corrupt/tooled index pointing past EOF; reading a small fixture with ranges copied from a larger archive.","solutions":["Check the buffer length against the archive's declared total size before parsing","Re-download or re-extract the file — truncation is the usual root cause","Validate header-declared offsets+sizes against `source.size` before reading ranges"],"exampleFix":"// before: reading without checking declared size\nconst bytes = readMemoryRange(buf, offset, offset + size);\n// after: bounds-check against the buffer first\nif (offset + size > buf.byteLength) throw new Error(\"archive truncated: header claims more data than present\");\nconst bytes = readMemoryRange(buf, offset, offset + size);","handlingStrategy":"validation","validationCode":"if (end > buffer.byteLength) throw new Error(`need [${start}, ${end}) but buffer is ${buffer.byteLength} bytes`);","typeGuard":"function fitsBuffer(buffer: Uint8Array, start: number, end: number): boolean {\n\treturn end <= buffer.byteLength;\n}","tryCatchPattern":"try {\n\tconst bytes = readMemoryRange(buf, start, end);\n} catch (err) {\n\tif (err instanceof ArchiveError && err.message.includes(\"truncated data\")) {\n\t\tthrow new Error(`archive truncated: wanted ${end} bytes, buffer has ${buf.byteLength}`);\n\t}\n\tthrow err;\n}","preventionTips":["Verify the full file was downloaded/copied (compare against expected size or checksum)","Bounds-check header-declared offsets and sizes against the buffer length","Do not parse archives still being written or transferred"],"tags":["archive","truncated-data","validation"],"backgroundTag":"truncated-archive","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}