{"record":{"id":"775bd93ed4451547","repo":"can1357/oh-my-pi","slug":"invalid-lzh-archive-truncated-data","errorCode":null,"errorMessage":"Invalid LZH archive: truncated data","messagePattern":"Invalid LZH archive: truncated data","errorType":"validation","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/lzh.ts","lineNumber":612,"sourceCode":"\tif (bytes.byteLength < 22 || bytes[2] !== 0x2d || bytes[6] !== 0x2d || bytes[3] !== 0x6c) return false;\n\tconst method = String.fromCharCode(...bytes.subarray(2, 7));\n\treturn LHA_METHOD_PATTERN.test(method) && bytes[20]! <= 2;\n}\n\n/** Index an LZH/LHA archive and lazily decode its members from the bounded archive buffer. */\nexport const readLzh: FormatReader = async (\n\tsource: ByteSource,\n\toptions: FormatReadOptions,\n): Promise<ArchiveIndexEntry[]> => {\n\tassertInMemorySize(source.size, options.limits);\n\tlet bytes: Uint8Array;\n\ttry {\n\t\tbytes = await readAllBytes(source);\n\t} catch (error) {\n\t\tif (error instanceof ArchiveError) throw error;\n\t\tthrow new ArchiveError(`Unable to read LZH archive: ${error instanceof Error ? error.message : String(error)}`);\n\t}\n\tif (bytes.byteLength !== source.size) throw new ArchiveError(\"Invalid LZH archive: truncated data\");\n\tif (!sniffLzh(bytes)) throw new ArchiveError(\"Invalid LZH archive header\");\n\tconst entries: ArchiveIndexEntry[] = [];\n\tlet offset = 0;\n\tlet parsedCount = 0;\n\tlet metadataSize = 0;\n\twhile (offset < bytes.byteLength && bytes[offset] !== 0) {\n\t\tconst header = parseLzhHeader(bytes, offset, options);\n\t\tmetadataSize += header.dataStart - offset;\n\t\tassertIndexSize(metadataSize, options.limits, \"index\");\n\t\tassertEntryCount(++parsedCount, options.limits);\n\t\tif (header.nextOffset <= offset) throw new ArchiveError(\"Invalid LZH archive: header did not advance\");\n\t\toffset = header.nextOffset;\n\t\tif (!header.path) continue;\n\t\tconst isDirectory = header.method === \"-lhd-\";\n\t\tif (isDirectory && header.mode !== undefined && (header.mode & 0xf000) === 0xa000) {\n\t\t\tconst separator = header.path.indexOf(\"|\");\n\t\t\tif (separator < 1) throw new ArchiveError(`Invalid LZH symbolic link '${header.path}'`);\n\t\t\tconst path = normalizeArchiveEntryPath(header.path.slice(0, separator));","sourceCodeStart":594,"sourceCodeEnd":630,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/lzh.ts#L594-L630","documentation":"After reading all bytes, the library verifies the buffer length equals the ByteSource's declared size. A mismatch means the source shrank or reported a wrong size — the archive is incomplete relative to its own metadata, so parsing would read out of bounds.","triggerScenarios":"readLzh() where readAllBytes() returns fewer bytes than source.size: file truncated during download/copy, concurrent writer shrinking the file, ByteSource.size stale after the file changed, partial transfer.","commonSituations":"Interrupted downloads; rsync/ftp copy cut short; another process rewriting the .lzh while it is being indexed; custom ByteSource built from a stale stat().","solutions":["Re-download or re-copy the archive and confirm the size matches the origin.","Rebuild the ByteSource from a fresh stat so size matches current file length.","Avoid reading archives while they are still being written; wait for the producer to finish (e.g. tmpfile + rename pattern).","Verify the archive's integrity with a checksum before indexing."],"exampleFix":"// before: size captured earlier, file changed since\nconst source = makeByteSource(path, staleSize);\nconst entries = await readLzh(source, options);\n// after: refresh size immediately before reading\nconst size = (await fs.stat(path)).size;\nconst source = makeByteSource(path, size);\nconst entries = await readLzh(source, options);","handlingStrategy":"validation","validationCode":"const st = await fs.stat(path);\nconst bytes = await readAllBytes(source);\nif (bytes.length !== st.size) throw new Error(\"file changed or truncated during read — retry with a fresh stat\");","typeGuard":null,"tryCatchPattern":"try {\n  const entries = await readLzh(source, options);\n} catch (err) {\n  if (err instanceof ArchiveError && err.message === \"Invalid LZH archive: truncated data\") {\n    throw new Error(\"archive incomplete — re-download or wait for the writer to finish\");\n  }\n  throw err;\n}","preventionTips":["Write archives to a temp file and atomically rename when complete","Never index archives while a producer is still writing them","Rebuild the ByteSource from a fresh stat immediately before reading"],"tags":["archive","lzh","truncated-file","io"],"backgroundTag":"truncated-file","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}