Skip to content

Commit

Permalink
zlib: add brotli support
Browse files Browse the repository at this point in the history
Refs: nodejs#20458

Co-authored-by: Hackzzila <[email protected]>
  • Loading branch information
addaleax and Hackzzila committed Jan 3, 2019
1 parent f42adea commit 193da61
Show file tree
Hide file tree
Showing 9 changed files with 537 additions and 37 deletions.
10 changes: 10 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a id="ERR_BROTLI_INVALID_PARAM"></a>
### ERR_BROTLI_INVALID_PARAM

An invalid parameter key was passed during construction of a Brotli stream.

<a id="ERR_BROTLI_COMPRESSION_FAILED">
### ERR_BROTLI_COMPRESSION_FAILED

Data passed to a Brotli stream was not successfully compressed.

<a id="ERR_BUFFER_CONTEXT_NOT_AVAILABLE"></a>
### ERR_BUFFER_CONTEXT_NOT_AVAILABLE

Expand Down
1 change: 1 addition & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
91 changes: 86 additions & 5 deletions lib/zlib.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -734,6 +745,67 @@ function createConvenienceMethod(ctor, sync) {
}
}

const kMaxBrotliParam = Object.keys(constants).map((key) => {
return key.startsWith('BROTLI_PARAM_') ? constants[key] : 0;
}).reduce((a, b) => Math.max(a, b));

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)
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,
Expand All @@ -759,6 +831,8 @@ module.exports = {
DeflateRaw,
InflateRaw,
Unzip,
BrotliCompress,
BrotliDecompress,

// Convenience methods.
// compress/decompress a string or buffer in one step.
Expand All @@ -775,7 +849,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, {
Expand All @@ -786,6 +864,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,
Expand All @@ -803,6 +883,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
});
Expand Down
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 4 additions & 0 deletions node.gypi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/node_metadata.cc
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/node_metadata.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace node {
V(v8) \
V(uv) \
V(zlib) \
V(brotli) \
V(ares) \
V(modules) \
V(nghttp2) \
Expand Down
Loading

0 comments on commit 193da61

Please sign in to comment.