{"record":{"id":"a09a512bf2f4e2d7","repo":"can1357/oh-my-pi","slug":"invalid-lzh-lzs-data-output-exceeds-declared-si","errorCode":null,"errorMessage":"Invalid LZH -lzs- data: output exceeds declared size","messagePattern":"Invalid LZH -lzs- data: output exceeds declared size","errorType":"exception","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/lzh.ts","lineNumber":253,"sourceCode":"\t\t\tthrow new ArchiveError(`Invalid ${label} compressed data: history distance is out of range`);\n\t\t}\n\t\tlet sourcePosition = outputPosition - distance - 1;\n\t\tfor (let index = 0; index < length; index++) output[outputPosition++] = output[sourcePosition++]!;\n\t}\n\tif (blockRemaining !== 0) throw new ArchiveError(`Invalid ${label} compressed data: block exceeds declared size`);\n\treader.assertZeroPadding();\n\treturn output;\n}\n\nfunction decompressLzs(packed: Uint8Array, outSize: number): Uint8Array {\n\tconst reader = new MsbBitReader(packed, \"LZH -lzs-\");\n\tconst output = new Uint8Array(outSize);\n\tconst history = new Uint8Array(2048);\n\thistory.fill(0x20);\n\tlet historyPosition = 2048 - 17;\n\tlet outputPosition = 0;\n\tconst emit = (value: number): void => {\n\t\tif (outputPosition >= outSize) throw new ArchiveError(\"Invalid LZH -lzs- data: output exceeds declared size\");\n\t\toutput[outputPosition++] = value;\n\t\thistory[historyPosition] = value;\n\t\thistoryPosition = (historyPosition + 1) & 2047;\n\t};\n\twhile (outputPosition < outSize) {\n\t\tif (reader.read(1) !== 0) {\n\t\t\temit(reader.read(8));\n\t\t} else {\n\t\t\tconst position = reader.read(11);\n\t\t\tconst length = reader.read(4) + 2;\n\t\t\tif (length > outSize - outputPosition)\n\t\t\t\tthrow new ArchiveError(\"Invalid LZH -lzs- data: match exceeds declared size\");\n\t\t\tfor (let index = 0; index < length; index++) emit(history[(position + index) & 2047]!);\n\t\t}\n\t}\n\treader.assertZeroPadding();\n\treturn output;\n}","sourceCodeStart":235,"sourceCodeEnd":271,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/lzh.ts#L235-L271","documentation":"decompressLzs's emit helper writes literal bytes; if the stream tries to emit more bytes than the declared outSize, the library refuses to overflow the fixed-size output buffer.","triggerScenarios":"decompressLzs (via the LZH -lzs- read path) on a stream with more literals than outSize allows — corrupt data, or outSize taken from a header smaller than the actual member size (wrong field/offset, damaged extended header).","commonSituations":"-lzs- members whose size field was edited; reading the wrong header size field (old 16-bit size vs 32-bit); fuzzed inputs.","solutions":["Use the correct (32-bit / extended) original-size field for the member","Re-download/validate the archive (CRC)","Catch ArchiveError and report the member as corrupt"],"exampleFix":"// before\nconst outSize = header.readUInt16LE(13); // 16-bit field truncates large sizes\n// after\nconst outSize = u64(header, 16); // full size field incl. extended header","handlingStrategy":"try-catch","validationCode":"if (outSize <= 0 || !Number.isSafeInteger(outSize)) throw new Error(\"invalid -lzs- output size\");","typeGuard":null,"tryCatchPattern":"try {\n  const out = decompressLzs(packed, outSize);\n} catch (err) {\n  if (err instanceof ArchiveError) return null;\n  throw err;\n}","preventionTips":["Use the 32-bit/extended size field for -lzs- members, not the legacy 16-bit one","Verify archive checksums","Treat member size fields as untrusted"],"tags":["archive","corruption","lzs","buffer-overflow"],"backgroundTag":"corrupt-archive-data","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}