diff --git a/doc/api/errors.md b/doc/api/errors.md
index b2a24e3b53ace5..9d6f438508d66c 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -624,6 +624,16 @@ An attempt was made to register something that is not a function as an
The type of an asynchronous resource was invalid. Note that users are also able
to define their own types if using the public embedder API.
+
+### ERR_BROTLI_INVALID_PARAM
+
+An invalid parameter key was passed during construction of a Brotli stream.
+
+
+### ERR_BROTLI_COMPRESSION_FAILED
+
+Data passed to a Brotli stream was not successfully compressed.
+
### ERR_BUFFER_CONTEXT_NOT_AVAILABLE
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index dae7f8600d1786..700277630054a6 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -546,6 +546,7 @@ E('ERR_ARG_NOT_ITERABLE', '%s must be iterable', TypeError);
E('ERR_ASSERTION', '%s', Error);
E('ERR_ASYNC_CALLBACK', '%s must be a function', TypeError);
E('ERR_ASYNC_TYPE', 'Invalid name for async "type": %s', TypeError);
+E('ERR_BROTLI_INVALID_PARAM', '%s is not a valid Brotli parameter', RangeError);
E('ERR_BUFFER_OUT_OF_BOUNDS',
// Using a default argument here is important so the argument is not counted
// towards `Function#length`.
diff --git a/lib/zlib.js b/lib/zlib.js
index 51fb316b30db50..5bb7f8bfcb0970 100644
--- a/lib/zlib.js
+++ b/lib/zlib.js
@@ -22,10 +22,11 @@
'use strict';
const {
+ ERR_BROTLI_INVALID_PARAM,
ERR_BUFFER_TOO_LARGE,
ERR_INVALID_ARG_TYPE,
ERR_OUT_OF_RANGE,
- ERR_ZLIB_INITIALIZATION_FAILED
+ ERR_ZLIB_INITIALIZATION_FAILED,
} = require('internal/errors').codes;
const Transform = require('_stream_transform');
const {
@@ -45,11 +46,18 @@ const { owner_symbol } = require('internal/async_hooks').symbols;
const constants = internalBinding('constants').zlib;
const {
+ // Zlib flush levels
Z_NO_FLUSH, Z_BLOCK, Z_PARTIAL_FLUSH, Z_SYNC_FLUSH, Z_FULL_FLUSH, Z_FINISH,
+ // Zlib option values
Z_MIN_CHUNK, Z_MIN_WINDOWBITS, Z_MAX_WINDOWBITS, Z_MIN_LEVEL, Z_MAX_LEVEL,
Z_MIN_MEMLEVEL, Z_MAX_MEMLEVEL, Z_DEFAULT_CHUNK, Z_DEFAULT_COMPRESSION,
Z_DEFAULT_STRATEGY, Z_DEFAULT_WINDOWBITS, Z_DEFAULT_MEMLEVEL, Z_FIXED,
- DEFLATE, DEFLATERAW, INFLATE, INFLATERAW, GZIP, GUNZIP, UNZIP
+ // Node's compression stream modes (node_zlib_mode)
+ DEFLATE, DEFLATERAW, INFLATE, INFLATERAW, GZIP, GUNZIP, UNZIP,
+ BROTLI_DECODE, BROTLI_ENCODE,
+ // Brotli operations (~flush levels)
+ BROTLI_OPERATION_PROCESS, BROTLI_OPERATION_FLUSH,
+ BROTLI_OPERATION_FINISH
} = constants;
// translation table for return codes.
@@ -212,7 +220,7 @@ 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 <= UNZIP);
+ assert(mode >= DEFLATE && mode <= BROTLI_ENCODE);
if (opts) {
chunkSize = opts.chunkSize;
@@ -481,7 +489,7 @@ function processCallback() {
// important to null out the values once they are no longer needed since
// `_handle` can stay in memory long after the buffer is needed.
var handle = this;
- var self = this.jsref;
+ var self = this[owner_symbol];
var state = self._writeState;
if (self._hadError) {
@@ -622,6 +630,9 @@ function Zlib(opts, mode) {
this._writeState,
processCallback,
dictionary)) {
+ // TODO(addaleax): Sometimes we generate better error codes in C++ land,
+ // e.g. ERR_BROTLI_PARAM_SET_FAILED -- it's hard to access them with
+ // the current bindings setup, though.
throw new ERR_ZLIB_INITIALIZATION_FAILED();
}
@@ -734,6 +745,70 @@ function createConvenienceMethod(ctor, sync) {
}
}
+const kMaxBrotliParam = Math.max(...Object.keys(constants).map((key) => {
+ return key.startsWith('BROTLI_PARAM_') ? constants[key] : 0;
+}));
+
+const brotliInitParamsArray = new Uint32Array(kMaxBrotliParam + 1);
+
+const brotliDefaultOpts = {
+ flush: BROTLI_OPERATION_PROCESS,
+ finishFlush: BROTLI_OPERATION_FINISH,
+ fullFlush: BROTLI_OPERATION_FLUSH
+};
+function Brotli(opts, mode) {
+ assert(mode === BROTLI_DECODE || mode === BROTLI_ENCODE);
+
+ brotliInitParamsArray.fill(-1);
+ if (opts && opts.params) {
+ for (const origKey of Object.keys(opts.params)) {
+ const key = +origKey;
+ if (Number.isNaN(key) || key < 0 || key > kMaxBrotliParam ||
+ (brotliInitParamsArray[key] | 0) !== -1) {
+ throw new ERR_BROTLI_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]);
+ }
+ brotliInitParamsArray[key] = value;
+ }
+ }
+
+ const handle = mode === BROTLI_DECODE ?
+ new binding.BrotliDecoder(mode) : new binding.BrotliEncoder(mode);
+
+ this._writeState = new Uint32Array(2);
+ if (!handle.init(brotliInitParamsArray,
+ this._writeState,
+ processCallback)) {
+ throw new ERR_ZLIB_INITIALIZATION_FAILED();
+ }
+
+ ZlibBase.call(this, opts, mode, handle, brotliDefaultOpts);
+}
+Object.setPrototypeOf(Brotli.prototype, Zlib.prototype);
+Object.setPrototypeOf(Brotli, Zlib);
+
+function BrotliCompress(opts) {
+ if (!(this instanceof BrotliCompress))
+ return new BrotliCompress(opts);
+ Brotli.call(this, opts, BROTLI_ENCODE);
+}
+Object.setPrototypeOf(BrotliCompress.prototype, Brotli.prototype);
+Object.setPrototypeOf(BrotliCompress, Brotli);
+
+function BrotliDecompress(opts) {
+ if (!(this instanceof BrotliDecompress))
+ return new BrotliDecompress(opts);
+ Brotli.call(this, opts, BROTLI_DECODE);
+}
+Object.setPrototypeOf(BrotliDecompress.prototype, Brotli.prototype);
+Object.setPrototypeOf(BrotliDecompress, Brotli);
+
+
function createProperty(ctor) {
return {
configurable: true,
@@ -759,6 +834,8 @@ module.exports = {
DeflateRaw,
InflateRaw,
Unzip,
+ BrotliCompress,
+ BrotliDecompress,
// Convenience methods.
// compress/decompress a string or buffer in one step.
@@ -775,7 +852,11 @@ module.exports = {
gunzip: createConvenienceMethod(Gunzip, false),
gunzipSync: createConvenienceMethod(Gunzip, true),
inflateRaw: createConvenienceMethod(InflateRaw, false),
- inflateRawSync: createConvenienceMethod(InflateRaw, true)
+ inflateRawSync: createConvenienceMethod(InflateRaw, true),
+ brotliCompress: createConvenienceMethod(BrotliCompress, false),
+ brotliCompressSync: createConvenienceMethod(BrotliCompress, true),
+ brotliDecompress: createConvenienceMethod(BrotliDecompress, false),
+ brotliDecompressSync: createConvenienceMethod(BrotliDecompress, true),
};
Object.defineProperties(module.exports, {
@@ -786,6 +867,8 @@ Object.defineProperties(module.exports, {
createGzip: createProperty(Gzip),
createGunzip: createProperty(Gunzip),
createUnzip: createProperty(Unzip),
+ createBrotliCompress: createProperty(BrotliCompress),
+ createBrotliDecompress: createProperty(BrotliDecompress),
constants: {
configurable: false,
enumerable: true,
@@ -803,6 +886,7 @@ Object.defineProperties(module.exports, {
const bkeys = Object.keys(constants);
for (var bk = 0; bk < bkeys.length; bk++) {
var bkey = bkeys[bk];
+ if (bkey.startsWith('BROTLI')) continue;
Object.defineProperty(module.exports, bkey, {
enumerable: false, value: constants[bkey], writable: false
});
diff --git a/node.gyp b/node.gyp
index e3fc7db47d6e7f..a0baf052be74d9 100644
--- a/node.gyp
+++ b/node.gyp
@@ -11,6 +11,7 @@
'node_shared%': 'false',
'force_dynamic_crt%': 0,
'node_module_version%': '',
+ 'node_shared_brotli%': 'false',
'node_shared_zlib%': 'false',
'node_shared_http_parser%': 'false',
'node_shared_cares%': 'false',
diff --git a/node.gypi b/node.gypi
index 7bcd0a42b4f55a..f6787e5ad8f317 100644
--- a/node.gypi
+++ b/node.gypi
@@ -208,6 +208,10 @@
'dependencies': [ 'deps/nghttp2/nghttp2.gyp:nghttp2' ],
}],
+ [ 'node_shared_brotli=="false"', {
+ 'dependencies': [ 'deps/brotli/brotli.gyp:brotli' ],
+ }],
+
[ 'OS=="mac"', {
# linking Corefoundation is needed since certain OSX debugging tools
# like Instruments require it for some features
diff --git a/src/node_metadata.cc b/src/node_metadata.cc
index ff8d408f5bdc68..3bfed6d4b57ea2 100644
--- a/src/node_metadata.cc
+++ b/src/node_metadata.cc
@@ -1,5 +1,6 @@
#include "node_metadata.h"
#include "ares.h"
+#include "brotli/encode.h"
#include "nghttp2/nghttp2ver.h"
#include "node.h"
#include "util.h"
@@ -72,6 +73,13 @@ Metadata::Versions::Versions() {
llhttp = per_process::llhttp_version;
http_parser = per_process::http_parser_version;
+ brotli =
+ std::to_string(BrotliEncoderVersion() >> 24) +
+ "." +
+ std::to_string((BrotliEncoderVersion() & 0xFFF000) >> 12) +
+ "." +
+ std::to_string(BrotliEncoderVersion() & 0xFFF);
+
#if HAVE_OPENSSL
openssl = GetOpenSSLVersion();
#endif
diff --git a/src/node_metadata.h b/src/node_metadata.h
index 3c3a430dd73584..ef93c7b7e41707 100644
--- a/src/node_metadata.h
+++ b/src/node_metadata.h
@@ -12,6 +12,7 @@ namespace node {
V(v8) \
V(uv) \
V(zlib) \
+ V(brotli) \
V(ares) \
V(modules) \
V(nghttp2) \
diff --git a/src/node_zlib.cc b/src/node_zlib.cc
index f476c554d4477b..4c65ede612c0ae 100644
--- a/src/node_zlib.cc
+++ b/src/node_zlib.cc
@@ -28,6 +28,9 @@
#include "util-inl.h"
#include "v8.h"
+
+#include "brotli/encode.h"
+#include "brotli/decode.h"
#include "zlib.h"
#include
@@ -82,7 +85,9 @@ enum node_zlib_mode {
GUNZIP,
DEFLATERAW,
INFLATERAW,
- UNZIP
+ UNZIP,
+ BROTLI_DECODE,
+ BROTLI_ENCODE
};
#define GZIP_HEADER_ID1 0x1f
@@ -111,13 +116,13 @@ class ZlibContext : public MemoryRetainer {
void SetFlush(int flush);
void GetAfterWriteOffsets(uint32_t* avail_in, uint32_t* avail_out) const;
CompressionError GetErrorInfo() const;
+ inline void SetMode(node_zlib_mode mode) { mode_ = mode; }
+ CompressionError ResetStream();
// Zlib-specific:
CompressionError Init(int level, int window_bits, int mem_level, int strategy,
std::vector&& dictionary);
- inline void SetMode(node_zlib_mode mode) { mode_ = mode; }
void SetAllocationFunctions(alloc_func alloc, free_func free, void* opaque);
- CompressionError ResetStream();
CompressionError SetParams(int level, int strategy);
SET_MEMORY_INFO_NAME(ZlibContext)
@@ -146,6 +151,77 @@ class ZlibContext : public MemoryRetainer {
DISALLOW_COPY_AND_ASSIGN(ZlibContext);
};
+// Brotli has different data types for compression and decompression streams,
+// so some of the specifics are implemented in more specific subclasses
+class BrotliContext : public MemoryRetainer {
+ public:
+ BrotliContext() = default;
+
+ void SetBuffers(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;
+ inline void SetMode(node_zlib_mode mode) { mode_ = mode; }
+
+ protected:
+ node_zlib_mode mode_ = NONE;
+ uint8_t* next_in_ = nullptr;
+ uint8_t* next_out_ = nullptr;
+ size_t avail_in_ = 0;
+ size_t avail_out_ = 0;
+ BrotliEncoderOperation flush_ = BROTLI_OPERATION_PROCESS;
+ // TODO(addaleax): These should not need to be stored here.
+ // This is currently only done this way to make implementing ResetStream()
+ // easier.
+ brotli_alloc_func alloc_ = nullptr;
+ brotli_free_func free_ = nullptr;
+ void* alloc_opaque_ = nullptr;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(BrotliContext);
+};
+
+class BrotliEncoderContext final : public BrotliContext {
+ public:
+ void Close();
+ void DoThreadPoolWork();
+ CompressionError Init(brotli_alloc_func alloc,
+ brotli_free_func free,
+ void* opaque);
+ CompressionError ResetStream();
+ CompressionError SetParams(int key, uint32_t value);
+ CompressionError GetErrorInfo() const;
+
+ SET_MEMORY_INFO_NAME(BrotliEncoderContext)
+ SET_SELF_SIZE(BrotliEncoderContext)
+ SET_NO_MEMORY_INFO() // state_ is covered through allocation tracking.
+
+ private:
+ bool last_result_ = false;
+ DeleteFnPtr state_;
+};
+
+class BrotliDecoderContext final : public BrotliContext {
+ public:
+ void Close();
+ void DoThreadPoolWork();
+ CompressionError Init(brotli_alloc_func alloc,
+ brotli_free_func free,
+ void* opaque);
+ CompressionError ResetStream();
+ CompressionError SetParams(int key, uint32_t value);
+ CompressionError GetErrorInfo() const;
+
+ SET_MEMORY_INFO_NAME(BrotliDecoderContext)
+ SET_SELF_SIZE(BrotliDecoderContext)
+ SET_NO_MEMORY_INFO() // state_ is covered through allocation tracking.
+
+ private:
+ BrotliDecoderResult last_result_ = BROTLI_DECODER_RESULT_SUCCESS;
+ BrotliDecoderErrorCode error_ = BROTLI_DECODER_NO_ERROR;
+ std::string error_string_;
+ DeleteFnPtr state_;
+};
+
template
class CompressionStream : public AsyncWrap, public ThreadPoolWork {
public:
@@ -340,6 +416,16 @@ class CompressionStream : public AsyncWrap, public ThreadPoolWork {
Close();
}
+ static void Reset(const FunctionCallbackInfo &args) {
+ CompressionStream* wrap;
+ ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
+
+ AllocScope alloc_scope(wrap);
+ const CompressionError err = wrap->context()->ResetStream();
+ if (err.IsError())
+ wrap->EmitError(err);
+ }
+
void MemoryInfo(MemoryTracker* tracker) const override {
tracker->TrackField("compression context", ctx_);
tracker->TrackFieldWithSize("zlib_memory",
@@ -362,14 +448,19 @@ class CompressionStream : public AsyncWrap, public ThreadPoolWork {
// to V8; rather, we first store it as "unreported" memory in a separate
// field and later report it back from the main thread.
static void* AllocForZlib(void* data, uInt items, uInt size) {
- CompressionStream* ctx = static_cast(data);
size_t real_size =
MultiplyWithOverflowCheck(static_cast(items),
- static_cast(size)) + sizeof(size_t);
- char* memory = UncheckedMalloc(real_size);
+ static_cast(size));
+ return AllocForBrotli(data, real_size);
+ }
+
+ static void* AllocForBrotli(void* data, size_t size) {
+ size += sizeof(size_t);
+ CompressionStream* ctx = static_cast(data);
+ char* memory = UncheckedMalloc(size);
if (UNLIKELY(memory == nullptr)) return nullptr;
- *reinterpret_cast(memory) = real_size;
- ctx->unreported_allocations_.fetch_add(real_size,
+ *reinterpret_cast(memory) = size;
+ ctx->unreported_allocations_.fetch_add(size,
std::memory_order_relaxed);
return memory + sizeof(size_t);
}
@@ -484,6 +575,7 @@ class ZlibStream : public CompressionStream {
Local ab = array->Buffer();
uint32_t* write_result = static_cast(ab->GetContents().Data());
+ CHECK(args[5]->IsFunction());
Local write_js_callback = args[5].As();
std::vector dictionary;
@@ -525,20 +617,88 @@ class ZlibStream : public CompressionStream {
wrap->EmitError(err);
}
- static void Reset(const FunctionCallbackInfo &args) {
- ZlibStream* wrap;
+ SET_MEMORY_INFO_NAME(ZlibStream)
+ SET_SELF_SIZE(ZlibStream)
+};
+
+template
+class BrotliCompressionStream : public CompressionStream {
+ public:
+ BrotliCompressionStream(Environment* env,
+ Local