From 066e76bf061f5c2d226822ab7206683a2e794a61 Mon Sep 17 00:00:00 2001 From: Jan Krems Date: Fri, 15 Mar 2024 14:54:30 -0700 Subject: [PATCH] zlib: add zstd support Fixes: https://github.com/nodejs/node/issues/48412 PR-URL: https://github.com/nodejs/node/pull/52100 --- benchmark/zlib/creation.js | 2 +- benchmark/zlib/pipe.js | 15 +- doc/api/errors.md | 6 + doc/api/zlib.md | 171 ++++++++- lib/internal/errors.js | 1 + lib/zlib.js | 95 ++++- src/node_zlib.cc | 361 +++++++++++++++++- test/fixtures/person.jpg.zst | Bin 0 -> 45404 bytes test/parallel/test-zlib-bytes-read.js | 1 + .../parallel/test-zlib-convenience-methods.js | 2 + test/parallel/test-zlib-empty-buffer.js | 2 + test/parallel/test-zlib-invalid-input.js | 1 + test/parallel/test-zlib-random-byte-pipes.js | 1 + test/parallel/test-zlib-write-after-flush.js | 1 + test/parallel/test-zlib-zero-byte.js | 9 +- test/parallel/test-zlib-zstd-flush.js | 28 ++ test/parallel/test-zlib-zstd-from-string.js | 38 ++ test/parallel/test-zlib-zstd-from-zstd.js | 34 ++ .../test-zlib-zstd-kmaxlength-rangeerror.js | 29 ++ test/parallel/test-zlib-zstd.js | 94 +++++ test/parallel/test-zlib.js | 1 + tools/doc/type-parser.mjs | 1 + 22 files changed, 876 insertions(+), 17 deletions(-) create mode 100644 test/fixtures/person.jpg.zst create mode 100644 test/parallel/test-zlib-zstd-flush.js create mode 100644 test/parallel/test-zlib-zstd-from-string.js create mode 100644 test/parallel/test-zlib-zstd-from-zstd.js create mode 100644 test/parallel/test-zlib-zstd-kmaxlength-rangeerror.js create mode 100644 test/parallel/test-zlib-zstd.js diff --git a/benchmark/zlib/creation.js b/benchmark/zlib/creation.js index 90b22780d2d312..41b1e4917a67bb 100644 --- a/benchmark/zlib/creation.js +++ b/benchmark/zlib/creation.js @@ -5,7 +5,7 @@ const zlib = require('zlib'); const bench = common.createBenchmark(main, { type: [ 'Deflate', 'DeflateRaw', 'Inflate', 'InflateRaw', 'Gzip', 'Gunzip', 'Unzip', - 'BrotliCompress', 'BrotliDecompress', + 'BrotliCompress', 'BrotliDecompress', 'ZstdCompress', 'ZstdDecompress', ], options: ['true', 'false'], n: [5e5], diff --git a/benchmark/zlib/pipe.js b/benchmark/zlib/pipe.js index a9c86e3de660a9..5a21c3ff417084 100644 --- a/benchmark/zlib/pipe.js +++ b/benchmark/zlib/pipe.js @@ -7,7 +7,7 @@ const bench = common.createBenchmark(main, { inputLen: [1024], duration: [5], type: ['string', 'buffer'], - algorithm: ['gzip', 'brotli'], + algorithm: ['gzip', 'brotli', 'zstd'], }, { test: { inputLen: 1024, @@ -15,14 +15,19 @@ const bench = common.createBenchmark(main, { }, }); +const algorithms = { + 'gzip': [zlib.createGzip, zlib.createGunzip], + 'brotli': [zlib.createBrotliCompress, zlib.createBrotliDecompress], + 'zstd': [zlib.createZstdCompress, zlib.createZstdDecompress], +}; + function main({ inputLen, duration, type, algorithm }) { const buffer = Buffer.alloc(inputLen, fs.readFileSync(__filename)); const chunk = type === 'buffer' ? buffer : buffer.toString('utf8'); - const input = algorithm === 'gzip' ? - zlib.createGzip() : zlib.createBrotliCompress(); - const output = algorithm === 'gzip' ? - zlib.createGunzip() : zlib.createBrotliDecompress(); + const [createCompress, createUncompress] = algorithms[algorithm]; + const input = createCompress(); + const output = createUncompress(); let readFromOutput = 0; input.pipe(output); diff --git a/doc/api/errors.md b/doc/api/errors.md index b0b90b4da87dbf..f61750affc4b41 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -3670,6 +3670,12 @@ removed: v10.0.0 Used when an attempt is made to use a `zlib` object after it has already been closed. + + +### `ERR_ZSTD_INVALID_PARAM` + +An invalid parameter key was passed during construction of a Zstd stream. + ### `ERR_CPU_USAGE` diff --git a/doc/api/zlib.md b/doc/api/zlib.md index a33de3833cbe9f..a504f589561639 100644 --- a/doc/api/zlib.md +++ b/doc/api/zlib.md @@ -124,8 +124,8 @@ operations be cached to avoid duplication of effort. ## Compressing HTTP requests and responses -The `node:zlib` module can be used to implement support for the `gzip`, `deflate` -and `br` content-encoding mechanisms defined by +The `node:zlib` module can be used to implement support for the `gzip`, `deflate`, +`br` and `zstd` content-encoding mechanisms defined by [HTTP](https://tools.ietf.org/html/rfc7230#section-4.2). The HTTP [`Accept-Encoding`][] header is used within an HTTP request to identify @@ -148,7 +148,7 @@ const { pipeline } = require('node:stream'); const request = http.get({ host: 'example.com', path: '/', port: 80, - headers: { 'Accept-Encoding': 'br,gzip,deflate' } }); + headers: { 'Accept-Encoding': 'br,gzip,deflate,zstd' } }); request.on('response', (response) => { const output = fs.createWriteStream('example.com_index.html'); @@ -170,6 +170,9 @@ request.on('response', (response) => { case 'deflate': pipeline(response, zlib.createInflate(), output, onError); break; + case 'zstd': + pipeline(response, zlib.createZstdDecompress(), output, onError); + break; default: pipeline(response, output, onError); break; @@ -218,6 +221,9 @@ http.createServer((request, response) => { } else if (/\bbr\b/.test(acceptEncoding)) { response.writeHead(200, { 'Content-Encoding': 'br' }); pipeline(raw, zlib.createBrotliCompress(), response, onError); + } else if (/\bzstd\b/.test(acceptEncoding)) { + response.writeHead(200, { 'Content-Encoding': 'zstd' }); + pipeline(raw, zlib.createZstdCompress(), response, onError); } else { response.writeHead(200, {}); pipeline(raw, response, onError); @@ -238,6 +244,7 @@ const buffer = Buffer.from('eJzT0yMA', 'base64'); zlib.unzip( buffer, // For Brotli, the equivalent is zlib.constants.BROTLI_OPERATION_FLUSH. + // For Zstd, the equivalent is zlib.constants.ZSTD_e_flush. { finishFlush: zlib.constants.Z_SYNC_FLUSH }, (err, buffer) => { if (err) { @@ -309,6 +316,16 @@ these options have different ranges than the zlib ones: See [below][Brotli parameters] for more details on Brotli-specific options. +### For Zstd-based streams + +There are equivalents to the zlib options for Zstd-based streams, although +these options have different ranges than the zlib ones: + +* zlib's `level` option matches Zstd's `ZSTD_c_compressionLevel` option. +* zlib's `windowBits` option matches Zstd's `ZSTD_c_windowLog` option. + +See [below][Zstd parameters] for more details on Zstd-specific options. + ## Flushing Calling [`.flush()`][] on a compression stream will make `zlib` return as much @@ -487,6 +504,44 @@ These advanced options are available for controlling decompression: * Boolean flag enabling “Large Window Brotli” mode (not compatible with the Brotli format as standardized in [RFC 7932][]). +### Zstd constants + + + +There are several options and other constants available for Zstd-based +streams: + +#### Flush operations + +The following values are valid flush operations for Zstd-based streams: + +* `zlib.constants.ZSTD_e_continue` (default for all operations) +* `zlib.constants.ZSTD_e_flush` (default when calling `.flush()`) +* `zlib.constants.ZSTD_e_end` (default for the last chunk) + +#### Compressor options + +There are several options that can be set on Zstd encoders, affecting +compression efficiency and speed. Both the keys and the values can be accessed +as properties of the `zlib.constants` object. + +The most important options are: + +* `ZSTD_c_compressionLevel` + * Set compression parameters according to pre-defined cLevel table. Default + level is ZSTD\_CLEVEL\_DEFAULT==3. + +#### Decompressor options + +These advanced options are available for controlling decompression: + +* `ZSTD_d_windowLogMax` + * Select a size limit (in power of 2) beyond which the streaming API will + refuse to allocate memory buffer in order to protect the host from + unreasonable memory requirements. + ## Class: `Options` + + + +Each Zstd-based class takes an `options` object. All options are optional. + +* `flush` {integer} **Default:** `zlib.constants.ZSTD_e_continue` +* `finishFlush` {integer} **Default:** `zlib.constants.ZSTD_e_end` +* `chunkSize` {integer} **Default:** `16 * 1024` +* `params` {Object} Key-value object containing indexed [Zstd parameters][]. +* `maxOutputLength` {integer} Limits output size when using + [convenience methods][]. **Default:** [`buffer.kMaxLength`][] + +For example: + +```js +const stream = zlib.createZstdCompress({ + chunkSize: 32 * 1024, + params: { + [zlib.constants.ZSTD_c_compressionLevel]: 10, + [zlib.constants.ZSTD_c_checksumFlag]: 1, + }, +}); +``` + +## Class: `zlib.ZstdCompress` + + + +Compress data using the Zstd algorithm. + +## Class: `zlib.ZstdDecompress` + + + +Decompress data using the Zstd algorithm. + ### `zlib.bytesRead` + +* `options` {zstd options} + +Creates and returns a new [`ZstdCompress`][] object. + +## `zlib.createZstdDecompress([options])` + + + +* `options` {zstd options} + +Creates and returns a new [`ZstdDecompress`][] object. + ## Convenience methods @@ -1220,10 +1340,53 @@ changes: Decompress a chunk of data with [`Unzip`][]. +### `zlib.zstdCompress(buffer[, options], callback)` + + + +* `buffer` {Buffer|TypedArray|DataView|ArrayBuffer|string} +* `options` {zstd options} +* `callback` {Function} + +### `zlib.zstdCompressSync(buffer[, options])` + + + +* `buffer` {Buffer|TypedArray|DataView|ArrayBuffer|string} +* `options` {zstd options} + +Compress a chunk of data with [`ZstdCompress`][]. + +### `zlib.zstdDecompress(buffer[, options], callback)` + + + +* `buffer` {Buffer|TypedArray|DataView|ArrayBuffer|string} +* `options` {zstd options} +* `callback` {Function} + +### `zlib.zstdDecompressSync(buffer[, options])` + + + +* `buffer` {Buffer|TypedArray|DataView|ArrayBuffer|string} +* `options` {zstd options} + +Decompress a chunk of data with [`ZstdDecompress`][]. + [Brotli parameters]: #brotli-constants [Memory usage tuning]: #memory-usage-tuning [RFC 7932]: https://www.rfc-editor.org/rfc/rfc7932.txt [Streams API]: stream.md +[Zstd parameters]: #zstd-constants [`.flush()`]: #zlibflushkind-callback [`Accept-Encoding`]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3 [`ArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer @@ -1240,6 +1403,8 @@ Decompress a chunk of data with [`Unzip`][]. [`Inflate`]: #class-zlibinflate [`TypedArray`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray [`Unzip`]: #class-zlibunzip +[`ZstdCompress`]: #class-zlibzstdcompress +[`ZstdDecompress`]: #class-zlibzstddecompress [`buffer.kMaxLength`]: buffer.md#bufferkmaxlength [`deflateInit2` and `inflateInit2`]: https://zlib.net/manual.html#Advanced [`stream.Transform`]: stream.md#class-streamtransform diff --git a/lib/internal/errors.js b/lib/internal/errors.js index ef4295f9eaaaef..fc0309dc42ce06 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1917,3 +1917,4 @@ E('ERR_WORKER_UNSERIALIZABLE_ERROR', E('ERR_WORKER_UNSUPPORTED_OPERATION', '%s is not supported in workers', TypeError); E('ERR_ZLIB_INITIALIZATION_FAILED', 'Initialization failed', Error); +E('ERR_ZSTD_INVALID_PARAM', '%s is not a valid zstd parameter', RangeError); diff --git a/lib/zlib.js b/lib/zlib.js index 4eaf1976e7d09f..fce2336021a918 100644 --- a/lib/zlib.js +++ b/lib/zlib.js @@ -49,6 +49,7 @@ const { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE, ERR_ZLIB_INITIALIZATION_FAILED, + ERR_ZSTD_INVALID_PARAM, }, genericNodeError, hideStackFrames, @@ -88,9 +89,12 @@ const { // Node's compression stream modes (node_zlib_mode) DEFLATE, DEFLATERAW, INFLATE, INFLATERAW, GZIP, GUNZIP, UNZIP, BROTLI_DECODE, BROTLI_ENCODE, + ZSTD_COMPRESS, ZSTD_DECOMPRESS, // Brotli operations (~flush levels) BROTLI_OPERATION_PROCESS, BROTLI_OPERATION_FLUSH, BROTLI_OPERATION_FINISH, BROTLI_OPERATION_EMIT_METADATA, + // Zstd end directives (~flush levels) + ZSTD_e_continue, ZSTD_e_flush, ZSTD_e_end, } = constants; // Translation table for return codes. @@ -237,9 +241,11 @@ const checkRangesOrGetDefault = hideStackFrames( const FLUSH_BOUND = [ [ Z_NO_FLUSH, Z_BLOCK ], [ BROTLI_OPERATION_PROCESS, BROTLI_OPERATION_EMIT_METADATA ], + [ ZSTD_e_continue, ZSTD_e_end ], ]; const FLUSH_BOUND_IDX_NORMAL = 0; const FLUSH_BOUND_IDX_BROTLI = 1; +const FLUSH_BOUND_IDX_ZSTD = 2; // The base class for all Zlib-style streams. function ZlibBase(opts, mode, handle, { flush, finishFlush, fullFlush }) { @@ -248,13 +254,15 @@ function ZlibBase(opts, mode, handle, { flush, finishFlush, fullFlush }) { // The ZlibBase class is not exported to user land, the mode should only be // passed in by us. assert(typeof mode === 'number'); - assert(mode >= DEFLATE && mode <= BROTLI_ENCODE); + assert(mode >= DEFLATE && mode <= ZSTD_DECOMPRESS); let flushBoundIdx; - if (mode !== BROTLI_ENCODE && mode !== BROTLI_DECODE) { - flushBoundIdx = FLUSH_BOUND_IDX_NORMAL; - } else { + if (mode === BROTLI_ENCODE || mode === BROTLI_DECODE) { flushBoundIdx = FLUSH_BOUND_IDX_BROTLI; + } else if (mode === ZSTD_COMPRESS || mode === ZSTD_DECOMPRESS) { + flushBoundIdx = FLUSH_BOUND_IDX_ZSTD; + } else { + flushBoundIdx = FLUSH_BOUND_IDX_NORMAL; } if (opts) { @@ -888,6 +896,77 @@ ObjectSetPrototypeOf(BrotliDecompress.prototype, Brotli.prototype); ObjectSetPrototypeOf(BrotliDecompress, Brotli); +const kMaxZstdParam = MathMaxApply(ArrayPrototypeMap( + ObjectKeys(constants), + (key) => (StringPrototypeStartsWith(key, 'ZSTD_c_') ? + constants[key] : + 0), +)); + +const zstdInitParamsArray = new Uint32Array(kMaxZstdParam + 1); + +const zstdDefaultOpts = { + flush: ZSTD_e_continue, + finishFlush: ZSTD_e_end, + fullFlush: ZSTD_e_flush, +}; +function Zstd(opts, mode) { + assert(mode === ZSTD_COMPRESS || mode === ZSTD_DECOMPRESS); + + TypedArrayPrototypeFill(zstdInitParamsArray, -1); + if (opts?.params) { + ArrayPrototypeForEach(ObjectKeys(opts.params), (origKey) => { + const key = +origKey; + if (NumberIsNaN(key) || key < 0 || key > kMaxZstdParam || + (zstdInitParamsArray[key] | 0) !== -1) { + throw new ERR_ZSTD_INVALID_PARAM(origKey); + } + + const value = opts.params[origKey]; + if (typeof value !== 'number' && typeof value !== 'boolean') { + throw new ERR_INVALID_ARG_TYPE('options.params[key]', + 'number', opts.params[origKey]); + } + zstdInitParamsArray[key] = value; + }); + } + + const handle = mode === ZSTD_COMPRESS ? + new binding.ZstdCompress() : new binding.ZstdDecompress(); + + this._writeState = new Uint32Array(2); + if (!handle.init(zstdInitParamsArray, + this._writeState, + processCallback)) { + throw new ERR_ZLIB_INITIALIZATION_FAILED(); + } + + ReflectApply(ZlibBase, this, [opts, mode, handle, zstdDefaultOpts]); +} +ObjectSetPrototypeOf(Zstd.prototype, ZlibBase.prototype); +ObjectSetPrototypeOf(Zstd, ZlibBase); + + +function ZstdCompress(opts) { + if (!(this instanceof ZstdCompress)) + return new ZstdCompress(opts); + + ReflectApply(Zstd, this, [opts, ZSTD_COMPRESS]); +} +ObjectSetPrototypeOf(ZstdCompress.prototype, Zstd.prototype); +ObjectSetPrototypeOf(ZstdCompress, Zstd); + + +function ZstdDecompress(opts) { + if (!(this instanceof ZstdDecompress)) + return new ZstdDecompress(opts); + + ReflectApply(Zstd, this, [opts, ZSTD_DECOMPRESS]); +} +ObjectSetPrototypeOf(ZstdDecompress.prototype, Zstd.prototype); +ObjectSetPrototypeOf(ZstdDecompress, Zstd); + + function createProperty(ctor) { return { __proto__: null, @@ -917,6 +996,8 @@ module.exports = { Unzip, BrotliCompress, BrotliDecompress, + ZstdCompress, + ZstdDecompress, // Convenience methods. // compress/decompress a string or buffer in one step. @@ -938,6 +1019,10 @@ module.exports = { brotliCompressSync: createConvenienceMethod(BrotliCompress, true), brotliDecompress: createConvenienceMethod(BrotliDecompress, false), brotliDecompressSync: createConvenienceMethod(BrotliDecompress, true), + zstdCompress: createConvenienceMethod(ZstdCompress, false), + zstdCompressSync: createConvenienceMethod(ZstdCompress, true), + zstdDecompress: createConvenienceMethod(ZstdDecompress, false), + zstdDecompressSync: createConvenienceMethod(ZstdDecompress, true), }; ObjectDefineProperties(module.exports, { @@ -950,6 +1035,8 @@ ObjectDefineProperties(module.exports, { createUnzip: createProperty(Unzip), createBrotliCompress: createProperty(BrotliCompress), createBrotliDecompress: createProperty(BrotliDecompress), + createZstdCompress: createProperty(ZstdCompress), + createZstdDecompress: createProperty(ZstdDecompress), constants: { __proto__: null, configurable: false, diff --git a/src/node_zlib.cc b/src/node_zlib.cc index 0c4ae0fc794347..3c5384d3e812fa 100644 --- a/src/node_zlib.cc +++ b/src/node_zlib.cc @@ -31,9 +31,11 @@ #include "v8.h" -#include "brotli/encode.h" #include "brotli/decode.h" +#include "brotli/encode.h" #include "zlib.h" +#include "zstd.h" +#include "zstd_errors.h" #include @@ -94,6 +96,44 @@ inline const char* ZlibStrerror(int err) { return "Z_UNKNOWN_ERROR"; } +#define ZSTD_ERROR_CODES(V) \ + V(ZSTD_error_no_error) \ + V(ZSTD_error_GENERIC) \ + V(ZSTD_error_prefix_unknown) \ + V(ZSTD_error_version_unsupported) \ + V(ZSTD_error_frameParameter_unsupported) \ + V(ZSTD_error_frameParameter_windowTooLarge) \ + V(ZSTD_error_corruption_detected) \ + V(ZSTD_error_checksum_wrong) \ + V(ZSTD_error_literals_headerWrong) \ + V(ZSTD_error_dictionary_corrupted) \ + V(ZSTD_error_dictionary_wrong) \ + V(ZSTD_error_dictionaryCreation_failed) \ + V(ZSTD_error_parameter_unsupported) \ + V(ZSTD_error_parameter_combination_unsupported) \ + V(ZSTD_error_parameter_outOfBound) \ + V(ZSTD_error_tableLog_tooLarge) \ + V(ZSTD_error_maxSymbolValue_tooLarge) \ + V(ZSTD_error_maxSymbolValue_tooSmall) \ + V(ZSTD_error_stabilityCondition_notRespected) \ + V(ZSTD_error_stage_wrong) \ + V(ZSTD_error_init_missing) \ + V(ZSTD_error_memory_allocation) \ + V(ZSTD_error_workSpace_tooSmall) \ + V(ZSTD_error_dstSize_tooSmall) \ + V(ZSTD_error_srcSize_wrong) \ + V(ZSTD_error_dstBuffer_null) \ + V(ZSTD_error_noForwardProgress_destFull) \ + V(ZSTD_error_noForwardProgress_inputEmpty) + +inline const char* ZstdStrerror(int err) { +#define V(code) \ + if (err == code) return #code; + ZSTD_ERROR_CODES(V) +#undef V + return "ZSTD_error_GENERIC"; +} + enum node_zlib_mode { NONE, DEFLATE, @@ -104,7 +144,9 @@ enum node_zlib_mode { INFLATERAW, UNZIP, BROTLI_DECODE, - BROTLI_ENCODE + BROTLI_ENCODE, + ZSTD_COMPRESS, + ZSTD_DECOMPRESS }; constexpr uint8_t GZIP_HEADER_ID1 = 0x1f; @@ -248,6 +290,80 @@ class BrotliDecoderContext final : public BrotliContext { DeleteFnPtr state_; }; +class ZstdContext : public MemoryRetainer { + public: + ZstdContext() = default; + + // Streaming-related, should be available for all compression libraries: + void Close(); + void SetBuffers(const char* in, uint32_t in_len, char* out, uint32_t out_len); + void SetFlush(int flush); + void GetAfterWriteOffsets(uint32_t* avail_in, uint32_t* avail_out) const; + CompressionError GetErrorInfo() const; + + ZstdContext(const ZstdContext&) = delete; + ZstdContext& operator=(const ZstdContext&) = delete; + + // Wrap ZSTD_freeCCtx to remove the return type. + static void FreeCCtx(ZSTD_CCtx* cctx) { ZSTD_freeCCtx(cctx); } + + protected: + ZSTD_EndDirective flush_ = ZSTD_e_continue; + + ZSTD_inBuffer input_ = {nullptr, 0, 0}; + ZSTD_outBuffer output_ = {nullptr, 0, 0}; + + ZSTD_ErrorCode error_ = ZSTD_error_no_error; + std::string error_string_; + std::string error_code_string_; +}; + +class ZstdCompressContext final : public ZstdContext { + public: + ZstdCompressContext() = default; + + // Streaming-related, should be available for all compression libraries: + void DoThreadPoolWork(); + CompressionError ResetStream(); + + // Zstd specific: + CompressionError Init(); + CompressionError SetParameter(int key, uint32_t value); + + // Wrap ZSTD_freeCCtx to remove the return type. + static void FreeCCtx(ZSTD_CCtx* cctx) { ZSTD_freeCCtx(cctx); } + + SET_MEMORY_INFO_NAME(ZstdCompressContext) + SET_SELF_SIZE(ZstdCompressContext) + SET_NO_MEMORY_INFO() + + private: + DeleteFnPtr cctx_; +}; + +class ZstdDecompressContext final : public ZstdContext { + public: + ZstdDecompressContext() = default; + + // Streaming-related, should be available for all compression libraries: + void DoThreadPoolWork(); + CompressionError ResetStream(); + + // Zstd specific: + CompressionError Init(); + CompressionError SetParameter(int key, uint32_t value); + + // Wrap ZSTD_freeDCtx to remove the return type. + static void FreeDCtx(ZSTD_DCtx* dctx) { ZSTD_freeDCtx(dctx); } + + SET_MEMORY_INFO_NAME(ZstdDecompressContext) + SET_SELF_SIZE(ZstdDecompressContext) + SET_NO_MEMORY_INFO() + + private: + DeleteFnPtr dctx_; +}; + template class CompressionStream : public AsyncWrap, public ThreadPoolWork { public: @@ -730,6 +846,74 @@ class BrotliCompressionStream final : using BrotliEncoderStream = BrotliCompressionStream; using BrotliDecoderStream = BrotliCompressionStream; +template +class ZstdStream final : public CompressionStream { + public: + ZstdStream(Environment* env, Local wrap) + : CompressionStream(env, wrap) {} + + inline CompressionContext* context() { + return this->CompressionStream::context(); + } + typedef typename CompressionStream::AllocScope AllocScope; + + static void New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + new ZstdStream(env, args.This()); + } + + static void Init(const FunctionCallbackInfo& args) { + CHECK(args.Length() == 3 && "init(params, writeResult, writeCallback)"); + ZstdStream* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + + CHECK(args[1]->IsUint32Array()); + uint32_t* write_result = reinterpret_cast(Buffer::Data(args[1])); + + CHECK(args[2]->IsFunction()); + Local write_js_callback = args[2].As(); + wrap->InitStream(write_result, write_js_callback); + + AllocScope alloc_scope(wrap); + CompressionError err = wrap->context()->Init(); + if (err.IsError()) { + wrap->EmitError(err); + args.GetReturnValue().Set(false); + return; + } + + CHECK(args[0]->IsUint32Array()); + const uint32_t* data = reinterpret_cast(Buffer::Data(args[0])); + size_t len = args[0].As()->Length(); + + for (int i = 0; static_cast(i) < len; i++) { + if (data[i] == static_cast(-1)) continue; + + CompressionError err = wrap->context()->SetParameter(i, data[i]); + if (err.IsError()) { + wrap->EmitError(err); + args.GetReturnValue().Set(false); + return; + } + } + + args.GetReturnValue().Set(true); + } + + static void Params(const FunctionCallbackInfo& args) { + // Currently a no-op, and not accessed from JS land. + // At some point zstd may support changing parameters on the fly, + // in which case we can implement this and a JS equivalent similar to + // the zlib Params() function. + } + + SET_MEMORY_INFO_NAME(ZstdStream) + SET_SELF_SIZE(ZstdStream) +}; + +using ZstdCompressStream = ZstdStream; +using ZstdDecompressStream = ZstdStream; + void ZlibContext::Close() { { Mutex::ScopedLock lock(mutex_); @@ -1253,6 +1437,109 @@ CompressionError BrotliDecoderContext::GetErrorInfo() const { } } +void ZstdContext::Close() {} + +void ZstdContext::SetBuffers(const char* in, + uint32_t in_len, + char* out, + uint32_t out_len) { + input_.src = reinterpret_cast(in); + input_.size = in_len; + input_.pos = 0; + + output_.dst = reinterpret_cast(out); + output_.size = out_len; + output_.pos = 0; +} + +void ZstdContext::SetFlush(int flush) { + flush_ = static_cast(flush); +} + +void ZstdContext::GetAfterWriteOffsets(uint32_t* avail_in, + uint32_t* avail_out) const { + *avail_in = input_.size - input_.pos; + *avail_out = output_.size - output_.pos; +} + +CompressionError ZstdContext::GetErrorInfo() const { + if (error_ != ZSTD_error_no_error) { + return CompressionError(error_string_.c_str(), + error_code_string_.c_str(), + static_cast(error_)); + } else { + return CompressionError{}; + } +} + +CompressionError ZstdCompressContext::SetParameter(int key, uint32_t value) { + size_t result = ZSTD_CCtx_setParameter( + cctx_.get(), static_cast(key), value); + if (ZSTD_isError(result)) { + return CompressionError( + "Setting parameter failed", "ERR_ZSTD_PARAM_SET_FAILED", -1); + } + return CompressionError{}; +} + +CompressionError ZstdCompressContext::Init() { + cctx_.reset(ZSTD_createCCtx()); + if (!cctx_) { + return CompressionError("Could not initialize zstd instance", + "ERR_ZLIB_INITIALIZATION_FAILED", + -1); + } + return CompressionError{}; +} + +CompressionError ZstdCompressContext::ResetStream() { + return Init(); +} + +void ZstdCompressContext::DoThreadPoolWork() { + size_t const remaining = + ZSTD_compressStream2(cctx_.get(), &output_, &input_, flush_); + if (ZSTD_isError(remaining)) { + error_ = ZSTD_getErrorCode(remaining); + error_code_string_ = ZstdStrerror(error_); + error_string_ = ZSTD_getErrorString(error_); + return; + } +} + +CompressionError ZstdDecompressContext::SetParameter(int key, uint32_t value) { + size_t result = ZSTD_DCtx_setParameter( + dctx_.get(), static_cast(key), value); + if (ZSTD_isError(result)) { + return CompressionError( + "Setting parameter failed", "ERR_ZSTD_PARAM_SET_FAILED", -1); + } + return CompressionError{}; +} + +CompressionError ZstdDecompressContext::Init() { + dctx_.reset(ZSTD_createDCtx()); + if (!dctx_) { + return CompressionError("Could not initialize zstd instance", + "ERR_ZLIB_INITIALIZATION_FAILED", + -1); + } + return CompressionError{}; +} + +CompressionError ZstdDecompressContext::ResetStream() { + return Init(); +} + +void ZstdDecompressContext::DoThreadPoolWork() { + size_t const ret = ZSTD_decompressStream(dctx_.get(), &output_, &input_); + if (ZSTD_isError(ret)) { + error_ = ZSTD_getErrorCode(ret); + error_code_string_ = ZstdStrerror(error_); + error_string_ = ZSTD_getErrorString(error_); + return; + } +} template struct MakeClass { @@ -1295,6 +1582,8 @@ void Initialize(Local target, MakeClass::Make(env, target, "Zlib"); MakeClass::Make(env, target, "BrotliEncoder"); MakeClass::Make(env, target, "BrotliDecoder"); + MakeClass::Make(env, target, "ZstdCompress"); + MakeClass::Make(env, target, "ZstdDecompress"); target->Set(env->context(), FIXED_ONE_BYTE_STRING(env->isolate(), "ZLIB_VERSION"), @@ -1305,6 +1594,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { MakeClass::Make(registry); MakeClass::Make(registry); MakeClass::Make(registry); + MakeClass::Make(registry); + MakeClass::Make(registry); } } // anonymous namespace @@ -1348,6 +1639,8 @@ void DefineZlibConstants(Local target) { NODE_DEFINE_CONSTANT(target, UNZIP); NODE_DEFINE_CONSTANT(target, BROTLI_DECODE); NODE_DEFINE_CONSTANT(target, BROTLI_ENCODE); + NODE_DEFINE_CONSTANT(target, ZSTD_DECOMPRESS); + NODE_DEFINE_CONSTANT(target, ZSTD_COMPRESS); NODE_DEFINE_CONSTANT(target, Z_MIN_WINDOWBITS); NODE_DEFINE_CONSTANT(target, Z_MAX_WINDOWBITS); @@ -1427,6 +1720,70 @@ void DefineZlibConstants(Local target) { NODE_DEFINE_CONSTANT(target, BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2); NODE_DEFINE_CONSTANT(target, BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES); NODE_DEFINE_CONSTANT(target, BROTLI_DECODER_ERROR_UNREACHABLE); + + // Zstd constants + NODE_DEFINE_CONSTANT(target, ZSTD_e_continue); + NODE_DEFINE_CONSTANT(target, ZSTD_e_flush); + NODE_DEFINE_CONSTANT(target, ZSTD_e_end); + NODE_DEFINE_CONSTANT(target, ZSTD_fast); + NODE_DEFINE_CONSTANT(target, ZSTD_dfast); + NODE_DEFINE_CONSTANT(target, ZSTD_greedy); + NODE_DEFINE_CONSTANT(target, ZSTD_lazy); + NODE_DEFINE_CONSTANT(target, ZSTD_lazy2); + NODE_DEFINE_CONSTANT(target, ZSTD_btlazy2); + NODE_DEFINE_CONSTANT(target, ZSTD_btopt); + NODE_DEFINE_CONSTANT(target, ZSTD_btultra); + NODE_DEFINE_CONSTANT(target, ZSTD_btultra2); + NODE_DEFINE_CONSTANT(target, ZSTD_c_compressionLevel); + NODE_DEFINE_CONSTANT(target, ZSTD_c_windowLog); + NODE_DEFINE_CONSTANT(target, ZSTD_c_hashLog); + NODE_DEFINE_CONSTANT(target, ZSTD_c_chainLog); + NODE_DEFINE_CONSTANT(target, ZSTD_c_searchLog); + NODE_DEFINE_CONSTANT(target, ZSTD_c_minMatch); + NODE_DEFINE_CONSTANT(target, ZSTD_c_targetLength); + NODE_DEFINE_CONSTANT(target, ZSTD_c_strategy); + NODE_DEFINE_CONSTANT(target, ZSTD_c_enableLongDistanceMatching); + NODE_DEFINE_CONSTANT(target, ZSTD_c_ldmHashLog); + NODE_DEFINE_CONSTANT(target, ZSTD_c_ldmMinMatch); + NODE_DEFINE_CONSTANT(target, ZSTD_c_ldmBucketSizeLog); + NODE_DEFINE_CONSTANT(target, ZSTD_c_ldmHashRateLog); + NODE_DEFINE_CONSTANT(target, ZSTD_c_contentSizeFlag); + NODE_DEFINE_CONSTANT(target, ZSTD_c_checksumFlag); + NODE_DEFINE_CONSTANT(target, ZSTD_c_dictIDFlag); + NODE_DEFINE_CONSTANT(target, ZSTD_c_nbWorkers); + NODE_DEFINE_CONSTANT(target, ZSTD_c_jobSize); + NODE_DEFINE_CONSTANT(target, ZSTD_c_overlapLog); + NODE_DEFINE_CONSTANT(target, ZSTD_d_windowLogMax); + NODE_DEFINE_CONSTANT(target, ZSTD_CLEVEL_DEFAULT); + // Error codes + NODE_DEFINE_CONSTANT(target, ZSTD_error_no_error); + NODE_DEFINE_CONSTANT(target, ZSTD_error_GENERIC); + NODE_DEFINE_CONSTANT(target, ZSTD_error_prefix_unknown); + NODE_DEFINE_CONSTANT(target, ZSTD_error_version_unsupported); + NODE_DEFINE_CONSTANT(target, ZSTD_error_frameParameter_unsupported); + NODE_DEFINE_CONSTANT(target, ZSTD_error_frameParameter_windowTooLarge); + NODE_DEFINE_CONSTANT(target, ZSTD_error_corruption_detected); + NODE_DEFINE_CONSTANT(target, ZSTD_error_checksum_wrong); + NODE_DEFINE_CONSTANT(target, ZSTD_error_literals_headerWrong); + NODE_DEFINE_CONSTANT(target, ZSTD_error_dictionary_corrupted); + NODE_DEFINE_CONSTANT(target, ZSTD_error_dictionary_wrong); + NODE_DEFINE_CONSTANT(target, ZSTD_error_dictionaryCreation_failed); + NODE_DEFINE_CONSTANT(target, ZSTD_error_parameter_unsupported); + NODE_DEFINE_CONSTANT(target, ZSTD_error_parameter_combination_unsupported); + NODE_DEFINE_CONSTANT(target, ZSTD_error_parameter_outOfBound); + NODE_DEFINE_CONSTANT(target, ZSTD_error_tableLog_tooLarge); + NODE_DEFINE_CONSTANT(target, ZSTD_error_maxSymbolValue_tooLarge); + NODE_DEFINE_CONSTANT(target, ZSTD_error_maxSymbolValue_tooSmall); + NODE_DEFINE_CONSTANT(target, ZSTD_error_stabilityCondition_notRespected); + NODE_DEFINE_CONSTANT(target, ZSTD_error_stage_wrong); + NODE_DEFINE_CONSTANT(target, ZSTD_error_init_missing); + NODE_DEFINE_CONSTANT(target, ZSTD_error_memory_allocation); + NODE_DEFINE_CONSTANT(target, ZSTD_error_workSpace_tooSmall); + NODE_DEFINE_CONSTANT(target, ZSTD_error_dstSize_tooSmall); + NODE_DEFINE_CONSTANT(target, ZSTD_error_srcSize_wrong); + NODE_DEFINE_CONSTANT(target, ZSTD_error_dstBuffer_null); + NODE_DEFINE_CONSTANT(target, ZSTD_error_noForwardProgress_destFull); + NODE_DEFINE_CONSTANT(target, ZSTD_error_noForwardProgress_inputEmpty); } } // namespace node diff --git a/test/fixtures/person.jpg.zst b/test/fixtures/person.jpg.zst new file mode 100644 index 0000000000000000000000000000000000000000..ea2df5c9a6668aa6056fce470902d13214545e79 GIT binary patch literal 45404 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 z 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 literal 0 HcmV?d00001 diff --git a/test/parallel/test-zlib-bytes-read.js b/test/parallel/test-zlib-bytes-read.js index 605281cd3bd7e6..09b515cc7b70cf 100644 --- a/test/parallel/test-zlib-bytes-read.js +++ b/test/parallel/test-zlib-bytes-read.js @@ -33,6 +33,7 @@ for (const method of [ ['createDeflate', 'createInflate', true], ['createDeflateRaw', 'createInflateRaw', true], ['createBrotliCompress', 'createBrotliDecompress', true], + ['createZstdCompress', 'createZstdDecompress', false], ]) { let compWriter; let compData = Buffer.alloc(0); diff --git a/test/parallel/test-zlib-convenience-methods.js b/test/parallel/test-zlib-convenience-methods.js index 01ec7e211bd5aa..f938a1f6a756ca 100644 --- a/test/parallel/test-zlib-convenience-methods.js +++ b/test/parallel/test-zlib-convenience-methods.js @@ -54,6 +54,8 @@ for (const [type, expect] of [ ['deflateRaw', 'inflateRaw', 'DeflateRaw', 'InflateRaw'], ['brotliCompress', 'brotliDecompress', 'BrotliCompress', 'BrotliDecompress'], + ['zstdCompress', 'zstdDecompress', + 'ZstdCompress', 'ZstdDecompress'], ]) { zlib[method[0]](expect, opts, common.mustCall((err, result) => { zlib[method[1]](result, opts, common.mustCall((err, result) => { diff --git a/test/parallel/test-zlib-empty-buffer.js b/test/parallel/test-zlib-empty-buffer.js index 27fd1340fd1eb4..af53b3013ee82f 100644 --- a/test/parallel/test-zlib-empty-buffer.js +++ b/test/parallel/test-zlib-empty-buffer.js @@ -11,10 +11,12 @@ const emptyBuffer = Buffer.alloc(0); [ zlib.deflateSync, zlib.inflateSync, 'deflate sync' ], [ zlib.gzipSync, zlib.gunzipSync, 'gzip sync' ], [ zlib.brotliCompressSync, zlib.brotliDecompressSync, 'br sync' ], + [ zlib.zstdCompressSync, zlib.zstdDecompressSync, 'zstd sync' ], [ promisify(zlib.deflateRaw), promisify(zlib.inflateRaw), 'raw' ], [ promisify(zlib.deflate), promisify(zlib.inflate), 'deflate' ], [ promisify(zlib.gzip), promisify(zlib.gunzip), 'gzip' ], [ promisify(zlib.brotliCompress), promisify(zlib.brotliDecompress), 'br' ], + [ promisify(zlib.zstdCompress), promisify(zlib.zstdDecompress), 'zstd' ], ]) { const compressed = await compress(emptyBuffer); const decompressed = await decompress(compressed); diff --git a/test/parallel/test-zlib-invalid-input.js b/test/parallel/test-zlib-invalid-input.js index 7aa44dfe7090a1..7aea0efa06dbcf 100644 --- a/test/parallel/test-zlib-invalid-input.js +++ b/test/parallel/test-zlib-invalid-input.js @@ -40,6 +40,7 @@ const unzips = [ zlib.Inflate(), zlib.InflateRaw(), zlib.BrotliDecompress(), + zlib.ZstdDecompress(), ]; nonStringInputs.forEach(common.mustCall((input) => { diff --git a/test/parallel/test-zlib-random-byte-pipes.js b/test/parallel/test-zlib-random-byte-pipes.js index d8d039a6d65c4f..6a7d7c505e5c25 100644 --- a/test/parallel/test-zlib-random-byte-pipes.js +++ b/test/parallel/test-zlib-random-byte-pipes.js @@ -144,6 +144,7 @@ class HashStream extends Stream { for (const [ createCompress, createDecompress ] of [ [ zlib.createGzip, zlib.createGunzip ], [ zlib.createBrotliCompress, zlib.createBrotliDecompress ], + [ zlib.createZstdCompress, zlib.createZstdDecompress ], ]) { const inp = new RandomReadStream({ total: 1024, block: 256, jitter: 16 }); const out = new HashStream(); diff --git a/test/parallel/test-zlib-write-after-flush.js b/test/parallel/test-zlib-write-after-flush.js index 6edcae2e2f18bf..57988191f84c57 100644 --- a/test/parallel/test-zlib-write-after-flush.js +++ b/test/parallel/test-zlib-write-after-flush.js @@ -27,6 +27,7 @@ const zlib = require('zlib'); for (const [ createCompress, createDecompress ] of [ [ zlib.createGzip, zlib.createGunzip ], [ zlib.createBrotliCompress, zlib.createBrotliDecompress ], + [ zlib.createZstdCompress, zlib.createZstdDecompress ], ]) { const gzip = createCompress(); const gunz = createDecompress(); diff --git a/test/parallel/test-zlib-zero-byte.js b/test/parallel/test-zlib-zero-byte.js index fc57960f1e56cb..6b3ecdcc0224e4 100644 --- a/test/parallel/test-zlib-zero-byte.js +++ b/test/parallel/test-zlib-zero-byte.js @@ -24,7 +24,13 @@ const common = require('../common'); const assert = require('assert'); const zlib = require('zlib'); -for (const Compressor of [ zlib.Gzip, zlib.BrotliCompress ]) { +const compressors = [ + [zlib.Gzip, 20], + [zlib.BrotliCompress, 1], + [zlib.ZstdCompress, 9], +]; + +for (const [Compressor, expected] of compressors) { const gz = Compressor(); const emptyBuffer = Buffer.alloc(0); let received = 0; @@ -33,7 +39,6 @@ for (const Compressor of [ zlib.Gzip, zlib.BrotliCompress ]) { }); gz.on('end', common.mustCall(function() { - const expected = Compressor === zlib.Gzip ? 20 : 1; assert.strictEqual(received, expected, `${received}, ${expected}, ${Compressor.name}`); })); diff --git a/test/parallel/test-zlib-zstd-flush.js b/test/parallel/test-zlib-zstd-flush.js new file mode 100644 index 00000000000000..bde202867bf56d --- /dev/null +++ b/test/parallel/test-zlib-zstd-flush.js @@ -0,0 +1,28 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); +const fixtures = require('../common/fixtures'); + +const file = fixtures.readSync('person.jpg'); +const chunkSize = 16; +const compress = new zlib.ZstdCompress(); + +const chunk = file.slice(0, chunkSize); +const expectedFull = Buffer.from('KLUv/QBYgAAA/9j/4AAQSkZJRgABAQEASA==', 'base64'); +let actualFull; + +compress.write(chunk, function() { + compress.flush(function() { + const bufs = []; + let buf; + while ((buf = compress.read()) !== null) + bufs.push(buf); + actualFull = Buffer.concat(bufs); + }); +}); + +process.once('exit', function() { + assert.deepStrictEqual(actualFull.toString('base64'), expectedFull.toString('base64')); + assert.deepStrictEqual(actualFull, expectedFull); +}); diff --git a/test/parallel/test-zlib-zstd-from-string.js b/test/parallel/test-zlib-zstd-from-string.js new file mode 100644 index 00000000000000..478e3be82604ed --- /dev/null +++ b/test/parallel/test-zlib-zstd-from-string.js @@ -0,0 +1,38 @@ +'use strict'; +// Test compressing and uncompressing a string with zstd + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +const inputString = 'ΩΩLorem ipsum dolor sit amet, consectetur adipiscing eli' + + 't. Morbi faucibus, purus at gravida dictum, libero arcu ' + + 'convallis lacus, in commodo libero metus eu nisi. Nullam' + + ' commodo, neque nec porta placerat, nisi est fermentum a' + + 'ugue, vitae gravida tellus sapien sit amet tellus. Aenea' + + 'n non diam orci. Proin quis elit turpis. Suspendisse non' + + ' diam ipsum. Suspendisse nec ullamcorper odio. Vestibulu' + + 'm arcu mi, sodales non suscipit id, ultrices ut massa. S' + + 'ed ac sem sit amet arcu malesuada fermentum. Nunc sed. '; +const compressedString = 'KLUv/QRYRQkA9tc9H6AlhTb/z/7/gbTI3kaWLKnbCtkZu/hXm0j' + + 'FpNz/VQM2ADMANQBHTuQOpIYzfVv7XGwXrpoIfgXNAB98xW4wV3' + + 'vnCF2bjcvWZF2wIZ1vr1mSHHvPHU0TgMGBwUFrF0xqReWcWPO8z' + + 'Ny6wMwFUilN+Lg987Zvs2GSRMy6uYvtovK9Uuhgst6l9FQrXLnA' + + '5gpZL7PdI8bO9sDH3tHm73XBzaUK+LjSPNKRmzQ3ZMYEPozdof1' + + '2KcZGfIcLa0PTsdkYqhGcAx/E9mWa8EGEeq0Qou2LTmzgg3YJz/' + + '21OuXSF+TOd662d60Qyb04xC5dOF4b8JFH8mpHAxAAELu3tg1oa' + + 'bBEIWaRHdE0l/+0RdEWWIVMAku8TgbiX/4bU+OpLo4UuY1FKDR8' + + 'RgBc'; + +zlib.zstdCompress(inputString, common.mustCall((err, buffer) => { + assert(inputString.length > buffer.length); + + zlib.zstdDecompress(buffer, common.mustCall((err, buffer) => { + assert.strictEqual(buffer.toString(), inputString); + })); +})); + +const buffer = Buffer.from(compressedString, 'base64'); +zlib.zstdDecompress(buffer, common.mustCall((err, buffer) => { + assert.strictEqual(buffer.toString(), inputString); +})); diff --git a/test/parallel/test-zlib-zstd-from-zstd.js b/test/parallel/test-zlib-zstd-from-zstd.js new file mode 100644 index 00000000000000..628dbefb25cb4e --- /dev/null +++ b/test/parallel/test-zlib-zstd-from-zstd.js @@ -0,0 +1,34 @@ +'use strict'; +// Test unzipping a file that was created with a non-node zstd lib, +// piped in as fast as possible. +// +// The compressed fixture was created using the reference CLI: +// $ zstd -19 test/fixtures/person.jpg -o test/fixtures/person.jpg.zst + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); +const fixtures = require('../common/fixtures'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +const decompress = new zlib.ZstdDecompress(); + +const fs = require('fs'); + +const fixture = fixtures.path('person.jpg.zst'); +const unzippedFixture = fixtures.path('person.jpg'); +const outputFile = tmpdir.resolve('person.jpg'); +const expect = fs.readFileSync(unzippedFixture); +const inp = fs.createReadStream(fixture); +const out = fs.createWriteStream(outputFile); + +inp.pipe(decompress).pipe(out); +out.on('close', common.mustCall(() => { + const actual = fs.readFileSync(outputFile); + assert.strictEqual(actual.length, expect.length); + for (let i = 0, l = actual.length; i < l; i++) { + assert.strictEqual(actual[i], expect[i], `byte[${i}]`); + } +})); diff --git a/test/parallel/test-zlib-zstd-kmaxlength-rangeerror.js b/test/parallel/test-zlib-zstd-kmaxlength-rangeerror.js new file mode 100644 index 00000000000000..58ad8ff2c98dea --- /dev/null +++ b/test/parallel/test-zlib-zstd-kmaxlength-rangeerror.js @@ -0,0 +1,29 @@ +'use strict'; +require('../common'); + +// This test ensures that zlib throws a RangeError if the final buffer needs to +// be larger than kMaxLength and concatenation fails. +// https://github.com/nodejs/node/pull/1811 + +const assert = require('assert'); + +// Change kMaxLength for zlib to trigger the error without having to allocate +// large Buffers. +const buffer = require('buffer'); +const oldkMaxLength = buffer.kMaxLength; +buffer.kMaxLength = 64; +const zlib = require('zlib'); +buffer.kMaxLength = oldkMaxLength; + +// "a".repeat(128), compressed using zstd. +const encoded = Buffer.from('KLUv/SCARQAAEGFhAQA7BVg=', 'base64'); + +// Async +zlib.zstdDecompress(encoded, function(err) { + assert.ok(err instanceof RangeError); +}); + +// Sync +assert.throws(function() { + zlib.zstdDecompressSync(encoded); +}, RangeError); diff --git a/test/parallel/test-zlib-zstd.js b/test/parallel/test-zlib-zstd.js new file mode 100644 index 00000000000000..ec823a6aeee77b --- /dev/null +++ b/test/parallel/test-zlib-zstd.js @@ -0,0 +1,94 @@ +'use strict'; +require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const zlib = require('zlib'); + +// Test some brotli-specific properties of the brotli streams that can not +// be easily covered through expanding zlib-only tests. + +const sampleBuffer = fixtures.readSync('/pss-vectors.json'); + +{ + // Test setting the quality parameter at stream creation: + const sizes = []; + for (let quality = 1; + quality <= 22; + quality++) { + const encoded = zlib.zstdCompressSync(sampleBuffer, { + params: { + [zlib.constants.ZSTD_c_compressionLevel]: quality + } + }); + sizes.push(encoded.length); + } + + // Increasing quality should roughly correspond to decreasing compressed size: + for (let i = 0; i < sizes.length - 1; i++) { + assert(sizes[i + 1] <= sizes[i] * 1.05, sizes); // 5 % margin of error. + } + assert(sizes[0] > sizes[sizes.length - 1], sizes); +} + +{ + // Test that setting out-of-bounds option values or keys fails. + assert.throws(() => { + zlib.createZstdCompress({ + params: { + 10000: 0 + } + }); + }, { + code: 'ERR_ZSTD_INVALID_PARAM', + name: 'RangeError', + message: '10000 is not a valid zstd parameter' + }); + + // Test that accidentally using duplicate keys fails. + assert.throws(() => { + zlib.createZstdCompress({ + params: { + '0': 0, + '00': 0 + } + }); + }, { + code: 'ERR_ZSTD_INVALID_PARAM', + name: 'RangeError', + message: '00 is not a valid zstd parameter' + }); + + assert.throws(() => { + zlib.createZstdCompress({ + params: { + // This is a boolean flag + [zlib.constants.BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING]: 42 + } + }); + }, { + code: 'ERR_ZLIB_INITIALIZATION_FAILED', + name: 'Error', + message: 'Initialization failed' + }); +} + +{ + // Test options.flush range + assert.throws(() => { + zlib.zstdCompressSync('', { flush: zlib.constants.Z_FINISH }); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.flush" is out of range. It must be >= 0 ' + + 'and <= 2. Received 4', + }); + + assert.throws(() => { + zlib.zstdCompressSync('', { finishFlush: zlib.constants.Z_FINISH }); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.finishFlush" is out of range. It must be ' + + '>= 0 and <= 2. Received 4', + }); +} diff --git a/test/parallel/test-zlib.js b/test/parallel/test-zlib.js index 65050b85a036cf..3b8878d915c097 100644 --- a/test/parallel/test-zlib.js +++ b/test/parallel/test-zlib.js @@ -42,6 +42,7 @@ let zlibPairs = [ [zlib.Gzip, zlib.Unzip], [zlib.DeflateRaw, zlib.InflateRaw], [zlib.BrotliCompress, zlib.BrotliDecompress], + [zlib.ZstdCompress, zlib.ZstdDecompress], ]; // How fast to trickle through the slowstream diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index c183b2963a4c3c..98eea1370d1f0e 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -236,6 +236,7 @@ const customTypesMap = { 'X509Certificate': 'crypto.html#class-x509certificate', 'zlib options': 'zlib.html#class-options', + 'zstd options': 'zlib.html#class-zstdoptions', 'ReadableStream': 'webstreams.html#class-readablestream',