From 972fcf18c769b946aaa26468bb5d4c33469d3051 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Tue, 27 Aug 2024 16:23:19 -0400 Subject: [PATCH] implement brotli classes --- src/node/internal/internal_errors.ts | 15 + src/node/internal/internal_zlib.ts | 76 ++-- src/node/internal/internal_zlib_base.ts | 104 ++++- src/node/internal/internal_zlib_constants.ts | 53 +++ src/node/internal/zlib.d.ts | 57 ++- src/node/zlib.ts | 19 +- src/workerd/api/node/BUILD.bazel | 1 + .../api/node/tests/zlib-nodejs-test.js | 14 +- src/workerd/api/node/zlib-util.c++ | 373 ++++++++++++++---- src/workerd/api/node/zlib-util.h | 267 +++++++++---- 10 files changed, 753 insertions(+), 226 deletions(-) create mode 100644 src/node/internal/internal_zlib_constants.ts diff --git a/src/node/internal/internal_errors.ts b/src/node/internal/internal_errors.ts index 3f829e27c3e..013d7ac854c 100644 --- a/src/node/internal/internal_errors.ts +++ b/src/node/internal/internal_errors.ts @@ -535,6 +535,21 @@ export class ERR_BUFFER_TOO_LARGE extends NodeRangeError { } } +export class ERR_BROTLI_INVALID_PARAM extends NodeRangeError { + constructor(value: unknown) { + super( + 'ERR_BROTLI_INVALID_PARAM', + `${value} is not a valid Brotli parameter` + ); + } +} + +export class ERR_ZLIB_INITIALIZATION_FAILED extends NodeError { + constructor() { + super('ERR_ZLIB_INITIALIZATION_FAILED', 'Initialization failed'); + } +} + export function aggregateTwoErrors(innerError: any, outerError: any) { if (innerError && outerError && innerError !== outerError) { if (Array.isArray(outerError.errors)) { diff --git a/src/node/internal/internal_zlib.ts b/src/node/internal/internal_zlib.ts index 7cd864fec82..0ef82df925a 100644 --- a/src/node/internal/internal_zlib.ts +++ b/src/node/internal/internal_zlib.ts @@ -7,22 +7,14 @@ import { default as zlibUtil, type ZlibOptions, type CompressCallback, + type BrotliOptions, } from 'node-internal:zlib'; import { Buffer } from 'node-internal:internal_buffer'; import { validateUint32 } from 'node-internal:validators'; import { ERR_INVALID_ARG_TYPE } from 'node-internal:internal_errors'; -import { Zlib } from 'node-internal:internal_zlib_base'; +import { Zlib, Brotli } from 'node-internal:internal_zlib_base'; const { - CONST_Z_OK, - CONST_Z_STREAM_END, - CONST_Z_NEED_DICT, - CONST_Z_ERRNO, - CONST_Z_STREAM_ERROR, - CONST_Z_DATA_ERROR, - CONST_Z_MEM_ERROR, - CONST_Z_BUF_ERROR, - CONST_Z_VERSION_ERROR, CONST_DEFLATE, CONST_DEFLATERAW, CONST_INFLATE, @@ -30,6 +22,8 @@ const { CONST_GUNZIP, CONST_GZIP, CONST_UNZIP, + CONST_BROTLI_DECODE, + CONST_BROTLI_ENCODE, } = zlibUtil; export function crc32( @@ -277,46 +271,6 @@ export function gzip( zlibUtil.zlib(data, options, zlibUtil.CONST_GZIP, wrapCallback(callback)); } -const constPrefix = 'CONST_'; -export const constants: Record = {}; - -Object.defineProperties( - constants, - Object.fromEntries( - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - Object.entries(Object.getPrototypeOf(zlibUtil)) - .filter(([k]) => k.startsWith(constPrefix)) - .map(([k, v]) => [ - k.slice(constPrefix.length), - { - value: v, - writable: false, - configurable: false, - enumerable: true, - }, - ]) - ) -); - -// Translation table for return codes. -const rawCodes: Record = { - Z_OK: CONST_Z_OK, - Z_STREAM_END: CONST_Z_STREAM_END, - Z_NEED_DICT: CONST_Z_NEED_DICT, - Z_ERRNO: CONST_Z_ERRNO, - Z_STREAM_ERROR: CONST_Z_STREAM_ERROR, - Z_DATA_ERROR: CONST_Z_DATA_ERROR, - Z_MEM_ERROR: CONST_Z_MEM_ERROR, - Z_BUF_ERROR: CONST_Z_BUF_ERROR, - Z_VERSION_ERROR: CONST_Z_VERSION_ERROR, -}; - -for (const key of Object.keys(rawCodes)) { - rawCodes[rawCodes[key] as number] = key; -} - -export const codes = Object.freeze(rawCodes); - export class Gzip extends Zlib { public constructor(options: ZlibOptions) { super(options, CONST_GZIP); @@ -362,6 +316,18 @@ export class Unzip extends Zlib { } } +export class BrotliCompress extends Brotli { + public constructor(options: BrotliOptions) { + super(options, CONST_BROTLI_ENCODE); + } +} + +export class BrotliDecompress extends Brotli { + public constructor(options: BrotliOptions) { + super(options, CONST_BROTLI_DECODE); + } +} + export function createGzip(options: ZlibOptions): Gzip { return new Gzip(options); } @@ -389,3 +355,13 @@ export function createInflateRaw(options: ZlibOptions): InflateRaw { export function createUnzip(options: ZlibOptions): Unzip { return new Unzip(options); } + +export function createBrotliCompress(options: BrotliOptions): BrotliCompress { + return new BrotliCompress(options); +} + +export function createBrotliDecompress( + options: BrotliOptions +): BrotliDecompress { + return new BrotliDecompress(options); +} diff --git a/src/node/internal/internal_zlib_base.ts b/src/node/internal/internal_zlib_base.ts index 543b81fa09d..d7e63b5372c 100644 --- a/src/node/internal/internal_zlib_base.ts +++ b/src/node/internal/internal_zlib_base.ts @@ -3,7 +3,11 @@ // https://opensource.org/licenses/Apache-2.0 // Copyright Joyent and Node contributors. All rights reserved. MIT license. -import { default as zlibUtil, type ZlibOptions } from 'node-internal:zlib'; +import { + default as zlibUtil, + type ZlibOptions, + type BrotliOptions, +} from 'node-internal:zlib'; import { Buffer, kMaxLength } from 'node-internal:internal_buffer'; import { checkRangesOrGetDefault, @@ -13,6 +17,8 @@ import { ERR_OUT_OF_RANGE, ERR_BUFFER_TOO_LARGE, ERR_INVALID_ARG_TYPE, + ERR_BROTLI_INVALID_PARAM, + ERR_ZLIB_INITIALIZATION_FAILED, NodeError, } from 'node-internal:internal_errors'; import { Transform, type DuplexOptions } from 'node-internal:streams_transform'; @@ -21,6 +27,7 @@ import { isArrayBufferView, isAnyArrayBuffer, } from 'node-internal:internal_types'; +import { constants } from 'node-internal:internal_zlib_constants'; // Explicitly import `ok()` to avoid typescript error requiring every name in the call target to // be annotated with an explicit type annotation. @@ -53,8 +60,15 @@ const { CONST_BROTLI_DECODE, CONST_BROTLI_OPERATION_PROCESS, CONST_BROTLI_OPERATION_EMIT_METADATA, + CONST_BROTLI_OPERATION_FINISH, + CONST_BROTLI_OPERATION_FLUSH, } = zlibUtil; +// This type contains all possible handler types. +type ZlibHandleType = + | zlibUtil.ZlibStream + | zlibUtil.BrotliEncoder + | zlibUtil.BrotliDecoder; export const owner_symbol = Symbol('owner'); const FLUSH_BOUND_IDX_NORMAL: number = 0; @@ -67,7 +81,7 @@ const FLUSH_BOUND: [[number, number], [number, number]] = [ const kFlushFlag = Symbol('kFlushFlag'); const kError = Symbol('kError'); -function processCallback(this: zlibUtil.ZlibStream): void { +function processCallback(this: ZlibHandleType): void { // This callback's context (`this`) is the `_handle` (ZCtx) object. It is // important to null out the values once they are no longer needed since // `_handle` can stay in memory long after the buffer is needed. @@ -186,23 +200,27 @@ for (let i = 0; i < kFlushFlagList.length; i++) { flushiness[kFlushFlagList[i] as number] = i; } -type BufferWithFlushFlag = Buffer & { [kFlushFlag]: number }; +function maxFlush(a: number, b: number): number { + return (flushiness[a] as number) > (flushiness[b] as number) ? a : b; +} // Set up a list of 'special' buffers that can be written using .write() // from the .flush() code as a way of introducing flushing operations into the // write sequence. -const kFlushBuffers: BufferWithFlushFlag[] = []; +const kFlushBuffers: (Buffer & { [kFlushFlag]: number })[] = []; { const dummyArrayBuffer = new ArrayBuffer(0); for (const flushFlag of kFlushFlagList) { - const buf = Buffer.from(dummyArrayBuffer) as BufferWithFlushFlag; + const buf = Buffer.from(dummyArrayBuffer) as Buffer & { + [kFlushFlag]: number; + }; buf[kFlushFlag] = flushFlag; kFlushBuffers[flushFlag] = buf; } } function zlibOnError( - this: zlibUtil.ZlibStream, + this: ZlibHandleType, errno: number, code: string, message: string @@ -332,7 +350,7 @@ export class ZlibBase extends Transform { public _finishFlushFlag: number; public _defaultFullFlushFlag: number; public _info: unknown; - public _handle: zlibUtil.ZlibStream | null = null; + public _handle: ZlibHandleType | null = null; public _writeState = new Uint32Array(2); public [kError]: NodeError | undefined; @@ -340,7 +358,7 @@ export class ZlibBase extends Transform { public constructor( opts: ZlibOptions & DuplexOptions, mode: number, - handle: zlibUtil.ZlibStream, + handle: ZlibHandleType, { flush, finishFlush, fullFlush }: ZlibDefaultOptions = zlibDefaultOptions ) { let chunkSize = CONST_Z_DEFAULT_CHUNK; @@ -544,10 +562,6 @@ export class ZlibBase extends Transform { } } -function maxFlush(a: number, b: number): number { - return (flushiness[a] as number) > (flushiness[b] as number) ? a : b; -} - export class Zlib extends ZlibBase { public _level = CONST_Z_DEFAULT_COMPRESSION; public _strategy = CONST_Z_DEFAULT_STRATEGY; @@ -686,3 +700,69 @@ export class Zlib extends ZlibBase { } } } + +const kMaxBrotliParam = Math.max( + ...Object.entries(constants).map(([key, value]) => + key.startsWith('BROTLI_PARAM_') ? value : 0 + ) +); +const brotliInitParamsArray = new Uint32Array(kMaxBrotliParam + 1); +const brotliDefaultOptions: ZlibDefaultOptions = { + flush: CONST_BROTLI_OPERATION_PROCESS, + finishFlush: CONST_BROTLI_OPERATION_FINISH, + fullFlush: CONST_BROTLI_OPERATION_FLUSH, +}; + +export class Brotli extends ZlibBase { + public constructor(options: BrotliOptions | undefined | null, mode: number) { + ok(mode === CONST_BROTLI_DECODE || mode === CONST_BROTLI_ENCODE); + brotliInitParamsArray.fill(-1); + + if (options?.params) { + for (const [origKey, value] of Object.entries(options.params)) { + const key = +origKey; + if ( + Number.isNaN(key) || + key < 0 || + key > kMaxBrotliParam || + ((brotliInitParamsArray[key] as number) | 0) !== -1 + ) { + throw new ERR_BROTLI_INVALID_PARAM(origKey); + } + + if (typeof value !== 'number' && typeof value !== 'boolean') { + throw new ERR_INVALID_ARG_TYPE( + 'options.params[key]', + 'number', + value + ); + } + // as number is required to avoid force type coercion on runtime. + // boolean has number representation, but typescript doesn't understand it. + brotliInitParamsArray[key] = value as number; + } + } + + const handle = + mode === CONST_BROTLI_DECODE + ? new zlibUtil.BrotliDecoder(mode) + : new zlibUtil.BrotliEncoder(mode); + + const _writeState = new Uint32Array(2); + super(options ?? {}, mode, handle, brotliDefaultOptions); + this._writeState = _writeState; + + // 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. + if ( + !handle.initialize( + brotliInitParamsArray, + _writeState, + processCallback.bind(handle) + ) + ) { + throw new ERR_ZLIB_INITIALIZATION_FAILED(); + } + } +} diff --git a/src/node/internal/internal_zlib_constants.ts b/src/node/internal/internal_zlib_constants.ts new file mode 100644 index 00000000000..43746150c99 --- /dev/null +++ b/src/node/internal/internal_zlib_constants.ts @@ -0,0 +1,53 @@ +import { default as zlibUtil } from 'node-internal:zlib'; + +const { + CONST_Z_OK, + CONST_Z_STREAM_END, + CONST_Z_NEED_DICT, + CONST_Z_ERRNO, + CONST_Z_STREAM_ERROR, + CONST_Z_DATA_ERROR, + CONST_Z_MEM_ERROR, + CONST_Z_BUF_ERROR, + CONST_Z_VERSION_ERROR, +} = zlibUtil; + +const constPrefix = 'CONST_'; +export const constants: Record = {}; + +Object.defineProperties( + constants, + Object.fromEntries( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + Object.entries(Object.getPrototypeOf(zlibUtil)) + .filter(([k]) => k.startsWith(constPrefix)) + .map(([k, v]) => [ + k.slice(constPrefix.length), + { + value: v, + writable: false, + configurable: false, + enumerable: true, + }, + ]) + ) +); + +// Translation table for return codes. +const rawCodes: Record = { + Z_OK: CONST_Z_OK, + Z_STREAM_END: CONST_Z_STREAM_END, + Z_NEED_DICT: CONST_Z_NEED_DICT, + Z_ERRNO: CONST_Z_ERRNO, + Z_STREAM_ERROR: CONST_Z_STREAM_ERROR, + Z_DATA_ERROR: CONST_Z_DATA_ERROR, + Z_MEM_ERROR: CONST_Z_MEM_ERROR, + Z_BUF_ERROR: CONST_Z_BUF_ERROR, + Z_VERSION_ERROR: CONST_Z_VERSION_ERROR, +}; + +for (const key of Object.keys(rawCodes)) { + rawCodes[rawCodes[key] as number] = key; +} + +export const codes = Object.freeze(rawCodes); diff --git a/src/node/internal/zlib.d.ts b/src/node/internal/zlib.d.ts index 2ff378062ca..0aade4d9feb 100644 --- a/src/node/internal/zlib.d.ts +++ b/src/node/internal/zlib.d.ts @@ -147,12 +147,23 @@ export interface ZlibOptions { maxOutputLength?: number | undefined; } +export interface BrotliOptions { + flush?: number | undefined; + finishFlush?: number | undefined; + chunkSize?: number | undefined; + params?: + | { + [key: number]: boolean | number; + } + | undefined; + maxOutputLength?: number | undefined; +} + type ErrorHandler = (errno: number, code: string, message: string) => void; type ProcessHandler = () => void; -export class ZlibStream { +export abstract class CompressionStream { public [owner_symbol]: Zlib; - // Not used by C++ implementation but required to be Node.js compatible. public inOff: number; /* eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents */ @@ -163,15 +174,6 @@ export class ZlibStream { public flushFlag: number; public constructor(mode: number); - public initialize( - windowBits: number, - level: number, - memLevel: number, - strategy: number, - writeState: NodeJS.TypedArray, - processCallback: ProcessHandler, - dictionary: ZlibOptions['dictionary'] - ): void; public close(): void; public write( flushFlag: number, @@ -191,8 +193,39 @@ export class ZlibStream { outputOffset: number, outputLength: number ): void; - public params(level: number, strategy: number): void; public reset(): void; + // Workerd specific functions public setErrorHandler(cb: ErrorHandler): void; } + +export class ZlibStream extends CompressionStream { + public initialize( + windowBits: number, + level: number, + memLevel: number, + strategy: number, + writeState: NodeJS.TypedArray, + processCallback: ProcessHandler, + dictionary: ZlibOptions['dictionary'] + ): void; + public params(level: number, strategy: number): void; +} + +export class BrotliDecoder extends CompressionStream { + public initialize( + params: Uint32Array, + writeResult: Uint32Array, + writeCallback: () => void + ): boolean; + public params(): void; +} + +export class BrotliEncoder extends CompressionStream { + public initialize( + params: Uint32Array, + writeResult: Uint32Array, + writeCallback: () => void + ): boolean; + public params(): void; +} diff --git a/src/node/zlib.ts b/src/node/zlib.ts index 10df4cb9a2c..6c45ff7220a 100644 --- a/src/node/zlib.ts +++ b/src/node/zlib.ts @@ -1,5 +1,6 @@ import * as zlib from 'node-internal:internal_zlib'; -import { crc32, constants, codes } from 'node-internal:internal_zlib'; +import { crc32 } from 'node-internal:internal_zlib'; +import { constants, codes } from 'node-internal:internal_zlib_constants'; import { default as compatFlags } from 'workerd:compatibility-flags'; const { nodeJsZlib } = compatFlags; @@ -21,6 +22,9 @@ const DeflateRaw = protectMethod(zlib.DeflateRaw); const Inflate = protectMethod(zlib.Inflate); const InflateRaw = protectMethod(zlib.InflateRaw); const Unzip = protectMethod(zlib.Unzip); +const BrotliCompress = protectMethod(zlib.BrotliCompress); +const BrotliDecompress = protectMethod(zlib.BrotliDecompress); + const createGzip = protectMethod(zlib.createGzip); const createGunzip = protectMethod(zlib.createGunzip); const createDeflate = protectMethod(zlib.createDeflate); @@ -28,22 +32,21 @@ const createDeflateRaw = protectMethod(zlib.createDeflateRaw); const createInflate = protectMethod(zlib.createInflate); const createInflateRaw = protectMethod(zlib.createInflateRaw); const createUnzip = protectMethod(zlib.createUnzip); +const createBrotliCompress = protectMethod(zlib.createBrotliCompress); +const createBrotliDecompress = protectMethod(zlib.createBrotliDecompress); const inflate = protectMethod(zlib.inflate); const inflateSync = protectMethod(zlib.inflateSync); const deflate = protectMethod(zlib.deflate); const deflateSync = protectMethod(zlib.deflateSync); - const inflateRaw = protectMethod(zlib.inflateRaw); const inflateRawSync = protectMethod(zlib.inflateRawSync); const deflateRaw = protectMethod(zlib.deflateRaw); const deflateRawSync = protectMethod(zlib.deflateRawSync); - const gzip = protectMethod(zlib.gzip); const gzipSync = protectMethod(zlib.gzipSync); const gunzip = protectMethod(zlib.gunzip); const gunzipSync = protectMethod(zlib.gunzipSync); - const unzip = protectMethod(zlib.unzip); const unzipSync = protectMethod(zlib.unzipSync); @@ -60,6 +63,8 @@ export { Inflate, InflateRaw, Unzip, + BrotliCompress, + BrotliDecompress, // Convenience methods to create classes createGzip, @@ -69,6 +74,8 @@ export { createInflate, createInflateRaw, createUnzip, + createBrotliCompress, + createBrotliDecompress, // One-shot methods inflate, @@ -100,6 +107,8 @@ export default { Inflate, InflateRaw, Unzip, + BrotliCompress, + BrotliDecompress, // Convenience methods to create classes createGzip, @@ -109,6 +118,8 @@ export default { createInflate, createInflateRaw, createUnzip, + createBrotliCompress, + createBrotliDecompress, // One-shot methods inflate, diff --git a/src/workerd/api/node/BUILD.bazel b/src/workerd/api/node/BUILD.bazel index c4e7e20d20c..aa820770fa7 100644 --- a/src/workerd/api/node/BUILD.bazel +++ b/src/workerd/api/node/BUILD.bazel @@ -14,6 +14,7 @@ wd_cc_library( "@capnp-cpp//src/kj/compat:kj-gzip", "@nbytes", "@simdutf", + "@zlib", ], visibility = ["//visibility:public"], deps = [ diff --git a/src/workerd/api/node/tests/zlib-nodejs-test.js b/src/workerd/api/node/tests/zlib-nodejs-test.js index 9ff33bb5ebe..568c92ac847 100644 --- a/src/workerd/api/node/tests/zlib-nodejs-test.js +++ b/src/workerd/api/node/tests/zlib-nodejs-test.js @@ -717,8 +717,12 @@ export const testZlibBytesRead = { } // This test is simplified a lot because of test runner limitations. - // TODO(soon): Add createBrotliCompress once it is implemented. - for (const method of ['createGzip', 'createDeflate', 'createDeflateRaw']) { + for (const method of [ + 'createGzip', + 'createDeflate', + 'createDeflateRaw', + 'createBrotliCompress', + ]) { assert(method in zlib, `${method} is not available in "node:zlib"`); const { promise, resolve, reject } = Promise.withResolvers(); let compData = Buffer.alloc(0); @@ -823,8 +827,7 @@ export const zlibObjectWrite = { // https://github.com/nodejs/node/blob/3a71ccf6c473357e89be61b26739fd9139dce4db/test/parallel/test-zlib-zero-byte.js export const zlibZeroByte = { async test() { - // TODO(soon): Add BrotliCompress once it is implemented - for (const Compressor of [zlib.Gzip]) { + for (const Compressor of [zlib.Gzip, zlib.BrotliCompress]) { const { promise, resolve, reject } = Promise.withResolvers(); let endCalled = false; const gz = new Compressor(); @@ -1020,8 +1023,7 @@ export const zlibInvalidInput = { new zlib.Gunzip(), new zlib.Inflate(), new zlib.InflateRaw(), - // TODO(soon): Enable once BrotliDecompress is implemented. - // zlib.BrotliDecompress(), + new zlib.BrotliDecompress(), ]; for (const input of nonStringInputs) { diff --git a/src/workerd/api/node/zlib-util.c++ b/src/workerd/api/node/zlib-util.c++ index 3fb66352e14..122840286fe 100644 --- a/src/workerd/api/node/zlib-util.c++ +++ b/src/workerd/api/node/zlib-util.c++ @@ -4,18 +4,24 @@ // Copyright Joyent and Node contributors. All rights reserved. MIT license. #include "zlib-util.h" -#include "workerd/jsg/exception.h" +#include "nbytes.h" + +// The following implementation is adapted from Node.js +// and therefore follows Node.js style as opposed to kj style. +// Latest implementation of Node.js zlib can be found at: +// https://github.com/nodejs/node/blob/main/src/node_zlib.cc namespace workerd::api::node { + kj::ArrayPtr ZlibUtil::getInputFromSource(InputSource& data) { KJ_SWITCH_ONEOF(data) { KJ_CASE_ONEOF(dataBuf, kj::Array) { - JSG_REQUIRE(dataBuf.size() < Z_MAX_CHUNK, RangeError, "Memory limit exceeded"); + JSG_REQUIRE(dataBuf.size() < Z_MAX_CHUNK, RangeError, "Memory limit exceeded"_kj); return dataBuf.asPtr(); } KJ_CASE_ONEOF(dataStr, jsg::NonCoercible) { - JSG_REQUIRE(dataStr.value.size() < Z_MAX_CHUNK, RangeError, "Memory limit exceeded"); + JSG_REQUIRE(dataStr.value.size() < Z_MAX_CHUNK, RangeError, "Memory limit exceeded"_kj); return dataStr.value.asBytes(); } } @@ -40,27 +46,27 @@ public: maxCapacity = _maxCapacity; } - inline size_t size() const { + size_t size() const { return builder.size(); } - inline bool empty() const { + bool empty() const { return size() == 0; } - inline size_t capacity() const { + size_t capacity() const { return builder.capacity(); } - inline size_t available() const { + size_t available() const { return capacity() - size(); } - inline kj::byte* begin() KJ_LIFETIMEBOUND { + kj::byte* begin() KJ_LIFETIMEBOUND { return builder.begin(); } - inline kj::byte* end() KJ_LIFETIMEBOUND { + kj::byte* end() KJ_LIFETIMEBOUND { return builder.end(); } - inline kj::Array releaseAsArray() { + kj::Array releaseAsArray() { // TODO(perf): Avoid a copy/move by allowing Array to point to incomplete space? if (!builder.isFull()) { setCapacity(size()); @@ -68,20 +74,20 @@ public: return builder.finish(); } - inline void adjustUnused(size_t unused) { + void adjustUnused(size_t unused) { resize(capacity() - unused); } - inline void resize(size_t size) { + void resize(size_t size) { if (size > builder.capacity()) grow(size); builder.resize(size); } - inline void addChunk() { + void addChunk() { reserve(size() + chunkSize); } - inline void reserve(size_t size) { + void reserve(size_t size) { if (size > builder.capacity()) { grow(size); } @@ -438,18 +444,20 @@ void ZlibContext::setOutputBuffer(kj::ArrayPtr output) { stream.avail_out = output.size(); } -jsg::Ref ZlibUtil::ZlibStream::constructor(ZlibModeValue mode) { - return jsg::alloc(static_cast(mode)); +template +jsg::Ref> ZlibUtil::CompressionStream< + CompressionContext>::constructor(ZlibModeValue mode) { + return jsg::alloc(static_cast(mode)); } template -CompressionStream::~CompressionStream() noexcept(false) { +ZlibUtil::CompressionStream::~CompressionStream() { JSG_ASSERT(!writing, Error, "Writing to compression stream"_kj); close(); } template -void CompressionStream::emitError( +void ZlibUtil::CompressionStream::emitError( jsg::Lock& js, const CompressionError& error) { KJ_IF_SOME(onError, errorHandler) { onError(js, error.err, kj::mv(error.code), kj::mv(error.message)); @@ -463,7 +471,7 @@ void CompressionStream::emitError( template template -void CompressionStream::writeStream(jsg::Lock& js, +void ZlibUtil::CompressionStream::writeStream(jsg::Lock& js, int flush, kj::ArrayPtr input, uint32_t inputLength, @@ -476,11 +484,11 @@ void CompressionStream::writeStream(jsg::Lock& js, writing = true; - context.setBuffers(input, inputLength, output, outputLength); - context.setFlush(flush); + context()->setBuffers(input, inputLength, output, outputLength); + context()->setFlush(flush); if constexpr (!async) { - context.work(); + context()->work(); if (checkError(js)) { updateWriteResult(); writing = false; @@ -490,7 +498,7 @@ void CompressionStream::writeStream(jsg::Lock& js, // On Node.js, this is called as a result of `ScheduleWork()` call. // Since, we implement the whole thing as sync, we're going to ahead and call the whole thing here. - context.work(); + context()->work(); // This is implemented slightly differently in Node.js // Node.js calls AfterThreadPoolWork(). @@ -508,19 +516,19 @@ void CompressionStream::writeStream(jsg::Lock& js, } template -void CompressionStream::close() { +void ZlibUtil::CompressionStream::close() { pending_close = writing; if (writing) { return; } closed = true; JSG_ASSERT(initialized, Error, "Closing before initialized"_kj); - context.close(); + context()->close(); } template -bool CompressionStream::checkError(jsg::Lock& js) { - KJ_IF_SOME(error, context.getError()) { +bool ZlibUtil::CompressionStream::checkError(jsg::Lock& js) { + KJ_IF_SOME(error, context()->getError()) { emitError(js, kj::mv(error)); return false; } @@ -528,7 +536,7 @@ bool CompressionStream::checkError(jsg::Lock& js) { } template -void CompressionStream::initializeStream( +void ZlibUtil::CompressionStream::initializeStream( jsg::BufferSource _writeResult, jsg::Function _writeCallback) { writeResult = kj::mv(_writeResult); writeCallback = kj::mv(_writeCallback); @@ -536,35 +544,21 @@ void CompressionStream::initializeStream( } template -void CompressionStream::updateWriteResult() { +void ZlibUtil::CompressionStream::updateWriteResult() { KJ_IF_SOME(wr, writeResult) { auto ptr = wr.template asArrayPtr(); - context.getAfterWriteResult(&ptr[1], &ptr[0]); + context()->getAfterWriteResult(&ptr[1], &ptr[0]); } } -ZlibUtil::ZlibStream::ZlibStream(ZlibMode mode): CompressionStream() { - context.setMode(mode); -} - -void ZlibUtil::ZlibStream::initialize(int windowBits, - int level, - int memLevel, - int strategy, - jsg::BufferSource writeState, - jsg::Function writeCallback, - jsg::Optional> dictionary) { - initializeStream(kj::mv(writeState), kj::mv(writeCallback)); - context.initialize(level, windowBits, memLevel, strategy, kj::mv(dictionary)); -} - -template -void ZlibUtil::ZlibStream::write_(jsg::Lock& js, +template +template +void ZlibUtil::CompressionStream::write(jsg::Lock& js, int flush, jsg::Optional> input, int inputOffset, int inputLength, - kj::ArrayPtr output, + kj::Array output, int outputOffset, int outputLength) { if (flush != Z_NO_FLUSH && flush != Z_PARTIAL_FLUSH && flush != Z_SYNC_FLUSH && @@ -590,41 +584,111 @@ void ZlibUtil::ZlibStream::write_(jsg::Lock& js, output.slice(outputOffset), outputLength); } -void ZlibUtil::ZlibStream::write(jsg::Lock& js, - int flush, - jsg::Optional> input, - int inputOffset, - int inputLength, - kj::Array output, - int outputOffset, - int outputLength) { - write_(js, flush, kj::mv(input), inputOffset, inputLength, output.asPtr(), outputOffset, - outputLength); +template +void ZlibUtil::CompressionStream::reset(jsg::Lock& js) { + KJ_IF_SOME(error, context()->resetStream()) { + emitError(js, kj::mv(error)); + } } -void ZlibUtil::ZlibStream::writeSync(jsg::Lock& js, - int flush, - jsg::Optional> input, - int inputOffset, - int inputLength, - kj::Array output, - int outputOffset, - int outputLength) { - write_(js, flush, kj::mv(input), inputOffset, inputLength, output.asPtr(), outputOffset, - outputLength); +jsg::Ref ZlibUtil::ZlibStream::constructor(ZlibModeValue mode) { + return jsg::alloc(static_cast(mode)); +} + +void ZlibUtil::ZlibStream::initialize(int windowBits, + int level, + int memLevel, + int strategy, + jsg::BufferSource writeState, + jsg::Function writeCallback, + jsg::Optional> dictionary) { + initializeStream(kj::mv(writeState), kj::mv(writeCallback)); + context()->setAllocationFunctions(AllocForZlib, FreeForZlib, this); + context()->initialize(level, windowBits, memLevel, strategy, kj::mv(dictionary)); } void ZlibUtil::ZlibStream::params(jsg::Lock& js, int _level, int _strategy) { - context.setParams(_level, _strategy); - KJ_IF_SOME(err, context.getError()) { + context()->setParams(_level, _strategy); + KJ_IF_SOME(err, context()->getError()) { emitError(js, kj::mv(err)); } } -void ZlibUtil::ZlibStream::reset(jsg::Lock& js) { - KJ_IF_SOME(error, context.resetStream()) { - emitError(js, kj::mv(error)); +void BrotliContext::setBuffers(kj::ArrayPtr input, + uint32_t inputLength, + kj::ArrayPtr output, + uint32_t outputLength) { + nextIn = reinterpret_cast(input.begin()); + nextOut = output.begin(); + availIn = inputLength; + availOut = outputLength; +} + +void BrotliContext::setFlush(int _flush) { + flush = static_cast(_flush); +} + +void BrotliContext::getAfterWriteResult(uint32_t* _availIn, uint32_t* _availOut) const { + *_availIn = availIn; + *_availOut = availOut; +} + +BrotliEncoderContext::BrotliEncoderContext(ZlibMode _mode): BrotliContext(_mode) { + auto instance = BrotliEncoderCreateInstance(alloc_brotli, free_brotli, alloc_opaque_brotli); + state = kj::disposeWith(instance); +} + +void BrotliEncoderContext::close() { + auto instance = BrotliEncoderCreateInstance(alloc_brotli, free_brotli, alloc_opaque_brotli); + state = kj::disposeWith(kj::mv(instance)); + mode = ZlibMode::NONE; +} + +void BrotliEncoderContext::work() { + JSG_REQUIRE(mode == ZlibMode::BROTLI_ENCODE, Error, "Mode should be BROTLI_ENCODE"_kj); + JSG_REQUIRE_NONNULL(state.get(), Error, "State should not be empty"_kj); + + const uint8_t* internalNext = nextIn; + lastResult = BrotliEncoderCompressStream( + state.get(), flush, &availIn, &internalNext, &availOut, &nextOut, nullptr); + nextIn += internalNext - nextIn; +} + +kj::Maybe BrotliEncoderContext::initialize( + brotli_alloc_func init_alloc_func, brotli_free_func init_free_func, void* init_opaque_func) { + alloc_brotli = init_alloc_func; + free_brotli = init_free_func; + alloc_opaque_brotli = init_opaque_func; + + auto instance = BrotliEncoderCreateInstance(alloc_brotli, free_brotli, alloc_opaque_brotli); + state = kj::disposeWith(kj::mv(instance)); + + if (state.get() == nullptr) { + return CompressionError( + "Could not initialize Brotli instance"_kj, "ERR_ZLIB_INITIALIZATION_FAILED"_kj, -1); + } + + return kj::none; +} + +kj::Maybe BrotliEncoderContext::resetStream() { + return initialize(alloc_brotli, free_brotli, alloc_opaque_brotli); +} + +kj::Maybe BrotliEncoderContext::setParams(int key, uint32_t value) { + if (!BrotliEncoderSetParameter(state.get(), static_cast(key), value)) { + return CompressionError("Setting parameter failed", "ERR_BROTLI_PARAM_SET_FAILED", -1); + } + + return kj::none; +} + +kj::Maybe BrotliEncoderContext::getError() const { + if (!lastResult) { + return CompressionError("Compression failed", "ERR_BROTLI_COMPRESSION_FAILED", -1); } + + return kj::none; } kj::Array syncProcessBuffer(ZlibContext& ctx, GrowableBuffer& result) { @@ -682,4 +746,167 @@ void ZlibUtil::zlibWithCallback( cb(js, tunneledError.message, kj::none); } } + +BrotliDecoderContext::BrotliDecoderContext(ZlibMode _mode): BrotliContext(_mode) { + auto instance = BrotliDecoderCreateInstance(alloc_brotli, free_brotli, alloc_opaque_brotli); + state = kj::disposeWith(instance); +} + +void BrotliDecoderContext::close() { + auto instance = BrotliDecoderCreateInstance(alloc_brotli, free_brotli, alloc_opaque_brotli); + state = kj::disposeWith(kj::mv(instance)); + mode = ZlibMode::NONE; +} + +kj::Maybe BrotliDecoderContext::initialize( + brotli_alloc_func init_alloc_func, brotli_free_func init_free_func, void* init_opaque_func) { + alloc_brotli = init_alloc_func; + free_brotli = init_free_func; + alloc_opaque_brotli = init_opaque_func; + + auto instance = BrotliDecoderCreateInstance(alloc_brotli, free_brotli, alloc_opaque_brotli); + state = kj::disposeWith(kj::mv(instance)); + + if (state.get() == nullptr) { + return CompressionError( + "Could not initialize Brotli instance", "ERR_ZLIB_INITIALIZATION_FAILED", -1); + } + + return kj::none; +} + +void BrotliDecoderContext::work() { + JSG_REQUIRE(mode == ZlibMode::BROTLI_DECODE, Error, "Mode should have been BROTLI_DECODE"_kj); + JSG_REQUIRE_NONNULL(state.get(), Error, "State should not be empty"_kj); + const uint8_t* internalNext = nextIn; + lastResult = BrotliDecoderDecompressStream( + state.get(), &availIn, &internalNext, &availOut, &nextOut, nullptr); + nextIn += internalNext - nextIn; + + if (lastResult == BROTLI_DECODER_RESULT_ERROR) { + error = BrotliDecoderGetErrorCode(state.get()); + errorString = kj::str("ERR_", BrotliDecoderErrorString(error)); + } +} + +kj::Maybe BrotliDecoderContext::resetStream() { + return initialize(alloc_brotli, free_brotli, alloc_opaque_brotli); +} + +kj::Maybe BrotliDecoderContext::setParams(int key, uint32_t value) { + if (!BrotliDecoderSetParameter(state.get(), static_cast(key), value)) { + return CompressionError("Setting parameter failed", "ERR_BROTLI_PARAM_SET_FAILED", -1); + } + + return kj::none; +} + +kj::Maybe BrotliDecoderContext::getError() const { + if (error != BROTLI_DECODER_NO_ERROR) { + return CompressionError("Compression failed", errorString, -1); + } + + if (flush == BROTLI_OPERATION_FINISH && lastResult == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT) { + // Match zlib behaviour, as brotli doesn't have its own code for this. + return CompressionError("Unexpected end of file", "Z_BUF_ERROR", Z_BUF_ERROR); + } + + return kj::none; +} + +template +jsg::Ref> ZlibUtil::BrotliCompressionStream< + CompressionContext>::constructor(ZlibModeValue mode) { + return jsg::alloc(static_cast(mode)); +} + +template +bool ZlibUtil::BrotliCompressionStream::initialize(jsg::Lock& js, + jsg::BufferSource params, + jsg::BufferSource writeResult, + jsg::Function writeCallback) { + auto results = writeResult.template asArrayPtr(); + this->initializeStream(kj::mv(writeResult), kj::mv(writeCallback)); + auto maybeError = + this->context()->initialize(CompressionStream::AllocForBrotli, + CompressionStream::FreeForZlib, + static_cast*>(this)); + + KJ_IF_SOME(err, maybeError) { + this->emitError(js, kj::mv(err)); + return false; + } + + for (int i = 0; i < results.size(); i++) { + if (results[i] == static_cast(-1)) { + continue; + } + + maybeError = this->context()->setParams(i, results[i]); + KJ_IF_SOME(err, maybeError) { + this->emitError(js, kj::mv(err)); + return false; + } + } + return true; +} + +template +void* ZlibUtil::CompressionStream::AllocForZlib( + void* data, uInt items, uInt size) { + size_t real_size = + nbytes::MultiplyWithOverflowCheck(static_cast(items), static_cast(size)); + return AllocForBrotli(data, real_size); +} + +template +void* ZlibUtil::CompressionStream::AllocForBrotli(void* data, size_t size) { + size += sizeof(size_t); + auto* ctx = static_cast(data); + auto memory = kj::heapArray(size); + auto begin = memory.begin(); + // TODO(soon): Check if we need to store the size of the block in the pointer like Node.js + *reinterpret_cast(begin) = size; + ctx->allocations.insert(begin, kj::mv(memory)); + return begin + sizeof(size_t); +} + +template +void ZlibUtil::CompressionStream::FreeForZlib(void* data, void* pointer) { + if (KJ_UNLIKELY(pointer == nullptr)) return; + auto* ctx = static_cast(data); + auto real_pointer = static_cast(pointer) - sizeof(size_t); + JSG_REQUIRE(ctx->allocations.erase(real_pointer), Error, "Zlib allocation should exist"_kj); +} + +#ifndef CREATE_TEMPLATE +#define CREATE_TEMPLATE(T) \ + template void* ZlibUtil::CompressionStream::AllocForZlib(void* data, uInt items, uInt size); \ + template void* ZlibUtil::CompressionStream::AllocForBrotli(void* data, size_t size); \ + template void ZlibUtil::CompressionStream::FreeForZlib(void* data, void* pointer); \ + template void ZlibUtil::CompressionStream::reset(jsg::Lock& js); \ + template void ZlibUtil::CompressionStream::write(jsg::Lock & js, int flush, \ + jsg::Optional> input, int inputOffset, int inputLength, \ + kj::Array output, int outputOffset, int outputLength); \ + template void ZlibUtil::CompressionStream::write(jsg::Lock & js, int flush, \ + jsg::Optional> input, int inputOffset, int inputLength, \ + kj::Array output, int outputOffset, int outputLength); \ + template jsg::Ref> ZlibUtil::CompressionStream::constructor( \ + ZlibModeValue mode); + +CREATE_TEMPLATE(ZlibContext) +CREATE_TEMPLATE(BrotliEncoderContext) +CREATE_TEMPLATE(BrotliDecoderContext) + +template jsg::Ref> ZlibUtil:: + BrotliCompressionStream::constructor(ZlibModeValue mode); +template jsg::Ref> ZlibUtil:: + BrotliCompressionStream::constructor(ZlibModeValue mode); +template bool ZlibUtil::BrotliCompressionStream::initialize( + jsg::Lock&, jsg::BufferSource, jsg::BufferSource, jsg::Function); +template bool ZlibUtil::BrotliCompressionStream::initialize( + jsg::Lock&, jsg::BufferSource, jsg::BufferSource, jsg::Function); + +#undef CREATE_TEMPLATE +#endif } // namespace workerd::api::node diff --git a/src/workerd/api/node/zlib-util.h b/src/workerd/api/node/zlib-util.h index ce7b2be89b8..e6b78cba2c2 100644 --- a/src/workerd/api/node/zlib-util.h +++ b/src/workerd/api/node/zlib-util.h @@ -11,13 +11,16 @@ #include #include -#include #include #include -#include -#include +#include +#include +// The following implementation is adapted from Node.js +// and therefore follows Node.js style as opposed to kj style. +// Latest implementation of Node.js zlib can be found at: +// https://github.com/nodejs/node/blob/main/src/node_zlib.cc namespace workerd::api::node { #ifndef ZLIB_ERROR_CODES @@ -91,6 +94,7 @@ struct CompressionError { class ZlibContext { public: + explicit ZlibContext(ZlibMode _mode): mode(_mode) {} ZlibContext() = default; KJ_DISALLOW_COPY(ZlibContext); @@ -121,6 +125,11 @@ class ZlibContext { }; kj::Maybe resetStream(); kj::Maybe getError() const; + void setAllocationFunctions(alloc_func alloc, free_func free, void* opaque) { + stream.zalloc = alloc; + stream.zfree = free; + stream.opaque = opaque; + } // Equivalent to Node.js' `DoThreadPoolWork` function. // Ref: https://github.com/nodejs/node/blob/9edf4a0856681a7665bd9dcf2ca7cac252784b98/src/node_zlib.cc#L760 @@ -173,42 +182,72 @@ class ZlibContext { using CompressionStreamErrorHandler = jsg::Function; -template -class CompressionStream { +class BrotliContext { public: - CompressionStream() = default; - ~CompressionStream() noexcept(false); - KJ_DISALLOW_COPY_AND_MOVE(CompressionStream); - - void close(); - bool checkError(jsg::Lock& js); - void emitError(jsg::Lock& js, const CompressionError& error); - template - void writeStream(jsg::Lock& js, - int flush, - kj::ArrayPtr input, + explicit BrotliContext(ZlibMode _mode): mode(_mode) {} + KJ_DISALLOW_COPY(BrotliContext); + void setBuffers(kj::ArrayPtr input, uint32_t inputLength, kj::ArrayPtr output, uint32_t outputLength); - void setErrorHandler(CompressionStreamErrorHandler handler) { - errorHandler = kj::mv(handler); - }; - void initializeStream(jsg::BufferSource _write_result, jsg::Function writeCallback); - void updateWriteResult(); + void setFlush(int flush); + void getAfterWriteResult(uint32_t* availIn, uint32_t* availOut) const; + void setMode(ZlibMode _mode) { + mode = _mode; + } protected: - CompressionContext context; + ZlibMode mode; + const uint8_t* nextIn = nullptr; + uint8_t* nextOut = nullptr; + size_t availIn = 0; + size_t availOut = 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_brotli = nullptr; + brotli_free_func free_brotli = nullptr; + void* alloc_opaque_brotli = nullptr; +}; + +class BrotliEncoderContext final: public BrotliContext { +public: + explicit BrotliEncoderContext(ZlibMode _mode); + + void close(); + // Equivalent to Node.js' `DoThreadPoolWork` implementation. + void work(); + kj::Maybe initialize( + brotli_alloc_func init_alloc_func, brotli_free_func init_free_func, void* init_opaque_func); + kj::Maybe resetStream(); + kj::Maybe setParams(int key, uint32_t value); + kj::Maybe getError() const; private: - bool initialized = false; - bool writing = false; - bool pending_close = false; - bool closed = false; - - // Equivalent to `write_js_callback` in Node.js - jsg::Optional> writeCallback; - jsg::Optional writeResult; - jsg::Optional errorHandler; + bool lastResult = false; + kj::Own state; +}; + +class BrotliDecoderContext final: public BrotliContext { +public: + explicit BrotliDecoderContext(ZlibMode _mode); + + void close(); + // Equivalent to Node.js' `DoThreadPoolWork` implementation. + void work(); + kj::Maybe initialize( + brotli_alloc_func init_alloc_func, brotli_free_func init_free_func, void* init_opaque_func); + kj::Maybe resetStream(); + kj::Maybe setParams(int key, uint32_t value); + kj::Maybe getError() const; + +private: + BrotliDecoderResult lastResult = BROTLI_DECODER_RESULT_SUCCESS; + BrotliDecoderErrorCode error = BROTLI_DECODER_NO_ERROR; + kj::String errorString; + kj::Own state; }; // Implements utilities in support of the Node.js Zlib @@ -217,31 +256,33 @@ class ZlibUtil final: public jsg::Object { ZlibUtil() = default; ZlibUtil(jsg::Lock&, const jsg::Url&) {} - class ZlibStream final: public jsg::Object, public CompressionStream { + template + class CompressionStream: public jsg::Object { public: - ZlibStream(ZlibMode mode); - KJ_DISALLOW_COPY_AND_MOVE(ZlibStream); - static jsg::Ref constructor(ZlibModeValue mode); + explicit CompressionStream(ZlibMode _mode): context_(_mode) {} + CompressionStream() = default; + ~CompressionStream(); + KJ_DISALLOW_COPY_AND_MOVE(CompressionStream); - // Instance methods - void initialize(int windowBits, - int level, - int memLevel, - int strategy, - jsg::BufferSource writeState, - jsg::Function writeCallback, - jsg::Optional> dictionary); + static jsg::Ref constructor(ZlibModeValue mode); + + void close(); + bool checkError(jsg::Lock& js); + void emitError(jsg::Lock& js, const CompressionError& error); template - void write_(jsg::Lock& js, + void writeStream(jsg::Lock& js, int flush, - jsg::Optional> input, - int inputOffset, - int inputLength, + kj::ArrayPtr input, + uint32_t inputLength, kj::ArrayPtr output, - int outputOffset, - int outputLength); + uint32_t outputLength); + void setErrorHandler(CompressionStreamErrorHandler handler) { + errorHandler = kj::mv(handler); + } + + void updateWriteResult(); - // TODO(soon): Find a way to expose functions with templates using JSG_METHOD. + template void write(jsg::Lock& js, int flush, jsg::Optional> input, @@ -250,38 +291,113 @@ class ZlibUtil final: public jsg::Object { kj::Array output, int outputOffset, int outputLength); - void writeSync(jsg::Lock& js, - int flush, - jsg::Optional> input, - int inputOffset, - int inputLength, - kj::Array output, - int outputOffset, - int outputLength); - void params(jsg::Lock& js, int level, int strategy); void reset(jsg::Lock& js); + JSG_RESOURCE_TYPE(CompressionStream) { + JSG_METHOD(close); + JSG_METHOD_NAMED(write, template write); + JSG_METHOD_NAMED(writeSync, template write); + JSG_METHOD(reset); + JSG_METHOD(setErrorHandler); + } + + protected: + CompressionContext* context() { + return &context_; + } + + void initializeStream(jsg::BufferSource _write_result, jsg::Function writeCallback); + + // Allocation functions provided to zlib itself. We store the real size of + // the allocated memory chunk just before the "payload" memory we return + // to zlib. + static void* AllocForZlib(void* data, uInt items, uInt size); + static void* AllocForBrotli(void* data, size_t size); + static void FreeForZlib(void* data, void* pointer); + + private: + // Used to store allocations in Brotli* operations. + // This declaration should be physically positioned before + // context to avoid `heap-use-after-free` ASan error. + kj::HashMap> allocations; + + CompressionContext context_; + bool initialized = false; + bool writing = false; + bool pending_close = false; + bool closed = false; + + // Equivalent to `write_js_callback` in Node.js + jsg::Optional> writeCallback; + jsg::Optional writeResult; + jsg::Optional errorHandler; + }; + + class ZlibStream final: public CompressionStream { + public: + explicit ZlibStream(ZlibMode mode): CompressionStream(mode) {} + KJ_DISALLOW_COPY_AND_MOVE(ZlibStream); + static jsg::Ref constructor(ZlibModeValue mode); + + // Instance methods + void initialize(int windowBits, + int level, + int memLevel, + int strategy, + jsg::BufferSource writeState, + jsg::Function writeCallback, + jsg::Optional> dictionary); + void params(jsg::Lock& js, int level, int strategy); + JSG_RESOURCE_TYPE(ZlibStream) { + JSG_INHERIT(CompressionStream); + + JSG_METHOD(initialize); + JSG_METHOD(params); + } + }; + + template + class BrotliCompressionStream: public CompressionStream { + public: + explicit BrotliCompressionStream(ZlibMode _mode) + : CompressionStream(_mode) {} + KJ_DISALLOW_COPY_AND_MOVE(BrotliCompressionStream); + static jsg::Ref constructor(ZlibModeValue mode); + + bool initialize(jsg::Lock& js, + jsg::BufferSource params, + jsg::BufferSource writeResult, + jsg::Function writeCallback); + + void params() { + // Currently a no-op, and not accessed from JS land. + // At some point Brotli may support changing parameters on the fly, + // in which case we can implement this and a JS equivalent similar to + // the zlib Params() function. + } + + JSG_RESOURCE_TYPE(BrotliCompressionStream) { + JSG_INHERIT(CompressionStream); + JSG_METHOD(initialize); - JSG_METHOD(close); - JSG_METHOD(write); - JSG_METHOD(writeSync); JSG_METHOD(params); - JSG_METHOD(setErrorHandler); - JSG_METHOD(reset); + } + + CompressionContext* context() { + return this->CompressionStream::context(); } }; struct Options { jsg::Optional flush; jsg::Optional finishFlush; - jsg::Optional chunkSize; + jsg::Optional chunkSize; jsg::Optional windowBits; jsg::Optional level; jsg::Optional memLevel; jsg::Optional strategy; jsg::Optional> dictionary; - // We'll handle info on the JS side for now jsg::Optional maxOutputLength; JSG_STRUCT(flush, @@ -307,10 +423,13 @@ class ZlibUtil final: public jsg::Object { JSG_RESOURCE_TYPE(ZlibUtil) { JSG_METHOD_NAMED(crc32, crc32Sync); - JSG_NESTED_TYPE(ZlibStream); JSG_METHOD(zlibSync); JSG_METHOD_NAMED(zlib, zlibWithCallback); + JSG_NESTED_TYPE(ZlibStream); + JSG_NESTED_TYPE_NAMED(BrotliCompressionStream, BrotliEncoder); + JSG_NESTED_TYPE_NAMED(BrotliCompressionStream, BrotliDecoder); + // zlib.constants (part of the API contract for node:zlib) JSG_STATIC_CONSTANT_NAMED(CONST_Z_NO_FLUSH, Z_NO_FLUSH); JSG_STATIC_CONSTANT_NAMED(CONST_Z_PARTIAL_FLUSH, Z_PARTIAL_FLUSH); @@ -458,9 +577,19 @@ class ZlibUtil final: public jsg::Object { BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES); JSG_STATIC_CONSTANT_NAMED( CONST_BROTLI_DECODER_ERROR_UNREACHABLE, BROTLI_DECODER_ERROR_UNREACHABLE); - }; + } }; #define EW_NODE_ZLIB_ISOLATE_TYPES \ - api::node::ZlibUtil, api::node::ZlibUtil::ZlibStream, api::node::ZlibUtil::Options + api::node::ZlibUtil, api::node::ZlibUtil::ZlibStream, \ + api::node::ZlibUtil::BrotliCompressionStream, \ + api::node::ZlibUtil::BrotliCompressionStream, \ + api::node::ZlibUtil::CompressionStream, \ + api::node::ZlibUtil::CompressionStream, \ + api::node::ZlibUtil::CompressionStream, \ + api::node::ZlibUtil::Options + } // namespace workerd::api::node + +KJ_DECLARE_NON_POLYMORPHIC(BrotliEncoderStateStruct) +KJ_DECLARE_NON_POLYMORPHIC(BrotliDecoderStateStruct)