diff --git a/src/bun.js/api/zlib.classes.ts b/src/bun.js/api/zlib.classes.ts index a89af81e32513a..d907e3c838a665 100644 --- a/src/bun.js/api/zlib.classes.ts +++ b/src/bun.js/api/zlib.classes.ts @@ -1,40 +1,16 @@ import { define } from "../../codegen/class-definitions"; -export default [ - define({ - name: "NativeZlib", +function generate(name: string) { + return define({ + name, construct: true, noConstructor: false, - wantsThis: true, - finalize: true, - configurable: false, - // estimatedSize: true, - klass: {}, - JSType: "0b11101110", - values: ["callback"], - - proto: { - init: { fn: "init" }, - write: { fn: "write" }, - writeSync: { fn: "writeSync" }, - params: { fn: "params" }, - reset: { fn: "reset" }, - close: { fn: "close" }, - onerror: { setter: "setOnError", getter: "getOnError" }, - }, - }), - - define({ - name: "NativeBrotli", - construct: true, - noConstructor: false, - wantsThis: true, finalize: true, configurable: false, estimatedSize: true, klass: {}, JSType: "0b11101110", - values: ["callback"], + values: ["writeCallback", "errorCallback"], proto: { init: { fn: "init" }, @@ -43,7 +19,9 @@ export default [ params: { fn: "params" }, reset: { fn: "reset" }, close: { fn: "close" }, - onerror: { setter: "setOnError", getter: "getOnError" }, + onerror: { setter: "setOnError", this: true, getter: "getOnError" }, }, - }), -]; + }); +} + +export default [generate("NativeZlib"), generate("NativeBrotli")]; diff --git a/src/bun.js/node/node_zlib_binding.zig b/src/bun.js/node/node_zlib_binding.zig index 19f5e7d556ba0a..d892844e4a28fb 100644 --- a/src/bun.js/node/node_zlib_binding.zig +++ b/src/bun.js/node/node_zlib_binding.zig @@ -6,6 +6,7 @@ const string = bun.string; const Output = bun.Output; const ZigString = JSC.ZigString; const validators = @import("./util/validators.zig"); +const debug = bun.Output.scoped(.zlib, true); pub fn crc32(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.arguments_old(2).ptr; @@ -71,6 +72,8 @@ pub fn CompressionStream(comptime T: type) type { var in: ?[]const u8 = null; var out: ?[]u8 = null; + const this_value = callframe.this(); + bun.assert(!arguments[0].isUndefined()); // must provide flush value flush = arguments[0].toU32(); _ = std.meta.intToEnum(bun.zlib.FlushValue, flush) catch bun.assert(false); // Invalid flush value @@ -102,7 +105,9 @@ pub fn CompressionStream(comptime T: type) type { this.stream.setBuffers(in, out); this.stream.setFlush(@intCast(flush)); - // + // Only create the strong handle when we have a pending write + // And make sure to clear it when we are done. + this.this_value.set(globalThis, this_value); const vm = globalThis.bunVM(); this.task = .{ .callback = &AsyncJob.runTask }; @@ -136,13 +141,23 @@ pub fn CompressionStream(comptime T: type) type { this.write_in_progress = false; - if (!(this.checkError(globalThis) catch return globalThis.reportActiveExceptionAsUnhandled(error.JSError))) { + // Clear the strong handle before we call any callbacks. + const this_value = this.this_value.trySwap() orelse { + debug("this_value is null in runFromJSThread", .{}); + return; + }; + + this_value.ensureStillAlive(); + + if (!(this.checkError(globalThis, this_value) catch return globalThis.reportActiveExceptionAsUnhandled(error.JSError))) { return; } this.stream.updateWriteResult(&this.write_result.?[1], &this.write_result.?[0]); + this_value.ensureStillAlive(); - _ = this.write_callback.get().?.call(globalThis, this.this_value.get().?, &.{}) catch |err| globalThis.reportActiveExceptionAsUnhandled(err); + const write_callback: JSC.JSValue = T.writeCallbackGetCached(this_value).?; + _ = write_callback.call(globalThis, this_value, &.{}) catch |err| globalThis.reportActiveExceptionAsUnhandled(err); if (this.pending_close) _ = this._close(); } @@ -192,11 +207,10 @@ pub fn CompressionStream(comptime T: type) type { this.stream.setBuffers(in, out); this.stream.setFlush(@intCast(flush)); - - // + const this_value = callframe.this(); this.stream.doWork(); - if (try this.checkError(globalThis)) { + if (try this.checkError(globalThis, this_value)) { this.stream.updateWriteResult(&this.write_result.?[1], &this.write_result.?[0]); this.write_in_progress = false; } @@ -206,11 +220,9 @@ pub fn CompressionStream(comptime T: type) type { } pub fn reset(this: *T, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - _ = callframe; - const err = this.stream.reset(); if (err.isError()) { - try this.emitError(globalThis, err); + try this.emitError(globalThis, callframe.this(), err); } return .undefined; } @@ -233,34 +245,34 @@ pub fn CompressionStream(comptime T: type) type { this.stream.close(); } - pub fn setOnError(this: *T, globalThis: *JSC.JSGlobalObject, value: JSC.JSValue) bool { + pub fn setOnError(_: *T, this_value: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) bool { if (value.isFunction()) { - this.onerror_value.set(globalThis, value); + T.errorCallbackSetCached(this_value, globalObject, value); } return true; } - pub fn getOnError(this: *T, globalThis: *JSC.JSGlobalObject) JSC.JSValue { - _ = globalThis; - return this.onerror_value.get() orelse .undefined; + pub fn getOnError(_: *T, this_value: JSC.JSValue, _: *JSC.JSGlobalObject) JSC.JSValue { + return T.errorCallbackGetCached(this_value) orelse .undefined; } /// returns true if no error was detected/emitted - fn checkError(this: *T, globalThis: *JSC.JSGlobalObject) !bool { + fn checkError(this: *T, globalThis: *JSC.JSGlobalObject, this_value: JSC.JSValue) !bool { const err = this.stream.getErrorInfo(); if (!err.isError()) return true; - try this.emitError(globalThis, err); + try this.emitError(globalThis, this_value, err); return false; } - fn emitError(this: *T, globalThis: *JSC.JSGlobalObject, err_: Error) !void { + fn emitError(this: *T, globalThis: *JSC.JSGlobalObject, this_value: JSC.JSValue, err_: Error) !void { var msg_str = bun.String.createFormat("{s}", .{std.mem.sliceTo(err_.msg, 0) orelse ""}) catch bun.outOfMemory(); const msg_value = msg_str.transferToJS(globalThis); const err_value = JSC.jsNumber(err_.err); var code_str = bun.String.createFormat("{s}", .{std.mem.sliceTo(err_.code, 0) orelse ""}) catch bun.outOfMemory(); const code_value = code_str.transferToJS(globalThis); - _ = try this.onerror_value.get().?.call(globalThis, this.this_value.get().?, &.{ msg_value, err_value, code_value }); + const callback: JSC.JSValue = T.errorCallbackGetCached(this_value) orelse Output.panic("Assertion failure: cachedErrorCallback is null in node:zlib binding", .{}); + _ = try callback.call(globalThis, this_value, &.{ msg_value, err_value, code_value }); this.write_in_progress = false; if (this.pending_close) _ = this._close(); @@ -307,8 +319,6 @@ pub const SNativeZlib = struct { globalThis: *JSC.JSGlobalObject, stream: ZlibContext = .{}, write_result: ?[*]u32 = null, - write_callback: JSC.Strong = .{}, - onerror_value: JSC.Strong = .{}, poll_ref: CountedKeepAlive = .{}, this_value: JSC.Strong = .{}, write_in_progress: bool = false, @@ -341,11 +351,10 @@ pub const SNativeZlib = struct { } //// adding this didnt help much but leaving it here to compare the number with later - // pub fn estimatedSize(this: *const SNativeZlib) usize { - // _ = this; - // const internal_state_size = 3309; // @sizeOf(@cImport(@cInclude("deflate.h")).internal_state) @ cloudflare/zlib @ 92530568d2c128b4432467b76a3b54d93d6350bd - // return @sizeOf(SNativeZlib) + internal_state_size; - // } + pub fn estimatedSize(_: *const SNativeZlib) usize { + const internal_state_size = 3309; // @sizeOf(@cImport(@cInclude("deflate.h")).internal_state) @ cloudflare/zlib @ 92530568d2c128b4432467b76a3b54d93d6350bd + return @sizeOf(SNativeZlib) + internal_state_size; + } pub fn init(this: *SNativeZlib, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.argumentsUndef(7).slice(); @@ -364,7 +373,7 @@ pub const SNativeZlib = struct { const dictionary = if (arguments[6].isUndefined()) null else arguments[6].asArrayBuffer(globalThis).?.byteSlice(); this.write_result = writeResult; - this.write_callback.set(globalThis, writeCallback); + SNativeZlib.writeCallbackSetCached(callframe.this(), globalThis, writeCallback); this.stream.init(level, windowBits, memLevel, strategy, dictionary); @@ -383,15 +392,15 @@ pub const SNativeZlib = struct { const err = this.stream.setParams(level, strategy); if (err.isError()) { - try this.emitError(globalThis, err); + try this.emitError(globalThis, callframe.this(), err); } return .undefined; } pub fn deinit(this: *@This()) void { - this.write_callback.deinit(); - this.onerror_value.deinit(); + this.this_value.deinit(); this.poll_ref.deinit(); + this.stream.close(); this.destroy(); } }; @@ -662,7 +671,7 @@ pub const NativeBrotli = JSC.Codegen.JSNativeBrotli.getConstructor; pub const SNativeBrotli = struct { pub usingnamespace bun.NewRefCounted(@This(), deinit); - pub usingnamespace JSC.Codegen.JSNativeZlib; + pub usingnamespace JSC.Codegen.JSNativeBrotli; pub usingnamespace CompressionStream(@This()); ref_count: u32 = 1, @@ -670,8 +679,6 @@ pub const SNativeBrotli = struct { globalThis: *JSC.JSGlobalObject, stream: BrotliContext = .{}, write_result: ?[*]u32 = null, - write_callback: JSC.Strong = .{}, - onerror_value: JSC.Strong = .{}, poll_ref: CountedKeepAlive = .{}, this_value: JSC.Strong = .{}, write_in_progress: bool = false, @@ -718,6 +725,7 @@ pub const SNativeBrotli = struct { pub fn init(this: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.argumentsUndef(3).slice(); + const this_value = callframe.this(); if (arguments.len != 3) { return globalThis.ERR_MISSING_ARGS("init(params, writeResult, writeCallback)", .{}).throw(); } @@ -725,12 +733,14 @@ pub const SNativeBrotli = struct { // this does not get gc'd because it is stored in the JS object's `this._writeState`. and the JS object is tied to the native handle as `_handle[owner_symbol]`. const writeResult = arguments[1].asArrayBuffer(globalThis).?.asU32().ptr; const writeCallback = try validators.validateFunction(globalThis, arguments[2], "writeCallback", .{}); + this.write_result = writeResult; - this.write_callback.set(globalThis, writeCallback); + + SNativeBrotli.writeCallbackSetCached(this_value, globalThis, writeCallback); var err = this.stream.init(); if (err.isError()) { - try this.emitError(globalThis, err); + try this.emitError(globalThis, this_value, err); return JSC.jsBoolean(false); } @@ -759,9 +769,12 @@ pub const SNativeBrotli = struct { } pub fn deinit(this: *@This()) void { - this.write_callback.deinit(); - this.onerror_value.deinit(); + this.this_value.deinit(); this.poll_ref.deinit(); + switch (this.stream.mode) { + .BROTLI_ENCODE, .BROTLI_DECODE => this.stream.close(), + else => {}, + } this.destroy(); } }; diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index ec2f9e8a4354ac..daf15ed5b5e7a1 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -58,7 +58,11 @@ export interface ClassDefinition { values?: string[]; JSType?: string; noConstructor?: boolean; - wantsThis?: boolean; + + // Do not try to track the `this` value in the constructor automatically. + // That is a memory leak. + wantsThis?: never; + /** * Called from any thread. * diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index 6f30301b080c88..b333caf88bcd26 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -361,12 +361,6 @@ JSC_DECLARE_CUSTOM_GETTER(js${typeName}Constructor); `; } - if (obj.wantsThis) { - externs += ` -extern JSC_CALLCONV void* JSC_HOST_CALL_ATTRIBUTES ${classSymbolName(typeName, "_setThis")}(JSC::JSGlobalObject*, void*, JSC::EncodedJSValue); -`; - } - if (obj.structuredClone) { externs += `extern JSC_CALLCONV void JSC_HOST_CALL_ATTRIBUTES ${symbolName( @@ -652,13 +646,6 @@ JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ${name}::construct(JSC::JSGlobalObj } auto value = JSValue::encode(instance); -${ - obj.wantsThis - ? ` - ${classSymbolName(typeName, "_setThis")}(globalObject, ptr, value); -` - : "" -} RELEASE_AND_RETURN(scope, value); } @@ -1661,7 +1648,6 @@ function generateZig( construct, finalize, noConstructor = false, - wantsThis = false, overridesToJS = false, estimatedSize, call = false, @@ -1794,16 +1780,6 @@ const JavaScriptCoreBindings = struct { `; } - if (construct && !noConstructor && wantsThis) { - exports.set("_setThis", classSymbolName(typeName, "_setThis")); - output += ` - pub fn ${classSymbolName(typeName, "_setThis")}(globalObject: *JSC.JSGlobalObject, ptr: *anyopaque, this: JSC.JSValue) callconv(JSC.conv) void { - const real: *${typeName} = @ptrCast(@alignCast(ptr)); - real.this_value.set(globalObject, this); - } - `; - } - if (call) { exports.set("call", classSymbolName(typeName, "call")); output += ` diff --git a/test/js/node/test/common/gc.js b/test/js/node/test/common/gc.js index 8e2c5ee5da4630..3774f81cc8f18a 100644 --- a/test/js/node/test/common/gc.js +++ b/test/js/node/test/common/gc.js @@ -120,8 +120,22 @@ async function checkIfCollectableByCounting(fn, ctor, count, waitTime = 20) { throw new Error(`${name} cannot be collected`); } +var finalizationRegistry = new FinalizationRegistry(heldValue => { + heldValue.ongc(); +}) + +function onGC(value, holder) { + if (holder?.ongc) { + + finalizationRegistry.register(value, { ongc: holder.ongc }); + } +} + module.exports = { checkIfCollectable, runAndBreathe, checkIfCollectableByCounting, + onGC, }; + + diff --git a/test/js/node/test/common/index.js b/test/js/node/test/common/index.js index c5822beb349ffb..6b5d1079ffad7b 100644 --- a/test/js/node/test/common/index.js +++ b/test/js/node/test/common/index.js @@ -120,8 +120,8 @@ if (process.argv.length === 2 && // invalid. The test itself should handle this case. (process.features.inspector || !flag.startsWith('--inspect'))) { if (flag === "--expose-gc" && process.versions.bun) { - globalThis.gc ??= Bun.gc; - continue; + globalThis.gc ??= () => Bun.gc(true); + break; } console.log( 'NOTE: The test started as a child_process using these flags:', @@ -134,7 +134,9 @@ if (process.argv.length === 2 && if (result.signal) { process.kill(0, result.signal); } else { - process.exit(result.status); + // Ensure we don't call the "exit" callbacks, as that will cause the + // test to fail when it may have passed in the child process. + process.kill(process.pid, result.status); } } } @@ -1216,3 +1218,5 @@ module.exports = new Proxy(common, { return obj[prop]; }, }); + + diff --git a/test/js/node/test/parallel/test-internal-module-require.js b/test/js/node/test/parallel/test-internal-module-require.js index c6e2057d3da1ee..3a02e25cd01a15 100644 --- a/test/js/node/test/parallel/test-internal-module-require.js +++ b/test/js/node/test/parallel/test-internal-module-require.js @@ -1,112 +1,114 @@ 'use strict'; -// Flags: --expose-internals -// This verifies that -// 1. We do not leak internal modules unless the --require-internals option -// is on. -// 2. We do not accidentally leak any modules to the public global scope. -// 3. Deprecated modules are properly deprecated. +// // Flags: --expose-internals +// // This verifies that +// // 1. We do not leak internal modules unless the --require-internals option +// // is on. +// // 2. We do not accidentally leak any modules to the public global scope. +// // 3. Deprecated modules are properly deprecated. const common = require('../common'); -if (!common.isMainThread) { - common.skip('Cannot test the existence of --expose-internals from worker'); -} +common.skip("This test is not going to be implemented in Bun. We do not support --expose-internals.") -const assert = require('assert'); -const fork = require('child_process').fork; +// if (!common.isMainThread) { +// common.skip('Cannot test the existence of --expose-internals from worker'); +// } -const expectedPublicModules = new Set([ - '_http_agent', - '_http_client', - '_http_common', - '_http_incoming', - '_http_outgoing', - '_http_server', - '_stream_duplex', - '_stream_passthrough', - '_stream_readable', - '_stream_transform', - '_stream_wrap', - '_stream_writable', - '_tls_common', - '_tls_wrap', - 'assert', - 'async_hooks', - 'buffer', - 'child_process', - 'cluster', - 'console', - 'constants', - 'crypto', - 'dgram', - 'dns', - 'domain', - 'events', - 'fs', - 'http', - 'http2', - 'https', - 'inspector', - 'module', - 'net', - 'os', - 'path', - 'perf_hooks', - 'process', - 'punycode', - 'querystring', - 'readline', - 'repl', - 'stream', - 'string_decoder', - 'sys', - 'timers', - 'tls', - 'trace_events', - 'tty', - 'url', - 'util', - 'v8', - 'vm', - 'worker_threads', - 'zlib', -]); +// const assert = require('assert'); +// const fork = require('child_process').fork; -if (process.argv[2] === 'child') { - assert(!process.execArgv.includes('--expose-internals')); - process.once('message', ({ allBuiltins }) => { - const publicModules = new Set(); - for (const id of allBuiltins) { - if (id.startsWith('internal/')) { - assert.throws(() => { - require(id); - }, { - code: 'MODULE_NOT_FOUND', - message: `Cannot find module '${id}'` - }); - } else { - require(id); - publicModules.add(id); - } - } - assert(allBuiltins.length > publicModules.size); - // Make sure all the public modules are available through - // require('module').builtinModules - assert.deepStrictEqual( - publicModules, - new Set(require('module').builtinModules) - ); - assert.deepStrictEqual(publicModules, expectedPublicModules); - }); -} else { - assert(process.execArgv.includes('--expose-internals')); - const child = fork(__filename, ['child'], { - execArgv: [] - }); - const { builtinModules } = require('module'); - // When --expose-internals is on, require('module').builtinModules - // contains internal modules. - const message = { allBuiltins: builtinModules }; - child.send(message); -} +// const expectedPublicModules = new Set([ +// '_http_agent', +// '_http_client', +// '_http_common', +// '_http_incoming', +// '_http_outgoing', +// '_http_server', +// '_stream_duplex', +// '_stream_passthrough', +// '_stream_readable', +// '_stream_transform', +// '_stream_wrap', +// '_stream_writable', +// '_tls_common', +// '_tls_wrap', +// 'assert', +// 'async_hooks', +// 'buffer', +// 'child_process', +// 'cluster', +// 'console', +// 'constants', +// 'crypto', +// 'dgram', +// 'dns', +// 'domain', +// 'events', +// 'fs', +// 'http', +// 'http2', +// 'https', +// 'inspector', +// 'module', +// 'net', +// 'os', +// 'path', +// 'perf_hooks', +// 'process', +// 'punycode', +// 'querystring', +// 'readline', +// 'repl', +// 'stream', +// 'string_decoder', +// 'sys', +// 'timers', +// 'tls', +// 'trace_events', +// 'tty', +// 'url', +// 'util', +// 'v8', +// 'vm', +// 'worker_threads', +// 'zlib', +// ]); + +// if (process.argv[2] === 'child') { +// assert(!process.execArgv.includes('--expose-internals')); +// process.once('message', ({ allBuiltins }) => { +// const publicModules = new Set(); +// for (const id of allBuiltins) { +// if (id.startsWith('internal/')) { +// assert.throws(() => { +// require(id); +// }, { +// code: 'MODULE_NOT_FOUND', +// message: `Cannot find module '${id}'` +// }); +// } else { +// require(id); +// publicModules.add(id); +// } +// } +// assert(allBuiltins.length > publicModules.size); +// // Make sure all the public modules are available through +// // require('module').builtinModules +// assert.deepStrictEqual( +// publicModules, +// new Set(require('module').builtinModules) +// ); +// assert.deepStrictEqual(publicModules, expectedPublicModules); +// }); +// } else { +// assert(process.execArgv.includes('--expose-internals')); +// const child = fork(__filename, ['child'], { +// execArgv: [] +// }); +// const { builtinModules } = require('module'); +// // When --expose-internals is on, require('module').builtinModules +// // contains internal modules. +// const message = { allBuiltins: builtinModules }; +// child.send(message); +// } diff --git a/test/js/node/test/parallel/test-zlib-deflate-constructors.js b/test/js/node/test/parallel/test-zlib-deflate-constructors.js new file mode 100644 index 00000000000000..6a5d4100862e9e --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-deflate-constructors.js @@ -0,0 +1,309 @@ +'use strict'; + +require('../common'); + +const zlib = require('zlib'); +const assert = require('assert'); + +// Work with and without `new` keyword +assert.ok(zlib.Deflate() instanceof zlib.Deflate); +assert.ok(new zlib.Deflate() instanceof zlib.Deflate); + +assert.ok(zlib.DeflateRaw() instanceof zlib.DeflateRaw); +assert.ok(new zlib.DeflateRaw() instanceof zlib.DeflateRaw); + +// Throws if `options.chunkSize` is invalid +assert.throws( + () => new zlib.Deflate({ chunkSize: 'test' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.chunkSize" property must be of type number. ' + + 'Received "test"' + } +); + +assert.throws( + () => new zlib.Deflate({ chunkSize: -Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.chunkSize" is out of range. It must ' + + 'be a finite number. Received -Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ chunkSize: 0 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.chunkSize" is out of range. It must ' + + 'be >= 64. Received: 0' + } +); + +// Confirm that maximum chunk size cannot be exceeded because it is `Infinity`. +assert.strictEqual(zlib.constants.Z_MAX_CHUNK, Infinity); + +// Throws if `options.windowBits` is invalid +assert.throws( + () => new zlib.Deflate({ windowBits: 'test' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.windowBits" property must be of type number. ' + + 'Received "test"' + } +); + +assert.throws( + () => new zlib.Deflate({ windowBits: -Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.windowBits" is out of range. It must ' + + 'be a finite number. Received -Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ windowBits: Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.windowBits" is out of range. It must ' + + 'be a finite number. Received Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ windowBits: 0 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.windowBits" is out of range. It must ' + + 'be >= 8 and <= 15. Received 0' + } +); + +// Throws if `options.level` is invalid +assert.throws( + () => new zlib.Deflate({ level: 'test' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.level" property must be of type number. ' + + 'Received "test"' + } +); + +assert.throws( + () => new zlib.Deflate({ level: -Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.level" is out of range. It must ' + + 'be a finite number. Received -Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ level: Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.level" is out of range. It must ' + + 'be a finite number. Received Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ level: -2 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.level" is out of range. It must ' + + 'be >= -1 and <= 9. Received -2' + } +); + +// Throws if `level` invalid in `Deflate.prototype.params()` +assert.throws( + () => new zlib.Deflate().params('test'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "level" argument must be of type number. ' + + 'Received "test"' + } +); + +assert.throws( + () => new zlib.Deflate().params(-Infinity), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "level" is out of range. It must ' + + 'be a finite number. Received -Infinity' + } +); + +assert.throws( + () => new zlib.Deflate().params(Infinity), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "level" is out of range. It must ' + + 'be a finite number. Received Infinity' + } +); + +assert.throws( + () => new zlib.Deflate().params(-2), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "level" is out of range. It must ' + + 'be >= -1 and <= 9. Received -2' + } +); + +// Throws if options.memLevel is invalid +assert.throws( + () => new zlib.Deflate({ memLevel: 'test' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.memLevel" property must be of type number. ' + + 'Received "test"' + } +); + +assert.throws( + () => new zlib.Deflate({ memLevel: -Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.memLevel" is out of range. It must ' + + 'be a finite number. Received -Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ memLevel: Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.memLevel" is out of range. It must ' + + 'be a finite number. Received Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ memLevel: -2 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.memLevel" is out of range. It must ' + + 'be >= 1 and <= 9. Received -2' + } +); + +// Does not throw if opts.strategy is valid +new zlib.Deflate({ strategy: zlib.constants.Z_FILTERED }); +new zlib.Deflate({ strategy: zlib.constants.Z_HUFFMAN_ONLY }); +new zlib.Deflate({ strategy: zlib.constants.Z_RLE }); +new zlib.Deflate({ strategy: zlib.constants.Z_FIXED }); +new zlib.Deflate({ strategy: zlib.constants.Z_DEFAULT_STRATEGY }); + +// Throws if options.strategy is invalid +assert.throws( + () => new zlib.Deflate({ strategy: 'test' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.strategy" property must be of type number. ' + + 'Received "test"' + } +); + +assert.throws( + () => new zlib.Deflate({ strategy: -Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.strategy" is out of range. It must ' + + 'be a finite number. Received -Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ strategy: Infinity }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.strategy" is out of range. It must ' + + 'be a finite number. Received Infinity' + } +); + +assert.throws( + () => new zlib.Deflate({ strategy: -2 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.strategy" is out of range. It must ' + + 'be >= 0 and <= 4. Received -2' + } +); + +// Throws TypeError if `strategy` is invalid in `Deflate.prototype.params()` +assert.throws( + () => new zlib.Deflate().params(0, 'test'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "strategy" argument must be of type number. ' + + 'Received "test"' + } +); + +assert.throws( + () => new zlib.Deflate().params(0, -Infinity), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "strategy" is out of range. It must ' + + 'be a finite number. Received -Infinity' + } +); + +assert.throws( + () => new zlib.Deflate().params(0, Infinity), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "strategy" is out of range. It must ' + + 'be a finite number. Received Infinity' + } +); + +assert.throws( + () => new zlib.Deflate().params(0, -2), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "strategy" is out of range. It must ' + + 'be >= 0 and <= 4. Received -2' + } +); + +// Throws if opts.dictionary is not a Buffer +assert.throws( + () => new zlib.Deflate({ dictionary: 'not a buffer' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } +); \ No newline at end of file diff --git a/test/js/node/test/parallel/test-zlib-dictionary.js b/test/js/node/test/parallel/test-zlib-dictionary.js new file mode 100644 index 00000000000000..47eaaa62d0355f --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-dictionary.js @@ -0,0 +1,175 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// Test compression/decompression with dictionary + +const common = require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +const spdyDict = Buffer.from([ + 'optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-', + 'languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi', + 'f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser', + '-agent10010120020120220320420520630030130230330430530630740040140240340440', + '5406407408409410411412413414415416417500501502503504505accept-rangesageeta', + 'glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic', + 'ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran', + 'sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati', + 'oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo', + 'ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe', + 'pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic', + 'ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1', + '.1statusversionurl\0', +].join('')); + +const input = [ + 'HTTP/1.1 200 Ok', + 'Server: node.js', + 'Content-Length: 0', + '', +].join('\r\n'); + +function basicDictionaryTest(spdyDict) { + let output = ''; + const deflate = zlib.createDeflate({ dictionary: spdyDict }); + const inflate = zlib.createInflate({ dictionary: spdyDict }); + inflate.setEncoding('utf-8'); + + deflate.on('data', function(chunk) { + inflate.write(chunk); + }); + + inflate.on('data', function(chunk) { + output += chunk; + }); + + deflate.on('end', function() { + inflate.end(); + }); + + inflate.on('end', common.mustCall(function() { + assert.strictEqual(input, output); + })); + + deflate.write(input); + deflate.end(); +} + +function deflateResetDictionaryTest(spdyDict) { + let doneReset = false; + let output = ''; + const deflate = zlib.createDeflate({ dictionary: spdyDict }); + const inflate = zlib.createInflate({ dictionary: spdyDict }); + inflate.setEncoding('utf-8'); + + deflate.on('data', function(chunk) { + if (doneReset) + inflate.write(chunk); + }); + + inflate.on('data', function(chunk) { + output += chunk; + }); + + deflate.on('end', function() { + inflate.end(); + }); + + inflate.on('end', common.mustCall(function() { + assert.strictEqual(input, output); + })); + + deflate.write(input); + deflate.flush(function() { + deflate.reset(); + doneReset = true; + deflate.write(input); + deflate.end(); + }); +} + +function rawDictionaryTest(spdyDict) { + let output = ''; + const deflate = zlib.createDeflateRaw({ dictionary: spdyDict }); + const inflate = zlib.createInflateRaw({ dictionary: spdyDict }); + inflate.setEncoding('utf-8'); + + deflate.on('data', function(chunk) { + inflate.write(chunk); + }); + + inflate.on('data', function(chunk) { + output += chunk; + }); + + deflate.on('end', function() { + inflate.end(); + }); + + inflate.on('end', common.mustCall(function() { + assert.strictEqual(input, output); + })); + + deflate.write(input); + deflate.end(); +} + +function deflateRawResetDictionaryTest(spdyDict) { + let doneReset = false; + let output = ''; + const deflate = zlib.createDeflateRaw({ dictionary: spdyDict }); + const inflate = zlib.createInflateRaw({ dictionary: spdyDict }); + inflate.setEncoding('utf-8'); + + deflate.on('data', function(chunk) { + if (doneReset) + inflate.write(chunk); + }); + + inflate.on('data', function(chunk) { + output += chunk; + }); + + deflate.on('end', function() { + inflate.end(); + }); + + inflate.on('end', common.mustCall(function() { + assert.strictEqual(input, output); + })); + + deflate.write(input); + deflate.flush(function() { + deflate.reset(); + doneReset = true; + deflate.write(input); + deflate.end(); + }); +} + +for (const dict of [spdyDict, ...common.getBufferSources(spdyDict)]) { + basicDictionaryTest(dict); + deflateResetDictionaryTest(dict); + rawDictionaryTest(dict); + deflateRawResetDictionaryTest(dict); +} \ No newline at end of file diff --git a/test/js/node/test/parallel/test-zlib-failed-init.js b/test/js/node/test/parallel/test-zlib-failed-init.js new file mode 100644 index 00000000000000..d47b61de662d77 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-failed-init.js @@ -0,0 +1,46 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const zlib = require('zlib'); + +assert.throws( + () => zlib.createGzip({ chunkSize: 0 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.chunkSize" is out of range. It must ' + + 'be >= 64. Received: 0' + } +); + +assert.throws( + () => zlib.createGzip({ windowBits: 0 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.windowBits" is out of range. It must ' + + 'be >= 9 and <= 15. Received 0' + } +); + +assert.throws( + () => zlib.createGzip({ memLevel: 0 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.memLevel" is out of range. It must ' + + 'be >= 1 and <= 9. Received 0' + } +); + +{ + const stream = zlib.createGzip({ level: NaN }); + assert.strictEqual(stream._level, zlib.constants.Z_DEFAULT_COMPRESSION); +} + +{ + const stream = zlib.createGzip({ strategy: NaN }); + assert.strictEqual(stream._strategy, zlib.constants.Z_DEFAULT_STRATEGY); +} \ No newline at end of file diff --git a/test/js/node/test/parallel/test-zlib-flush-flags.js b/test/js/node/test/parallel/test-zlib-flush-flags.js new file mode 100644 index 00000000000000..1cb552390c1864 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-flush-flags.js @@ -0,0 +1,48 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const zlib = require('zlib'); + +zlib.createGzip({ flush: zlib.constants.Z_SYNC_FLUSH }); + +assert.throws( + () => zlib.createGzip({ flush: 'foobar' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.flush" property must be of type number. ' + + 'Received "foobar"' + } +); + +assert.throws( + () => zlib.createGzip({ flush: 10000 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.flush" is out of range. It must ' + + 'be >= 0 and <= 5. Received 10000' + } +); + +zlib.createGzip({ finishFlush: zlib.constants.Z_SYNC_FLUSH }); + +assert.throws( + () => zlib.createGzip({ finishFlush: 'foobar' }), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "options.finishFlush" property must be of type number. ' + + 'Received "foobar"' + } +); + +assert.throws( + () => zlib.createGzip({ finishFlush: 10000 }), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "options.finishFlush" is out of range. It must ' + + 'be >= 0 and <= 5. Received 10000' + } +); \ No newline at end of file diff --git a/test/js/node/test/parallel/test-zlib-invalid-input-memory.js b/test/js/node/test/parallel/test-zlib-invalid-input-memory.js new file mode 100644 index 00000000000000..c4dbe4c08170e8 --- /dev/null +++ b/test/js/node/test/parallel/test-zlib-invalid-input-memory.js @@ -0,0 +1,28 @@ +// Flags: --expose-gc +'use strict'; +const common = require('../common'); +const { onGC } = require('../common/gc'); +const assert = require('assert'); +const zlib = require('zlib'); + +// Checks that, if a zlib context fails with an error, it can still be GC'ed: +// Refs: https://github.com/nodejs/node/issues/22705 + +const ongc = common.mustCall(); + +{ + const input = Buffer.from('foobar'); + const strm = zlib.createInflate(); + strm.end(input); + strm.once('error', common.mustCall((err) => { + assert(err); + setImmediate(() => { + global.gc(); + // Keep the event loop alive for seeing the async_hooks destroy hook + // we use for GC tracking... + // TODO(addaleax): This should maybe not be necessary? + setImmediate(() => {}); + }); + })); + onGC(strm, { ongc }); +} \ No newline at end of file