Skip to content

Commit 47c7ab8

Browse files
committed
zlib: add zstd support
Fixes #48412
1 parent 085f01b commit 47c7ab8

14 files changed

+534
-16
lines changed

benchmark/zlib/creation.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const zlib = require('zlib');
55
const bench = common.createBenchmark(main, {
66
type: [
77
'Deflate', 'DeflateRaw', 'Inflate', 'InflateRaw', 'Gzip', 'Gunzip', 'Unzip',
8-
'BrotliCompress', 'BrotliDecompress',
8+
'BrotliCompress', 'BrotliDecompress', 'ZstdCompress', 'ZstdDecompress',
99
],
1010
options: ['true', 'false'],
1111
n: [5e5],

benchmark/zlib/pipe.js

+10-5
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,27 @@ const bench = common.createBenchmark(main, {
77
inputLen: [1024],
88
duration: [5],
99
type: ['string', 'buffer'],
10-
algorithm: ['gzip', 'brotli'],
10+
algorithm: ['gzip', 'brotli', 'zstd'],
1111
}, {
1212
test: {
1313
inputLen: 1024,
1414
duration: 0.2,
1515
},
1616
});
1717

18+
const algorithms = {
19+
'gzip': [zlib.createGzip, zlib.createGunzip],
20+
'brotli': [zlib.createBrotliCompress, zlib.createBrotliDecompress],
21+
'zstd': [zlib.createZstdCompress, zlib.createZstdDecompress],
22+
};
23+
1824
function main({ inputLen, duration, type, algorithm }) {
1925
const buffer = Buffer.alloc(inputLen, fs.readFileSync(__filename));
2026
const chunk = type === 'buffer' ? buffer : buffer.toString('utf8');
2127

22-
const input = algorithm === 'gzip' ?
23-
zlib.createGzip() : zlib.createBrotliCompress();
24-
const output = algorithm === 'gzip' ?
25-
zlib.createGunzip() : zlib.createBrotliDecompress();
28+
const [createCompress, createUncompress] = algorithms[algorithm];
29+
const input = createCompress();
30+
const output = createUncompress();
2631

2732
let readFromOutput = 0;
2833
input.pipe(output);

doc/api/zlib.md

+58-3
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@ operations be cached to avoid duplication of effort.
124124

125125
## Compressing HTTP requests and responses
126126

127-
The `node:zlib` module can be used to implement support for the `gzip`, `deflate`
128-
and `br` content-encoding mechanisms defined by
127+
The `node:zlib` module can be used to implement support for the `gzip`, `deflate`,
128+
`br` and `zstd` content-encoding mechanisms defined by
129129
[HTTP](https://tools.ietf.org/html/rfc7230#section-4.2).
130130

131131
The HTTP [`Accept-Encoding`][] header is used within an HTTP request to identify
@@ -148,7 +148,7 @@ const { pipeline } = require('node:stream');
148148
const request = http.get({ host: 'example.com',
149149
path: '/',
150150
port: 80,
151-
headers: { 'Accept-Encoding': 'br,gzip,deflate' } });
151+
headers: { 'Accept-Encoding': 'br,gzip,deflate,zstd' } });
152152
request.on('response', (response) => {
153153
const output = fs.createWriteStream('example.com_index.html');
154154

@@ -170,6 +170,9 @@ request.on('response', (response) => {
170170
case 'deflate':
171171
pipeline(response, zlib.createInflate(), output, onError);
172172
break;
173+
case 'zstd':
174+
pipeline(response, zlib.createZstdDecompress(), output, onError);
175+
break;
173176
default:
174177
pipeline(response, output, onError);
175178
break;
@@ -218,6 +221,9 @@ http.createServer((request, response) => {
218221
} else if (/\bbr\b/.test(acceptEncoding)) {
219222
response.writeHead(200, { 'Content-Encoding': 'br' });
220223
pipeline(raw, zlib.createBrotliCompress(), response, onError);
224+
} else if (/\bzstd\b/.test(acceptEncoding)) {
225+
response.writeHead(200, { 'Content-Encoding': 'zstd' });
226+
pipeline(raw, zlib.createZstdCompress(), response, onError);
221227
} else {
222228
response.writeHead(200, {});
223229
pipeline(raw, response, onError);
@@ -238,6 +244,7 @@ const buffer = Buffer.from('eJzT0yMA', 'base64');
238244
zlib.unzip(
239245
buffer,
240246
// For Brotli, the equivalent is zlib.constants.BROTLI_OPERATION_FLUSH.
247+
// For Zstd, the equivalent is zlib.constants.ZSTD_e_flush.
241248
{ finishFlush: zlib.constants.Z_SYNC_FLUSH },
242249
(err, buffer) => {
243250
if (err) {
@@ -309,6 +316,16 @@ these options have different ranges than the zlib ones:
309316

310317
See [below][Brotli parameters] for more details on Brotli-specific options.
311318

319+
### For Zstd-based streams
320+
321+
There are equivalents to the zlib options for Zstd-based streams, although
322+
these options have different ranges than the zlib ones:
323+
324+
* zlib's `level` option matches Zstd's `ZSTD_c_compressionLevel` option.
325+
* zlib's `windowBits` option matches Zstd's `ZSTD_c_windowLog` option.
326+
327+
See [below][Zstd parameters] for more details on Zstd-specific options.
328+
312329
## Flushing
313330

314331
Calling [`.flush()`][] on a compression stream will make `zlib` return as much
@@ -487,6 +504,44 @@ These advanced options are available for controlling decompression:
487504
* Boolean flag enabling “Large Window Brotli” mode (not compatible with the
488505
Brotli format as standardized in [RFC 7932][]).
489506

507+
### Zstd constants
508+
509+
<!-- YAML
510+
added: REPLACEME
511+
-->
512+
513+
There are several options and other constants available for Zstd-based
514+
streams:
515+
516+
#### Flush operations
517+
518+
The following values are valid flush operations for Zstd-based streams:
519+
520+
* `zlib.constants.ZSTD_e_continue` (default for all operations)
521+
* `zlib.constants.ZSTD_e_flush` (default when calling `.flush()`)
522+
* `zlib.constants.ZSTD_e_end` (default for the last chunk)
523+
524+
#### Compressor options
525+
526+
There are several options that can be set on Zstd encoders, affecting
527+
compression efficiency and speed. Both the keys and the values can be accessed
528+
as properties of the `zlib.constants` object.
529+
530+
The most important options are:
531+
532+
* `ZSTD_c_compressionLevel`
533+
* Set compression parameters according to pre-defined cLevel table. Default
534+
level is ZSTD_CLEVEL_DEFAULT==3.
535+
536+
#### Decompressor options
537+
538+
These advanced options are available for controlling decompression:
539+
540+
* `ZSTD_d_windowLogMax`
541+
* Select a size limit (in power of 2) beyond which the streaming API will
542+
refuse to allocate memory buffer in order to protect the host from
543+
unreasonable memory requirements.
544+
490545
## Class: `Options`
491546

492547
<!-- YAML

lib/internal/errors.js

+1
Original file line numberDiff line numberDiff line change
@@ -1917,3 +1917,4 @@ E('ERR_WORKER_UNSERIALIZABLE_ERROR',
19171917
E('ERR_WORKER_UNSUPPORTED_OPERATION',
19181918
'%s is not supported in workers', TypeError);
19191919
E('ERR_ZLIB_INITIALIZATION_FAILED', 'Initialization failed', Error);
1920+
E('ERR_ZSTD_INVALID_PARAM', '%s is not a valid zstd parameter', RangeError);

lib/zlib.js

+91-4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const {
4949
ERR_INVALID_ARG_TYPE,
5050
ERR_OUT_OF_RANGE,
5151
ERR_ZLIB_INITIALIZATION_FAILED,
52+
ERR_ZSTD_INVALID_PARAM,
5253
},
5354
genericNodeError,
5455
hideStackFrames,
@@ -88,9 +89,12 @@ const {
8889
// Node's compression stream modes (node_zlib_mode)
8990
DEFLATE, DEFLATERAW, INFLATE, INFLATERAW, GZIP, GUNZIP, UNZIP,
9091
BROTLI_DECODE, BROTLI_ENCODE,
92+
ZSTD_COMPRESS, ZSTD_DECOMPRESS,
9193
// Brotli operations (~flush levels)
9294
BROTLI_OPERATION_PROCESS, BROTLI_OPERATION_FLUSH,
9395
BROTLI_OPERATION_FINISH, BROTLI_OPERATION_EMIT_METADATA,
96+
// Zstd end directives (~flush levels)
97+
ZSTD_e_continue, ZSTD_e_flush, ZSTD_e_end,
9498
} = constants;
9599

96100
// Translation table for return codes.
@@ -237,9 +241,11 @@ const checkRangesOrGetDefault = hideStackFrames(
237241
const FLUSH_BOUND = [
238242
[ Z_NO_FLUSH, Z_BLOCK ],
239243
[ BROTLI_OPERATION_PROCESS, BROTLI_OPERATION_EMIT_METADATA ],
244+
[ ZSTD_e_continue, ZSTD_e_end ],
240245
];
241246
const FLUSH_BOUND_IDX_NORMAL = 0;
242247
const FLUSH_BOUND_IDX_BROTLI = 1;
248+
const FLUSH_BOUND_IDX_ZSTD = 2;
243249

244250
// The base class for all Zlib-style streams.
245251
function ZlibBase(opts, mode, handle, { flush, finishFlush, fullFlush }) {
@@ -248,13 +254,15 @@ function ZlibBase(opts, mode, handle, { flush, finishFlush, fullFlush }) {
248254
// The ZlibBase class is not exported to user land, the mode should only be
249255
// passed in by us.
250256
assert(typeof mode === 'number');
251-
assert(mode >= DEFLATE && mode <= BROTLI_ENCODE);
257+
assert(mode >= DEFLATE && mode <= ZSTD_DECOMPRESS);
252258

253259
let flushBoundIdx;
254-
if (mode !== BROTLI_ENCODE && mode !== BROTLI_DECODE) {
255-
flushBoundIdx = FLUSH_BOUND_IDX_NORMAL;
256-
} else {
260+
if (mode === BROTLI_ENCODE || mode === BROTLI_DECODE) {
257261
flushBoundIdx = FLUSH_BOUND_IDX_BROTLI;
262+
} else if (mode === ZSTD_COMPRESS || mode === ZSTD_DECOMPRESS) {
263+
flushBoundIdx = FLUSH_BOUND_IDX_ZSTD;
264+
} else {
265+
flushBoundIdx = FLUSH_BOUND_IDX_NORMAL;
258266
}
259267

260268
if (opts) {
@@ -888,6 +896,77 @@ ObjectSetPrototypeOf(BrotliDecompress.prototype, Brotli.prototype);
888896
ObjectSetPrototypeOf(BrotliDecompress, Brotli);
889897

890898

899+
const kMaxZstdParam = MathMaxApply(ArrayPrototypeMap(
900+
ObjectKeys(constants),
901+
(key) => (StringPrototypeStartsWith(key, 'ZSTD_c_') ?
902+
constants[key] :
903+
0),
904+
));
905+
906+
const zstdInitParamsArray = new Uint32Array(kMaxZstdParam + 1);
907+
908+
const zstdDefaultOpts = {
909+
flush: ZSTD_e_continue,
910+
finishFlush: ZSTD_e_end,
911+
fullFlush: ZSTD_e_flush,
912+
};
913+
function Zstd(opts, mode) {
914+
assert(mode === ZSTD_COMPRESS || mode === ZSTD_DECOMPRESS);
915+
916+
TypedArrayPrototypeFill(zstdInitParamsArray, -1);
917+
if (opts?.params) {
918+
ArrayPrototypeForEach(ObjectKeys(opts.params), (origKey) => {
919+
const key = +origKey;
920+
if (NumberIsNaN(key) || key < 0 || key > kMaxZstdParam ||
921+
(zstdInitParamsArray[key] | 0) !== -1) {
922+
throw new ERR_ZSTD_INVALID_PARAM(origKey);
923+
}
924+
925+
const value = opts.params[origKey];
926+
if (typeof value !== 'number' && typeof value !== 'boolean') {
927+
throw new ERR_INVALID_ARG_TYPE('options.params[key]',
928+
'number', opts.params[origKey]);
929+
}
930+
zstdInitParamsArray[key] = value;
931+
});
932+
}
933+
934+
const handle = mode === ZSTD_COMPRESS ?
935+
new binding.ZstdCompress() : new binding.ZstdDecompress();
936+
937+
this._writeState = new Uint32Array(2);
938+
if (!handle.init(zstdInitParamsArray,
939+
this._writeState,
940+
processCallback)) {
941+
throw new ERR_ZLIB_INITIALIZATION_FAILED();
942+
}
943+
944+
ReflectApply(ZlibBase, this, [opts, mode, handle, zstdDefaultOpts]);
945+
}
946+
ObjectSetPrototypeOf(Zstd.prototype, ZlibBase.prototype);
947+
ObjectSetPrototypeOf(Zstd, ZlibBase);
948+
949+
950+
function ZstdCompress(opts) {
951+
if (!(this instanceof ZstdCompress))
952+
return new ZstdCompress(opts);
953+
954+
ReflectApply(Zstd, this, [opts, ZSTD_COMPRESS]);
955+
}
956+
ObjectSetPrototypeOf(ZstdCompress.prototype, Zstd.prototype);
957+
ObjectSetPrototypeOf(ZstdCompress, Zstd);
958+
959+
960+
function ZstdDecompress(opts) {
961+
if (!(this instanceof ZstdDecompress))
962+
return new ZstdDecompress(opts);
963+
964+
ReflectApply(Zstd, this, [opts, ZSTD_DECOMPRESS]);
965+
}
966+
ObjectSetPrototypeOf(ZstdDecompress.prototype, Zstd.prototype);
967+
ObjectSetPrototypeOf(ZstdDecompress, Zstd);
968+
969+
891970
function createProperty(ctor) {
892971
return {
893972
__proto__: null,
@@ -917,6 +996,8 @@ module.exports = {
917996
Unzip,
918997
BrotliCompress,
919998
BrotliDecompress,
999+
ZstdCompress,
1000+
ZstdDecompress,
9201001

9211002
// Convenience methods.
9221003
// compress/decompress a string or buffer in one step.
@@ -938,6 +1019,10 @@ module.exports = {
9381019
brotliCompressSync: createConvenienceMethod(BrotliCompress, true),
9391020
brotliDecompress: createConvenienceMethod(BrotliDecompress, false),
9401021
brotliDecompressSync: createConvenienceMethod(BrotliDecompress, true),
1022+
zstdCompress: createConvenienceMethod(ZstdCompress, false),
1023+
zstdCompressSync: createConvenienceMethod(ZstdCompress, true),
1024+
zstdDecompress: createConvenienceMethod(ZstdDecompress, false),
1025+
zstdDecompressSync: createConvenienceMethod(ZstdDecompress, true),
9411026
};
9421027

9431028
ObjectDefineProperties(module.exports, {
@@ -950,6 +1035,8 @@ ObjectDefineProperties(module.exports, {
9501035
createUnzip: createProperty(Unzip),
9511036
createBrotliCompress: createProperty(BrotliCompress),
9521037
createBrotliDecompress: createProperty(BrotliDecompress),
1038+
createZstdCompress: createProperty(ZstdCompress),
1039+
createZstdDecompress: createProperty(ZstdDecompress),
9531040
constants: {
9541041
__proto__: null,
9551042
configurable: false,

0 commit comments

Comments
 (0)