diff --git a/src/node/internal/internal_zlib.ts b/src/node/internal/internal_zlib.ts index 7c2836792ee..81400c6d13f 100644 --- a/src/node/internal/internal_zlib.ts +++ b/src/node/internal/internal_zlib.ts @@ -3,11 +3,14 @@ // 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 CompressCallback, +} 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 { isArrayBufferView } from 'node-internal:internal_types'; import { Zlib } from 'node-internal:internal_zlib_base'; const { @@ -20,6 +23,253 @@ const { CONST_UNZIP, } = zlibUtil; +export function crc32( + data: ArrayBufferView | string, + value: number = 0 +): number { + validateUint32(value, 'value'); + return zlibUtil.crc32(data, value); +} + +export function inflateSync( + data: ArrayBufferView | string, + options: ZlibOptions = {} +): Buffer { + return Buffer.from(zlibUtil.zlibSync(data, options, zlibUtil.CONST_INFLATE)); +} + +export function deflateSync( + data: ArrayBufferView | string, + options: ZlibOptions = {} +): Buffer { + return Buffer.from(zlibUtil.zlibSync(data, options, zlibUtil.CONST_DEFLATE)); +} + +export function gunzipSync( + data: ArrayBufferView | string, + options: ZlibOptions = {} +): Buffer { + return Buffer.from(zlibUtil.zlibSync(data, options, zlibUtil.CONST_GUNZIP)); +} + +export function gzipSync( + data: ArrayBufferView | string, + options: ZlibOptions = {} +): Buffer { + return Buffer.from(zlibUtil.zlibSync(data, options, zlibUtil.CONST_GZIP)); +} + +export function inflateRawSync( + data: ArrayBufferView | string, + options: ZlibOptions = {} +): Buffer { + return Buffer.from( + zlibUtil.zlibSync(data, options, zlibUtil.CONST_INFLATERAW) + ); +} + +export function deflateRawSync( + data: ArrayBufferView | string, + options: ZlibOptions = {} +): Buffer { + return Buffer.from( + zlibUtil.zlibSync(data, options, zlibUtil.CONST_DEFLATERAW) + ); +} + +export function unzipSync( + data: ArrayBufferView | string, + options: ZlibOptions = {} +): Buffer { + return Buffer.from( + zlibUtil.zlibSync(data, options, zlibUtil.CONST_DEFLATERAW) + ); +} + +function normalizeArgs( + optionsOrCallback: ZlibOptions | CompressCallback, + callbackOrUndefined?: CompressCallback +): [ZlibOptions, CompressCallback] { + if (typeof optionsOrCallback === 'function') { + return [{}, optionsOrCallback]; + } else if (typeof callbackOrUndefined === 'function') { + return [optionsOrCallback, callbackOrUndefined]; + } + + throw new ERR_INVALID_ARG_TYPE('callback', 'Function', callbackOrUndefined); +} + +function wrapCallback( + callback: CompressCallback +): CompressCallback { + return (error: string | undefined, result: ArrayBuffer | undefined) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + queueMicrotask(() => { + callback( + error ? new Error(error) : undefined, + result ? Buffer.from(result) : undefined + ); + }); + }; +} + +export function inflate( + data: ArrayBufferView | string, + callback: CompressCallback +): void; +export function inflate( + data: ArrayBufferView | string, + options: ZlibOptions, + callback: CompressCallback +): void; +export function inflate( + data: ArrayBufferView | string, + optionsOrCallback: ZlibOptions | CompressCallback, + callbackOrUndefined?: CompressCallback +): void { + const [options, callback] = normalizeArgs( + optionsOrCallback, + callbackOrUndefined + ); + zlibUtil.zlib(data, options, zlibUtil.CONST_INFLATE, wrapCallback(callback)); +} + +export function unzip( + data: ArrayBufferView | string, + callback: CompressCallback +): void; +export function unzip( + data: ArrayBufferView | string, + options: ZlibOptions, + callback: CompressCallback +): void; +export function unzip( + data: ArrayBufferView | string, + optionsOrCallback: ZlibOptions | CompressCallback, + callbackOrUndefined?: CompressCallback +): void { + const [options, callback] = normalizeArgs( + optionsOrCallback, + callbackOrUndefined + ); + zlibUtil.zlib(data, options, zlibUtil.CONST_UNZIP, wrapCallback(callback)); +} + +export function inflateRaw( + data: ArrayBufferView | string, + callback: CompressCallback +): void; +export function inflateRaw( + data: ArrayBufferView | string, + options: ZlibOptions, + callback: CompressCallback +): void; +export function inflateRaw( + data: ArrayBufferView | string, + optionsOrCallback: ZlibOptions | CompressCallback, + callbackOrUndefined?: CompressCallback +): void { + const [options, callback] = normalizeArgs( + optionsOrCallback, + callbackOrUndefined + ); + zlibUtil.zlib( + data, + options, + zlibUtil.CONST_INFLATERAW, + wrapCallback(callback) + ); +} + +export function gunzip( + data: ArrayBufferView | string, + callback: CompressCallback +): void; +export function gunzip( + data: ArrayBufferView | string, + options: ZlibOptions, + callback: CompressCallback +): void; +export function gunzip( + data: ArrayBufferView | string, + optionsOrCallback: ZlibOptions | CompressCallback, + callbackOrUndefined?: CompressCallback +): void { + const [options, callback] = normalizeArgs( + optionsOrCallback, + callbackOrUndefined + ); + zlibUtil.zlib(data, options, zlibUtil.CONST_GUNZIP, wrapCallback(callback)); +} + +export function deflate( + data: ArrayBufferView | string, + callback: CompressCallback +): void; +export function deflate( + data: ArrayBufferView | string, + options: ZlibOptions, + callback: CompressCallback +): void; +export function deflate( + data: ArrayBufferView | string, + optionsOrCallback: ZlibOptions | CompressCallback, + callbackOrUndefined?: CompressCallback +): void { + const [options, callback] = normalizeArgs( + optionsOrCallback, + callbackOrUndefined + ); + zlibUtil.zlib(data, options, zlibUtil.CONST_DEFLATE, wrapCallback(callback)); +} + +export function deflateRaw( + data: ArrayBufferView | string, + callback: CompressCallback +): void; +export function deflateRaw( + data: ArrayBufferView | string, + options: ZlibOptions, + callback: CompressCallback +): void; +export function deflateRaw( + data: ArrayBufferView | string, + optionsOrCallback: ZlibOptions | CompressCallback, + callbackOrUndefined?: CompressCallback +): void { + const [options, callback] = normalizeArgs( + optionsOrCallback, + callbackOrUndefined + ); + zlibUtil.zlib( + data, + options, + zlibUtil.CONST_DEFLATERAW, + wrapCallback(callback) + ); +} + +export function gzip( + data: ArrayBufferView | string, + callback: CompressCallback +): void; +export function gzip( + data: ArrayBufferView | string, + options: ZlibOptions, + callback: CompressCallback +): void; +export function gzip( + data: ArrayBufferView | string, + optionsOrCallback: ZlibOptions | CompressCallback, + callbackOrUndefined?: CompressCallback +): void { + const [options, callback] = normalizeArgs( + optionsOrCallback, + callbackOrUndefined + ); + zlibUtil.zlib(data, options, zlibUtil.CONST_GZIP, wrapCallback(callback)); +} + const constPrefix = 'CONST_'; export const constants: Record = {}; @@ -41,19 +291,6 @@ Object.defineProperties( ) ); -export function crc32( - data: ArrayBufferView | string, - value: number = 0 -): number { - if (typeof data === 'string') { - data = Buffer.from(data); - } else if (!isArrayBufferView(data)) { - throw new ERR_INVALID_ARG_TYPE('data', 'ArrayBufferView', typeof data); - } - validateUint32(value, 'value'); - return zlibUtil.crc32(data, value); -} - export class Gzip extends Zlib { public constructor(options: ZlibOptions) { super(options, CONST_GZIP); diff --git a/src/node/internal/zlib.d.ts b/src/node/internal/zlib.d.ts index ce050f1110b..a053454e7dd 100644 --- a/src/node/internal/zlib.d.ts +++ b/src/node/internal/zlib.d.ts @@ -2,6 +2,24 @@ import { owner_symbol, type Zlib } from 'node-internal:internal_zlib_base'; export function crc32(data: ArrayBufferView, value: number): number; +export type CompressCallback = ( + error?: ErrT, + result?: BufT +) => void; + +export function crc32(data: ArrayBufferView | string, value: number): number; +export function zlibSync( + data: ArrayBufferView | string, + options: ZlibOptions, + mode: number +): ArrayBuffer; +export function zlib( + data: ArrayBufferView | string, + options: ZlibOptions, + mode: number, + cb: CompressCallback +): ArrayBuffer; + // zlib.constants (part of the API contract for node:zlib) export const CONST_Z_NO_FLUSH: number; export const CONST_Z_PARTIAL_FLUSH: number; diff --git a/src/node/zlib.ts b/src/node/zlib.ts index efecda82e4c..dd4e7413063 100644 --- a/src/node/zlib.ts +++ b/src/node/zlib.ts @@ -29,6 +29,24 @@ const createInflate = protectMethod(zlib.createInflate); const createInflateRaw = protectMethod(zlib.createInflateRaw); const createUnzip = protectMethod(zlib.createUnzip); +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); + export { crc32, constants, @@ -50,6 +68,22 @@ export { createInflate, createInflateRaw, createUnzip, + + // One-shot methods + inflate, + inflateSync, + deflate, + deflateSync, + inflateRaw, + inflateRawSync, + deflateRaw, + deflateRawSync, + gzip, + gzipSync, + gunzip, + gunzipSync, + unzip, + unzipSync, }; export default { @@ -73,4 +107,20 @@ export default { createInflate, createInflateRaw, createUnzip, + + // One-shot methods + inflate, + inflateSync, + deflate, + deflateSync, + inflateRaw, + inflateRawSync, + deflateRaw, + deflateRawSync, + gzip, + gzipSync, + gunzip, + gunzipSync, + unzip, + unzipSync, }; diff --git a/src/workerd/api/node/tests/zlib-nodejs-test.js b/src/workerd/api/node/tests/zlib-nodejs-test.js index 0cc072a3505..3e1d4f85758 100644 --- a/src/workerd/api/node/tests/zlib-nodejs-test.js +++ b/src/workerd/api/node/tests/zlib-nodejs-test.js @@ -239,12 +239,9 @@ export const crc32Test = { } [undefined, null, true, 1, () => {}, {}].forEach((invalid) => { - throws( - () => { - zlib.crc32(invalid); - }, - { code: 'ERR_INVALID_ARG_TYPE' } - ); + throws(() => { + zlib.crc32(invalid); + }, new TypeError("Failed to execute 'crc32' on 'ZlibUtil': parameter 1 is not of type 'string or ArrayBuffer or ArrayBufferView'.")); }); [null, true, () => {}, {}].forEach((invalid) => { @@ -667,3 +664,148 @@ export const testFailedInit = { // - [x] test-zlib-failed-init.js // - [ ] test-zlib-invalid-input.js // - [ ] test-zlib-reset-before-write.js + +const BIG_DATA = 'horse'.repeat(50_000) + 'cow'.repeat(49_000); + +export const inflateSyncTest = { + test() { + strictEqual( + zlib.inflateSync(zlib.deflateSync(BIG_DATA)).toString(), + BIG_DATA + ); + + throws( + () => zlib.inflateSync('garbage data'), + new Error('incorrect header check') + ); + + strictEqual( + zlib + .inflateSync(Buffer.from('OE9LyixKUUiCEQAmfgUk', 'base64'), { + windowBits: 11, + level: 4, + }) + .toString(), + 'bird bird bird' + ); + }, +}; + +export const zipBombTest = { + test() { + // 225 bytes (raw) + const ZLIB_BOMB_3 = + 'eNqruPX2jqHeUkaJtov/25xEzoho9fL6aF70yPBZvUBsqZPmzHO+F253euVbWScs28QQ2LG' + + 'CpdBc8ef84mkfpubvs6n/Z/9vrk9b4oy4ffG2ujp8DAwMB074/p97traX8fCt43fPvbMDCh' + + 'mUtZ/2/rw58MHbhNLblkCBhC96ZuZ1Z+xY5vn3P5/LDBRxy3/6NTF7G+udupj/d4r5gSISe' + + '6sPAikGD0EeINkgBuZkQDgSyJxRmVGZUZlRmVGZURlCMpZRcVE/bjGdi/fzr+O/n/P32XWD' + + 'nyxJaZvuAwDXRDs+'; + + // 1799 bytes + const zlib_bomb_2 = zlib.inflateSync(Buffer.from(ZLIB_BOMB_3, 'base64')); + + // ~ 1MB + const zlib_bomb_1 = zlib.inflateSync(zlib_bomb_2); + + // Would be 1 GB, if we let it + throws( + () => zlib.inflateSync(zlib_bomb_1), + new RangeError('Memory limit exceeded') + ); + }, +}; + +export const deflateSyncTest = { + test() { + function maskOsId(buf) { + // Clear the OS ID byte in gzip, which varies based on the platform used to run the tests + return buf.fill(0x0, 9, 10); + } + + throws( + () => zlib.deflateSync('hello world', { windowBits: 9000 }), + new Error('Invalid windowBits') + ); + throws( + () => zlib.deflateSync('hello world', { strategy: 400 }), + new Error('invalid strategy') + ); + throws( + () => + zlib.deflateSync(BIG_DATA, { maxOutputLength: 64 }).toString('base64'), + new RangeError('Memory limit exceeded') + ); + + strictEqual( + zlib + .deflateSync('bird bird bird', { windowBits: 11, level: 4 }) + .toString('base64'), + 'OE9LyixKUUiCEQAmfgUk' + ); + + strictEqual( + zlib + .deflateSync('what happens if you do not flush?', { + finishFlush: zlib.constants.Z_NO_FLUSH, + }) + .toString('base64'), + 'eJw=' + ); + + strictEqual( + zlib + .deflateSync(Buffer.from('bird bird bird'), { + windowBits: 11, + level: 4, + }) + .toString('base64'), + 'OE9LyixKUUiCEQAmfgUk' + ); + + deepStrictEqual( + maskOsId( + zlib.gzipSync('water, water, everywhere, nor any drop to drink') + ).toString('base64'), + 'H4sIAAAAAAAAACtPLEkt0lEoh1CpZalFleUZqUWpOgp5+UUKiXmVCilF+QUKJflAOjMvGwCqkvI8LwAAAA==' + ); + + strictEqual( + zlib + .deflateRawSync('as idle as a painted ship upon a painted ocean') + .toString('base64'), + 'SyxWyEzJSVVILFZIVChIzMwrSU1RKM7ILFAoLcjPQxLLT05NzAMA' + ); + + strictEqual(zlib.deflateSync('').toString('base64'), 'eJwDAAAAAAE='); + }, +}; +export const inflateTest = { + test() { + zlib.inflate( + Buffer.from('OE9LyixKUUiCEQAmfgUk', 'base64'), + { + windowBits: 11, + level: 4, + }, + (_, result) => { + strictEqual(result.toString(), 'bird bird bird'); + } + ); + }, +}; + +export const deflateTest = { + test() { + zlib.deflate( + 'bird bird bird', + { windowBits: 11, level: 4 }, + (_, result) => { + strictEqual(result.toString('base64'), 'OE9LyixKUUiCEQAmfgUk'); + } + ); + + zlib.deflate('garbage data', { level: -9000 }, (error, _) => { + strictEqual(error.message, 'Error: Invalid compression level'); + }); + }, +}; diff --git a/src/workerd/api/node/zlib-util.c++ b/src/workerd/api/node/zlib-util.c++ index 77632c6c9b5..62254d7fe6e 100644 --- a/src/workerd/api/node/zlib-util.c++ +++ b/src/workerd/api/node/zlib-util.c++ @@ -4,14 +4,110 @@ // Copyright Joyent and Node contributors. All rights reserved. MIT license. #include "zlib-util.h" +#include "workerd/jsg/exception.h" 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"); + return dataBuf.asPtr(); + } + + KJ_CASE_ONEOF(dataStr, jsg::NonCoercible) { + JSG_REQUIRE(dataStr.value.size() < Z_MAX_CHUNK, RangeError, "Memory limit exceeded"); + return dataStr.value.asBytes(); + } + } + + KJ_UNREACHABLE; +} -uint32_t ZlibUtil::crc32Sync(kj::Array data, uint32_t value) { - // Note: Bytef is defined in zlib.h - return crc32(value, reinterpret_cast(data.begin()), data.size()); +uint32_t ZlibUtil::crc32Sync(InputSource data, uint32_t value) { + auto dataPtr = getInputFromSource(data); + return crc32(value, dataPtr.begin(), dataPtr.size()); } +namespace { +class GrowableBuffer final { + // A copy of kj::Vector with some additional methods for use as a growable buffer with a maximum + // size +public: + inline explicit GrowableBuffer(size_t _chunkSize, size_t _maxCapacity) { + auto maxChunkSize = kj::min(_chunkSize, _maxCapacity); + builder = kj::heapArrayBuilder(maxChunkSize); + chunkSize = maxChunkSize; + maxCapacity = _maxCapacity; + } + + inline size_t size() const { + return builder.size(); + } + inline bool empty() const { + return size() == 0; + } + inline size_t capacity() const { + return builder.capacity(); + } + inline size_t available() const { + return capacity() - size(); + } + + inline kj::byte* begin() KJ_LIFETIMEBOUND { + return builder.begin(); + } + inline kj::byte* end() KJ_LIFETIMEBOUND { + return builder.end(); + } + + inline kj::Array releaseAsArray() { + // TODO(perf): Avoid a copy/move by allowing Array to point to incomplete space? + if (!builder.isFull()) { + setCapacity(size()); + } + return builder.finish(); + } + + inline void adjustUnused(size_t unused) { + resize(capacity() - unused); + } + + inline void resize(size_t size) { + if (size > builder.capacity()) grow(size); + builder.resize(size); + } + + inline void addChunk() { + reserve(size() + chunkSize); + } + + inline void reserve(size_t size) { + if (size > builder.capacity()) { + grow(size); + } + } + +private: + kj::ArrayBuilder builder; + size_t chunkSize; + size_t maxCapacity; + + void grow(size_t minCapacity = 0) { + JSG_REQUIRE(minCapacity <= maxCapacity, RangeError, "Memory limit exceeded"); + setCapacity(kj::min(maxCapacity, kj::max(minCapacity, capacity() == 0 ? 4 : capacity() * 2))); + } + void setCapacity(size_t newSize) { + if (builder.size() > newSize) { + builder.truncate(newSize); + } + + kj::ArrayBuilder newBuilder = kj::heapArrayBuilder(newSize); + newBuilder.addAll(kj::mv(builder)); + builder = kj::mv(newBuilder); + } +}; +} // namespace + void ZlibContext::initialize(int _level, int _windowBits, int _memLevel, @@ -71,10 +167,11 @@ kj::Maybe ZlibContext::getError() const { // normal statuses, not fatal break; case Z_NEED_DICT: - if (dictionary.empty()) + if (dictionary.empty()) { return constructError("Missing dictionary"_kj); - else + } else { return constructError("Bad dictionary"_kj); + } default: // something else. return constructError("Zlib error"); @@ -92,7 +189,6 @@ kj::Maybe ZlibContext::setDictionary() { case ZlibMode::DEFLATE: case ZlibMode::DEFLATERAW: err = deflateSetDictionary(&stream, dictionary.begin(), dictionary.size()); - ; break; case ZlibMode::INFLATERAW: err = inflateSetDictionary(&stream, dictionary.begin(), dictionary.size()); @@ -324,6 +420,16 @@ void ZlibContext::setBuffers(kj::ArrayPtr input, stream.next_out = output.begin(); } +void ZlibContext::setInputBuffer(kj::ArrayPtr input) { + stream.next_in = input.begin(); + stream.avail_in = input.size(); +} + +void ZlibContext::setOutputBuffer(kj::ArrayPtr output) { + stream.next_out = output.begin(); + stream.avail_out = output.size(); +} + jsg::Ref ZlibUtil::ZlibStream::constructor(ZlibModeValue mode) { return jsg::alloc(static_cast(mode)); } @@ -450,4 +556,59 @@ void ZlibUtil::ZlibStream::reset(jsg::Lock& js) { } } +kj::Array syncProcessBuffer(ZlibContext& ctx, GrowableBuffer& result) { + do { + result.addChunk(); + ctx.setOutputBuffer(kj::ArrayPtr(result.end(), result.available())); + + ctx.work(); + + KJ_IF_SOME(error, ctx.getError()) { + JSG_FAIL_REQUIRE(Error, error.message); + } + + result.adjustUnused(ctx.getAvailOut()); + } while (ctx.getAvailOut() == 0); + + return result.releaseAsArray(); +} + +// It's ZlibContext but it's RAII +class ZlibContextRAII: public ZlibContext { +public: + using ZlibContext::ZlibContext; + + ~ZlibContextRAII() { + close(); + } +}; + +kj::Array ZlibUtil::zlibSync(InputSource data, Options opts, ZlibModeValue mode) { + ZlibContextRAII ctx; + + auto chunkSize = opts.chunkSize.orDefault(ZLIB_PERFORMANT_CHUNK_SIZE); + auto maxOutputLength = opts.maxOutputLength.orDefault(Z_MAX_CHUNK); + + JSG_REQUIRE(Z_MIN_CHUNK <= chunkSize && chunkSize <= Z_MAX_CHUNK, Error, "Invalid chunkSize"); + JSG_REQUIRE(maxOutputLength <= Z_MAX_CHUNK, Error, "Invalid maxOutputLength"); + GrowableBuffer result(ZLIB_PERFORMANT_CHUNK_SIZE, maxOutputLength); + + ctx.setMode(static_cast(mode)); + ctx.initialize(opts.level.orDefault(Z_DEFAULT_LEVEL), + opts.windowBits.orDefault(Z_DEFAULT_WINDOWBITS), opts.memLevel.orDefault(Z_DEFAULT_MEMLEVEL), + opts.strategy.orDefault(Z_DEFAULT_STRATEGY), kj::mv(opts.dictionary)); + ctx.setFlush(opts.finishFlush.orDefault(Z_FINISH)); + ctx.setInputBuffer(getInputFromSource(data)); + return syncProcessBuffer(ctx, result); +} + +void ZlibUtil::zlibWithCallback( + jsg::Lock& js, InputSource data, Options options, ZlibModeValue mode, CompressCallback cb) { + try { + cb(js, kj::none, zlibSync(kj::mv(data), kj::mv(options), mode)); + } catch (kj::Exception& ex) { + auto tunneledError = jsg::tunneledErrorType(ex.getDescription()); + cb(js, tunneledError.message, kj::none); + } +} } // namespace workerd::api::node diff --git a/src/workerd/api/node/zlib-util.h b/src/workerd/api/node/zlib-util.h index ecf34a2aa37..bd143583bea 100644 --- a/src/workerd/api/node/zlib-util.h +++ b/src/workerd/api/node/zlib-util.h @@ -4,14 +4,16 @@ // Copyright Joyent and Node contributors. All rights reserved. MIT license. #pragma once -#include #include +#include +#include -#include "zlib.h" #include #include +#include #include +#include #include namespace workerd::api::node { @@ -69,6 +71,9 @@ enum class ZlibMode : ZlibModeValue { BROTLI_ENCODE }; +// When possible, we intentionally override chunkSize to a value that is likely to perform better +static constexpr auto ZLIB_PERFORMANT_CHUNK_SIZE = 40 * 1024; + struct CompressionError { CompressionError(kj::StringPtr _message, kj::StringPtr _code, int _err) : message(kj::str(_message)), @@ -82,7 +87,7 @@ struct CompressionError { int err; }; -class ZlibContext final { +class ZlibContext { public: ZlibContext() = default; @@ -93,6 +98,10 @@ class ZlibContext final { uint32_t inputLength, kj::ArrayPtr output, uint32_t outputLength); + + void setInputBuffer(kj::ArrayPtr input); + void setOutputBuffer(kj::ArrayPtr output); + int getFlush() const { return flush; }; @@ -234,11 +243,44 @@ class ZlibUtil final: public jsg::Object { } }; - uint32_t crc32Sync(kj::Array data, uint32_t value); + struct Options { + jsg::Optional flush; + jsg::Optional finishFlush; + 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, + finishFlush, + chunkSize, + windowBits, + level, + memLevel, + strategy, + dictionary, + maxOutputLength); + }; + + using InputSource = kj::OneOf, kj::Array>; + using CompressCallback = + jsg::Function, jsg::Optional>)>; + + static kj::ArrayPtr getInputFromSource(InputSource& data); + uint32_t crc32Sync(InputSource data, uint32_t value); + void zlibWithCallback( + jsg::Lock& js, InputSource data, Options options, ZlibModeValue mode, CompressCallback cb); + kj::Array zlibSync(InputSource data, Options options, ZlibModeValue mode); JSG_RESOURCE_TYPE(ZlibUtil) { JSG_METHOD_NAMED(crc32, crc32Sync); JSG_NESTED_TYPE(ZlibStream); + JSG_METHOD(zlibSync); + JSG_METHOD_NAMED(zlib, zlibWithCallback); // zlib.constants (part of the API contract for node:zlib) JSG_STATIC_CONSTANT_NAMED(CONST_Z_NO_FLUSH, Z_NO_FLUSH); @@ -280,6 +322,7 @@ class ZlibUtil final: public jsg::Object { CONST_BROTLI_DECODE, static_cast(ZlibMode::BROTLI_DECODE)); JSG_STATIC_CONSTANT_NAMED( CONST_BROTLI_ENCODE, static_cast(ZlibMode::BROTLI_ENCODE)); + JSG_STATIC_CONSTANT_NAMED(CONST_Z_MIN_WINDOWBITS, Z_MIN_WINDOWBITS); JSG_STATIC_CONSTANT_NAMED(CONST_Z_MAX_WINDOWBITS, Z_MAX_WINDOWBITS); JSG_STATIC_CONSTANT_NAMED(CONST_Z_DEFAULT_WINDOWBITS, Z_DEFAULT_WINDOWBITS); @@ -389,6 +432,6 @@ class ZlibUtil final: public jsg::Object { }; }; -#define EW_NODE_ZLIB_ISOLATE_TYPES api::node::ZlibUtil, api::node::ZlibUtil::ZlibStream - +#define EW_NODE_ZLIB_ISOLATE_TYPES \ + api::node::ZlibUtil, api::node::ZlibUtil::ZlibStream, api::node::ZlibUtil::Options } // namespace workerd::api::node