zstd.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. /**
  2. * Zstandard frame primitives for the JSONL persistence backend. The backend
  3. * owns a concatenated-frame container so it can append and recover batches
  4. * without exposing compression mechanics through the persistence seam.
  5. * @module dsh-session-persistence-jsonl/zstd
  6. */
  7. import {
  8. constants, zstdCompress, zstdDecompress, type ZstdOptions,
  9. } from 'node:zlib'
  10. import { promisify } from 'node:util'
  11. import { NodePrivateZstdFrameDecoder } from './zstd-private-decoder.ts'
  12. import { PublicZstdFrameDecoder } from './zstd-public-decoder.ts'
  13. const ZSTD_MAGIC = 0xFD2FB528
  14. const zstdCompressAsync = promisify(zstdCompress)
  15. const zstdDecompressAsync = promisify(zstdDecompress)
  16. const CHECKSUM_OPTIONS: ZstdOptions = {
  17. params: { [constants.ZSTD_c_checksumFlag]: 1 },
  18. }
  19. const INCOMPLETE_FRAME_OPTIONS: ZstdOptions = {
  20. finishFlush: constants.ZSTD_e_flush,
  21. }
  22. /** Byte range occupied by one structurally complete Zstandard frame. */
  23. export interface ZstdFrameRange {
  24. /** Inclusive frame start. */
  25. start: number
  26. /** Exclusive frame end. */
  27. end: number
  28. }
  29. /** Structural scan result for a concatenated Zstandard stream. */
  30. export interface ZstdFrameScan {
  31. /** Complete frames in file order. */
  32. frames: ZstdFrameRange[]
  33. /** Start of an incomplete final frame, when EOF interrupts one. */
  34. tornStart?: number
  35. }
  36. /**
  37. * Locate complete frames without decompressing their blocks. Invalid complete
  38. * structure rejects; EOF inside the final frame returns its start for repair.
  39. * @param buffer - complete bytes currently present in the session artifact.
  40. * @param maxFrames - optional complete-frame limit for metadata-only readers.
  41. * @returns complete frame ranges and an optional incomplete-final-frame start.
  42. */
  43. export function scanZstdFrames(buffer: Buffer, maxFrames = Number.POSITIVE_INFINITY): ZstdFrameScan {
  44. const frames: ZstdFrameRange[] = []
  45. let offset = 0
  46. while (offset < buffer.length) {
  47. const start = offset
  48. if (buffer.length - offset < 4) return { frames, tornStart: start }
  49. if (buffer.readUInt32LE(offset) !== ZSTD_MAGIC) {
  50. throw new Error(`corrupt Zstandard session log: invalid frame magic at byte ${offset}`)
  51. }
  52. offset += 4
  53. if (offset === buffer.length) return { frames, tornStart: start }
  54. const descriptor = buffer.readUInt8(offset)
  55. offset += 1
  56. if ((descriptor & 0x18) !== 0) {
  57. throw new Error(`corrupt Zstandard session log: reserved frame-header bit at byte ${offset - 1}`)
  58. }
  59. const contentSizeFlag = descriptor >>> 6
  60. const singleSegment = (descriptor & 0x20) !== 0
  61. const checksum = (descriptor & 0x04) !== 0
  62. const dictionaryFlag = descriptor & 0x03
  63. const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag
  64. const contentSizeBytes = contentSizeFlag === 0
  65. ? (singleSegment ? 1 : 0)
  66. : 1 << contentSizeFlag
  67. const remainingHeaderBytes = (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes
  68. if (buffer.length - offset < remainingHeaderBytes) return { frames, tornStart: start }
  69. offset += remainingHeaderBytes
  70. for (;;) {
  71. if (buffer.length - offset < 3) return { frames, tornStart: start }
  72. const blockHeader = buffer.readUIntLE(offset, 3)
  73. offset += 3
  74. const lastBlock = (blockHeader & 1) !== 0
  75. const blockType = (blockHeader >>> 1) & 0x03
  76. const blockSize = blockHeader >>> 3
  77. if (blockType === 0x03) {
  78. throw new Error(`corrupt Zstandard session log: reserved block type at byte ${offset - 3}`)
  79. }
  80. const payloadBytes = blockType === 0x01 ? 1 : blockSize
  81. if (buffer.length - offset < payloadBytes) return { frames, tornStart: start }
  82. offset += payloadBytes
  83. if (lastBlock) break
  84. }
  85. if (checksum) {
  86. if (buffer.length - offset < 4) return { frames, tornStart: start }
  87. offset += 4
  88. }
  89. frames.push({ start, end: offset })
  90. if (frames.length === maxFrames) return { frames }
  91. }
  92. return { frames }
  93. }
  94. /**
  95. * Compress one independently decodable, checksummed Zstandard frame.
  96. * @param input - JSONL bytes for a header or durable event batch.
  97. * @returns the complete encoded frame.
  98. */
  99. export async function compressZstdFrame(input: Buffer | string): Promise<Buffer> {
  100. return zstdCompressAsync(input, CHECKSUM_OPTIONS)
  101. }
  102. /**
  103. * Decompress one complete frame and validate its checksum.
  104. * @param input - one structurally complete Zstandard frame.
  105. * @returns the frame plaintext.
  106. */
  107. export async function decompressZstdFrame(input: Buffer): Promise<Buffer> {
  108. return zstdDecompressAsync(input)
  109. }
  110. /** Common lifecycle for interchangeable synchronous multi-frame decoders. */
  111. export interface ZstdFrameDecoder {
  112. /**
  113. * Decode and checksum complete frames in source order. Each yielded buffer
  114. * remains valid only until the iterator advances to the next frame.
  115. * @param source - concatenated Zstandard frame bytes.
  116. * @param frames - structurally complete ranges within `source`.
  117. * @returns one plaintext buffer per frame.
  118. */
  119. decode(source: Buffer, frames: readonly ZstdFrameRange[]): Generator<Buffer, void, void>
  120. /** Release decoder-owned resources; repeated calls are harmless. */
  121. close(): void
  122. }
  123. /**
  124. * Select the shared private decoder when the running Node 22/24/26 shape is
  125. * compatible, otherwise preserve correctness with the public one-shot API.
  126. * @returns a synchronous decoder with an implementation-independent lifecycle.
  127. */
  128. export function createZstdFrameDecoder(): ZstdFrameDecoder {
  129. return NodePrivateZstdFrameDecoder.create() ?? new PublicZstdFrameDecoder()
  130. }
  131. /**
  132. * Recover available plaintext from a structurally incomplete final frame.
  133. * `ZSTD_e_flush` deliberately suppresses final-frame and checksum completion;
  134. * callers must establish the torn frame boundary before using this helper.
  135. * @param input - available bytes from a known incomplete Zstandard frame.
  136. * @returns plaintext produced from the available input.
  137. */
  138. export async function decompressZstdPrefix(input: Buffer): Promise<Buffer> {
  139. return zstdDecompressAsync(input, INCOMPLETE_FRAME_OPTIONS)
  140. }