Commit 1d3835e7dd5 for nodejs
commit 1d3835e7dd524f2813e5b761be6cd2f20597f8c9
Author: Marco <marco.piraccini@gmail.com>
Date: Thu Oct 29 09:56:52 2026 +0100
zlib: avoid an extra empty zstd frame
Signed-off-by: marcopiraccini <marco.piraccini@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66161
Fixes: https://github.com/nodejs/node/issues/66078
Reviewed-By: Xuguang Mei <meixuguang@gmail.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: James M Snell <jasnell@gmail.com>
diff --git a/src/node_zlib.cc b/src/node_zlib.cc
index 49e2a6e4ddf..c87703800fd 100644
--- a/src/node_zlib.cc
+++ b/src/node_zlib.cc
@@ -385,6 +385,7 @@ class ZstdCompressContext final : public ZstdContext {
uint64_t pledged_src_size_ = ZSTD_CONTENTSIZE_UNKNOWN;
std::optional<uint64_t> consumed_src_size_;
+ bool frame_ended_ = false;
// A frame is complete once ZSTD_compressStream2() has been called with
// ZSTD_e_end and has returned 0. Resetting an incomplete frame is only unsafe
@@ -1787,6 +1788,7 @@ CompressionError ZstdCompressContext::Init(uint64_t pledged_src_size,
std::string_view dictionary,
bool) {
pledged_src_size_ = pledged_src_size;
+ frame_ended_ = false;
frame_complete_ = true;
frame_output_emitted_ = false;
if (pledged_src_size == ZSTD_CONTENTSIZE_UNKNOWN) {
@@ -1859,6 +1861,7 @@ CompressionError ZstdCompressContext::ResetStream() {
} else {
consumed_src_size_ = 0;
}
+ frame_ended_ = false;
frame_complete_ = true;
frame_output_emitted_ = false;
error_ = ZSTD_error_no_error;
@@ -1868,6 +1871,10 @@ CompressionError ZstdCompressContext::ResetStream() {
}
void ZstdCompressContext::DoThreadPoolWork() {
+ // Ending an already ended frame would append an empty one.
+ if (frame_ended_ && input_.size == 0) {
+ return;
+ }
// Zstd overrides a configured pledge when the first call uses ZSTD_e_end.
size_t const input_pos = input_.pos;
size_t const remaining =
@@ -1875,6 +1882,7 @@ void ZstdCompressContext::DoThreadPoolWork() {
if (consumed_src_size_.has_value()) {
*consumed_src_size_ += input_.pos - input_pos;
}
+ frame_ended_ = remaining == 0 && flush_ == ZSTD_e_end;
if (output_.pos > 0) {
frame_output_emitted_ = true;
}
diff --git a/test/parallel/test-zlib-zstd-compress-single-frame.js b/test/parallel/test-zlib-zstd-compress-single-frame.js
new file mode 100644
index 00000000000..4bd222839ca
--- /dev/null
+++ b/test/parallel/test-zlib-zstd-compress-single-frame.js
@@ -0,0 +1,64 @@
+'use strict';
+
+// Regression test for https://github.com/nodejs/node/issues/66078: ending a
+// ZstdCompress stream with writes still queued appended an empty frame.
+
+const common = require('../common');
+const assert = require('assert');
+const { finished } = require('stream/promises');
+const zlib = require('zlib');
+
+const kMagic = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
+
+function countFrames(buffer) {
+ let count = 0;
+ for (let i = buffer.indexOf(kMagic); i !== -1; i = buffer.indexOf(kMagic, i + 1)) {
+ count++;
+ }
+ return count;
+}
+
+async function compress(use) {
+ const stream = zlib.createZstdCompress();
+ const chunks = [];
+ stream.on('data', (chunk) => chunks.push(chunk));
+ await use(stream);
+ stream.end();
+ await finished(stream);
+ return Buffer.concat(chunks);
+}
+
+(async () => {
+ const expected = await compress((stream) => stream.write('hello world'));
+ assert.strictEqual(countFrames(expected), 1);
+
+ assert.deepStrictEqual(await compress((stream) => {
+ stream.write('hello ');
+ stream.write('world');
+ }), expected);
+
+ assert.deepStrictEqual(await compress((stream) => {
+ stream.write('hello world');
+ stream.flush(zlib.constants.ZSTD_e_end);
+ }), expected);
+
+ // The empty flush has to complete before end(), which would otherwise
+ // promote it to ZSTD_e_end.
+ assert.deepStrictEqual(await compress((stream) => {
+ stream.write('hello world');
+ stream.flush(zlib.constants.ZSTD_e_end);
+ return new Promise((resolve) => stream.flush(resolve));
+ }), expected);
+
+ const empty = await compress(() => {});
+ assert.strictEqual(countFrames(empty), 1);
+ assert.strictEqual(zlib.zstdDecompressSync(empty).length, 0);
+
+ const twoFrames = await compress((stream) => {
+ stream.write('hello ');
+ stream.flush(zlib.constants.ZSTD_e_end);
+ stream.write('world');
+ });
+ assert.strictEqual(countFrames(twoFrames), 2);
+ assert.strictEqual(zlib.zstdDecompressSync(twoFrames).toString(), 'hello world');
+})().then(common.mustCall());