From 13145acb47230336aa55aa924b533dc3127ccb6b Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 6 Sep 2026 15:27:21 -0700 Subject: [PATCH 1/3] zlib: fix zstd reset Preserve dictionaries and params when zstd is reset. Update missing documentation. Signed-off-by: James M Snell Assisted-by: Opencode --- doc/api/zlib.md | 10 +++- src/node_zlib.cc | 40 ++++++++++++++-- test/parallel/test-zlib-zstd-reset.js | 68 +++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 test/parallel/test-zlib-zstd-reset.js diff --git a/doc/api/zlib.md b/doc/api/zlib.md index 9ab581678e08..1b267a1e02dc 100644 --- a/doc/api/zlib.md +++ b/doc/api/zlib.md @@ -2190,8 +2190,14 @@ Only applicable to deflate algorithm. added: v0.7.0 --> -Reset the compressor/decompressor to factory defaults. Only applicable to -the inflate and deflate algorithms. +For inflate and deflate streams, reset the compressor/decompressor to factory +defaults. + +For Zstd streams, cancel the current frame and start a new session while +preserving the configured parameters and dictionary. If `pledgedSrcSize` was +configured for a Zstd compressor, it applies again to the next frame. + +Calling `reset()` while a write is in progress throws an `Error`. ## Class: `ZstdOptions` diff --git a/src/node_zlib.cc b/src/node_zlib.cc index af82aa2ae73b..c77deafab651 100644 --- a/src/node_zlib.cc +++ b/src/node_zlib.cc @@ -1703,7 +1703,29 @@ CompressionError ZstdCompressContext::Init(uint64_t pledged_src_size, } CompressionError ZstdCompressContext::ResetStream() { - return Init(pledged_src_size_); + size_t result = ZSTD_CCtx_reset(cctx_.get(), ZSTD_reset_session_only); + if (ZSTD_isError(result)) { + const ZSTD_ErrorCode error = ZSTD_getErrorCode(result); + return CompressionError( + ZSTD_getErrorString(error), ZstdStrerror(error), error); + } + + result = ZSTD_CCtx_setPledgedSrcSize(cctx_.get(), pledged_src_size_); + if (ZSTD_isError(result)) { + const ZSTD_ErrorCode error = ZSTD_getErrorCode(result); + return CompressionError( + ZSTD_getErrorString(error), ZstdStrerror(error), error); + } + + if (pledged_src_size_ == ZSTD_CONTENTSIZE_UNKNOWN) { + consumed_src_size_.reset(); + } else { + consumed_src_size_ = 0; + } + error_ = ZSTD_error_no_error; + error_string_.clear(); + error_code_string_.clear(); + return {}; } void ZstdCompressContext::DoThreadPoolWork() { @@ -1777,9 +1799,19 @@ CompressionError ZstdDecompressContext::Init(uint64_t pledged_src_size, } CompressionError ZstdDecompressContext::ResetStream() { - // We pass ZSTD_CONTENTSIZE_UNKNOWN because the argument is ignored for - // decompression. - return Init(ZSTD_CONTENTSIZE_UNKNOWN); + const size_t result = + ZSTD_DCtx_reset(dctx_.get(), ZSTD_reset_session_only); + if (ZSTD_isError(result)) { + const ZSTD_ErrorCode error = ZSTD_getErrorCode(result); + return CompressionError( + ZSTD_getErrorString(error), ZstdStrerror(error), error); + } + + frame_complete_ = false; + error_ = ZSTD_error_no_error; + error_string_.clear(); + error_code_string_.clear(); + return {}; } void ZstdDecompressContext::DoThreadPoolWork() { diff --git a/test/parallel/test-zlib-zstd-reset.js b/test/parallel/test-zlib-zstd-reset.js new file mode 100644 index 000000000000..839669bc630a --- /dev/null +++ b/test/parallel/test-zlib-zstd-reset.js @@ -0,0 +1,68 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { finished } = require('stream/promises'); +const test = require('node:test'); +const zlib = require('zlib'); + +const dictionary = Buffer.from( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + + 'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', +); +const input = Buffer.from( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(100), +); + +async function collect(stream, ...data) { + const chunks = []; + stream.on('data', (chunk) => chunks.push(chunk)); + for (let i = 0; i < data.length - 1; i++) { + stream.write(data[i]); + } + stream.end(data[data.length - 1]); + await finished(stream); + return Buffer.concat(chunks); +} + +test('ZstdCompress reset preserves its initial options', async () => { + const options = { + dictionary, + pledgedSrcSize: input.length, + params: { + [zlib.constants.ZSTD_c_compressionLevel]: 19, + [zlib.constants.ZSTD_c_checksumFlag]: 1, + }, + }; + const expected = await collect(zlib.createZstdCompress(options), input); + const reset = zlib.createZstdCompress(options); + reset.reset(); + + assert.deepStrictEqual(await collect(reset, input), expected); +}); + +test('ZstdDecompress reset preserves its dictionary', async () => { + const compressed = zlib.zstdCompressSync(input, { dictionary }); + const decompress = zlib.createZstdDecompress({ dictionary }); + decompress.reset(); + + assert.deepStrictEqual(await collect(decompress, compressed), input); +}); + +test('ZstdDecompress reset preserves its parameters', async () => { + const compressed = await collect(zlib.createZstdCompress({ + params: { + [zlib.constants.ZSTD_c_windowLog]: 11, + }, + }), Buffer.alloc(2048), Buffer.alloc(2048)); + const decompress = zlib.createZstdDecompress({ + params: { + [zlib.constants.ZSTD_d_windowLogMax]: 10, + }, + }); + decompress.reset(); + + await assert.rejects(collect(decompress, compressed), { + code: 'ZSTD_error_frameParameter_windowTooLarge', + }); +}); From 4491d9dca5784fd65ef9fb0b8fa5de28308cf2ca Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 6 Sep 2026 15:34:06 -0700 Subject: [PATCH 2/3] doc: fill in missing zstd docs Signed-off-by: James M Snell --- doc/api/zlib.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/api/zlib.md b/doc/api/zlib.md index 1b267a1e02dc..a8360825c9b1 100644 --- a/doc/api/zlib.md +++ b/doc/api/zlib.md @@ -2158,7 +2158,8 @@ added: v0.5.8 --> * `kind` **Default:** `zlib.constants.Z_FULL_FLUSH` for zlib-based streams, - `zlib.constants.BROTLI_OPERATION_FLUSH` for Brotli-based streams. + `zlib.constants.BROTLI_OPERATION_FLUSH` for Brotli-based streams, and + `zlib.constants.ZSTD_e_flush` for Zstd-based streams. * `callback` {Function} Flush pending data. Don't call this frivolously, premature flushes negatively @@ -2229,6 +2230,9 @@ Each Zstd-based class takes an `options` object. All options are optional. * `finishFlush` {integer} **Default:** `zlib.constants.ZSTD_e_end` * `chunkSize` {integer} **Default:** `16 * 1024` * `params` {Object} Key-value object containing indexed [Zstd parameters][]. +* `pledgedSrcSize` {number} Expected total size of the uncompressed input. It + must be a non-negative safe integer and must match the input size when + compression finishes. Only applicable to Zstd compressors. * `maxOutputLength` {integer} Limits output size when using [convenience methods][]. **Default:** [`buffer.kMaxLength`][] * `info` {boolean} If `true`, returns an object with `buffer` and `engine`. **Default:** `false` @@ -3068,6 +3072,8 @@ Compress a chunk of data with [`ZstdCompress`][]. ### `zlib.zstdDecompress(buffer[, options], callback)` +> Stability: 1 - Experimental +