Commit 1e0167d7d60 for nodejs
commit 1e0167d7d60b3b3de5be4a80d164fd4bf990acd3
Author: Xia Chao <shapirolutts@gmail.com>
Date: Sat Sep 26 11:28:05 2026 -0700
zlib: preserve brotli params and dictionary on reset
ResetStream currently calls Init() with no arguments, which drops
the stored dictionary and never replays SetParams. Remember each
successful parameter and replay both the dictionary and the params
when the stream is reset.
Fixes: https://github.com/nodejs/node/issues/66156
Signed-off-by: Xia Chao <shapirolutts@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66157
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
diff --git a/doc/api/zlib.md b/doc/api/zlib.md
index 72f8223acdb..cef9e4b7937 100644
--- a/doc/api/zlib.md
+++ b/doc/api/zlib.md
@@ -2189,11 +2189,18 @@ Only applicable to deflate algorithm.
<!-- YAML
added: v0.7.0
+changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/66157
+ description: Brotli streams preserve parameters and dictionary on reset.
-->
For inflate and deflate streams, reset the compressor/decompressor to factory
defaults.
+For Brotli streams, start a new compression or decompression session while
+preserving the configured parameters and dictionary.
+
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.
diff --git a/src/node_zlib.cc b/src/node_zlib.cc
index 82a88bd98f7..3880a933a9a 100644
--- a/src/node_zlib.cc
+++ b/src/node_zlib.cc
@@ -52,6 +52,8 @@
#include <cstdlib>
#include <cstring>
#include <optional>
+#include <utility>
+#include <vector>
namespace node {
@@ -178,6 +180,18 @@ struct CompressionError {
inline bool IsError() const { return code != nullptr; }
};
+void RecordBrotliParam(std::vector<std::pair<int, uint32_t>>* params,
+ int key,
+ uint32_t value) {
+ for (auto& entry : *params) {
+ if (entry.first == key) {
+ entry.second = value;
+ return;
+ }
+ }
+ params->emplace_back(key, value);
+}
+
class ZlibContext final : public MemoryRetainer {
public:
ZlibContext() = default;
@@ -286,6 +300,8 @@ class BrotliEncoderContext final : public BrotliContext {
prepared_dictionary_;
// Dictionary data must remain valid while the prepared dictionary is alive.
std::vector<uint8_t> dictionary_;
+ // Last successful parameters, replayed by ResetStream.
+ std::vector<std::pair<int, uint32_t>> params_;
};
class BrotliDecoderContext final : public BrotliContext {
@@ -308,6 +324,8 @@ class BrotliDecoderContext final : public BrotliContext {
DeleteFnPtr<BrotliDecoderState, BrotliDecoderDestroyInstance> state_;
// Dictionary data must remain valid for the lifetime of the decoder.
std::vector<uint8_t> dictionary_;
+ // Last successful parameters, replayed by ResetStream.
+ std::vector<std::pair<int, uint32_t>> params_;
};
class ZstdContext : public MemoryRetainer {
@@ -1505,6 +1523,7 @@ void BrotliEncoderContext::Close() {
state_.reset();
prepared_dictionary_.reset();
dictionary_.clear();
+ params_.clear();
mode_ = NONE;
}
@@ -1518,6 +1537,7 @@ CompressionError BrotliEncoderContext::Init(std::vector<uint8_t>&& dictionary) {
// Clean up any previous dictionary state before re-initializing.
prepared_dictionary_.reset();
dictionary_.clear();
+ params_.clear();
state_.reset(BrotliEncoderCreateInstance(alloc, free, opaque));
if (!state_) {
@@ -1557,7 +1577,19 @@ CompressionError BrotliEncoderContext::Init(std::vector<uint8_t>&& dictionary) {
}
CompressionError BrotliEncoderContext::ResetStream() {
- return Init();
+ std::vector<uint8_t> dictionary = dictionary_;
+ const auto params = params_;
+ CompressionError err = Init(std::move(dictionary));
+ if (err.IsError()) {
+ return err;
+ }
+ for (const auto& entry : params) {
+ err = SetParams(entry.first, entry.second);
+ if (err.IsError()) {
+ return err;
+ }
+ }
+ return CompressionError{};
}
CompressionError BrotliEncoderContext::SetParams(int key, uint32_t value) {
@@ -1568,6 +1600,7 @@ CompressionError BrotliEncoderContext::SetParams(int key, uint32_t value) {
"ERR_BROTLI_PARAM_SET_FAILED",
-1);
} else {
+ RecordBrotliParam(¶ms_, key, value);
return CompressionError {};
}
}
@@ -1586,6 +1619,7 @@ CompressionError BrotliEncoderContext::GetErrorInfo() const {
void BrotliDecoderContext::Close() {
state_.reset();
dictionary_.clear();
+ params_.clear();
mode_ = NONE;
}
@@ -1615,6 +1649,7 @@ CompressionError BrotliDecoderContext::Init(std::vector<uint8_t>&& dictionary) {
// Clean up any previous dictionary state before re-initializing.
dictionary_.clear();
+ params_.clear();
state_.reset(BrotliDecoderCreateInstance(alloc, free, opaque));
if (!state_) {
@@ -1642,7 +1677,19 @@ CompressionError BrotliDecoderContext::Init(std::vector<uint8_t>&& dictionary) {
}
CompressionError BrotliDecoderContext::ResetStream() {
- return Init();
+ std::vector<uint8_t> dictionary = dictionary_;
+ const auto params = params_;
+ CompressionError err = Init(std::move(dictionary));
+ if (err.IsError()) {
+ return err;
+ }
+ for (const auto& entry : params) {
+ err = SetParams(entry.first, entry.second);
+ if (err.IsError()) {
+ return err;
+ }
+ }
+ return CompressionError{};
}
CompressionError BrotliDecoderContext::SetParams(int key, uint32_t value) {
@@ -1653,6 +1700,7 @@ CompressionError BrotliDecoderContext::SetParams(int key, uint32_t value) {
"ERR_BROTLI_PARAM_SET_FAILED",
-1);
} else {
+ RecordBrotliParam(¶ms_, key, value);
return CompressionError {};
}
}
diff --git a/test/parallel/test-zlib-brotli-reset.js b/test/parallel/test-zlib-brotli-reset.js
new file mode 100644
index 00000000000..6641648c89d
--- /dev/null
+++ b/test/parallel/test-zlib-brotli-reset.js
@@ -0,0 +1,48 @@
+'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('BrotliCompress reset preserves its initial options', async () => {
+ const options = {
+ dictionary,
+ params: {
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 0,
+ },
+ };
+ const expected = await collect(zlib.createBrotliCompress(options), input);
+ const reset = zlib.createBrotliCompress(options);
+ reset.reset();
+
+ assert.deepStrictEqual(await collect(reset, input), expected);
+});
+
+test('BrotliDecompress reset preserves its dictionary', async () => {
+ const compressed = zlib.brotliCompressSync(input, { dictionary });
+ const decompress = zlib.createBrotliDecompress({ dictionary });
+ decompress.reset();
+
+ assert.deepStrictEqual(await collect(decompress, compressed), input);
+});