Files
bun.sh/src/bun.js/node/node_zlib_binding.zig
2024-10-29 12:56:10 -07:00

920 lines
34 KiB
Zig

const std = @import("std");
const bun = @import("root").bun;
const Environment = bun.Environment;
const JSC = bun.JSC;
const string = bun.string;
const Output = bun.Output;
const ZigString = JSC.ZigString;
const validators = @import("./util/validators.zig");
pub fn crc32(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue {
const arguments = callframe.arguments(2).ptr;
const data: ZigString.Slice = blk: {
const data: JSC.JSValue = arguments[0];
var exceptionref: JSC.C.JSValueRef = null;
if (data == .zero) {
return globalThis.throwInvalidArgumentTypeValue("data", "string or an instance of Buffer, TypedArray, or DataView", .undefined);
}
if (data.isString()) {
break :blk data.asString().toSlice(globalThis, bun.default_allocator);
}
const buffer: JSC.Buffer = JSC.Buffer.fromJS(globalThis, data, &exceptionref) orelse {
const ty_str = data.jsTypeString(globalThis).toSlice(globalThis, bun.default_allocator);
defer ty_str.deinit();
globalThis.ERR_INVALID_ARG_TYPE("The \"data\" property must be an instance of Buffer, TypedArray, DataView, or ArrayBuffer. Received {s}", .{ty_str.slice()}).throw();
return .zero;
};
if (exceptionref) |ptr| {
globalThis.throwValue(JSC.JSValue.c(ptr));
return .zero;
}
break :blk ZigString.Slice.fromUTF8NeverFree(buffer.slice());
};
defer data.deinit();
const value: u32 = blk: {
const value: JSC.JSValue = arguments[1];
if (value == .zero) {
break :blk 0;
}
if (!value.isNumber()) {
return globalThis.throwInvalidArgumentTypeValue("value", "number", value);
}
const valuef = value.asNumber();
const min = 0;
const max = std.math.maxInt(u32);
if (@floor(valuef) != valuef) {
globalThis.ERR_OUT_OF_RANGE("The value of \"{s}\" is out of range. It must be an integer. Received {}", .{ "value", valuef }).throw();
return .zero;
}
if (valuef < min or valuef > max) {
globalThis.ERR_OUT_OF_RANGE("The value of \"{s}\" is out of range. It must be >= {d} and <= {d}. Received {d}", .{ "value", min, max, valuef }).throw();
return .zero;
}
break :blk @intFromFloat(valuef);
};
// crc32 returns a u64 but the data will always be within a u32 range so the outer @intCast is always safe.
const slice_u8 = data.slice();
return JSC.JSValue.jsNumber(@as(u32, @intCast(bun.zlib.crc32(value, slice_u8.ptr, @intCast(slice_u8.len)))));
}
pub fn CompressionStream(comptime T: type) type {
return struct {
pub fn write(this: *T, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue {
const arguments = callframe.argumentsUndef(7).slice();
if (arguments.len != 7) {
globalThis.ERR_MISSING_ARGS("write(flush, in, in_off, in_len, out, out_off, out_len)", .{}).throw();
return .zero;
}
var in_off: u32 = 0;
var in_len: u32 = 0;
var out_off: u32 = 0;
var out_len: u32 = 0;
var flush: u32 = 0;
var in: ?[]const u8 = null;
var out: ?[]u8 = null;
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
if (arguments[1].isNull()) {
// just a flush
in = null;
in_len = 0;
in_off = 0;
} else {
const in_buf = arguments[1].asArrayBuffer(globalThis).?;
in_off = arguments[2].toU32();
in_len = arguments[3].toU32();
bun.assert(in_buf.byte_len >= in_off + in_len);
in = in_buf.byteSlice()[in_off..][0..in_len];
}
const out_buf = arguments[4].asArrayBuffer(globalThis).?;
out_off = arguments[5].toU32();
out_len = arguments[6].toU32();
bun.assert(out_buf.byte_len >= out_off + out_len);
out = out_buf.byteSlice()[out_off..][0..out_len];
bun.assert(!this.write_in_progress);
bun.assert(!this.pending_close);
this.write_in_progress = true;
this.ref();
this.stream.setBuffers(in, out);
this.stream.setFlush(@intCast(flush));
//
const vm = globalThis.bunVM();
this.task = .{ .callback = &AsyncJob.runTask };
this.poll_ref.ref(vm);
JSC.WorkPool.schedule(&this.task);
return .undefined;
}
const AsyncJob = struct {
pub fn runTask(task: *JSC.WorkPoolTask) void {
const this: *T = @fieldParentPtr("task", task);
AsyncJob.run(this);
}
pub fn run(this: *T) void {
const globalThis: *JSC.JSGlobalObject = this.globalThis;
const vm = globalThis.bunVMConcurrently();
this.stream.doWork();
vm.enqueueTaskConcurrent(JSC.ConcurrentTask.create(JSC.Task.init(this)));
}
};
pub fn runFromJSThread(this: *T) void {
const globalThis: *JSC.JSGlobalObject = this.globalThis;
const vm = globalThis.bunVM();
this.poll_ref.unref(vm);
defer this.deref();
this.write_in_progress = false;
if (!(this.checkError(globalThis) catch return globalThis.reportActiveExceptionAsUnhandled(error.JSError))) {
return;
}
this.stream.updateWriteResult(&this.write_result.?[1], &this.write_result.?[0]);
_ = this.write_callback.get().?.call(globalThis, this.this_value.get().?, &.{}) catch |err| globalThis.reportActiveExceptionAsUnhandled(err);
if (this.pending_close) _ = this._close();
}
pub fn writeSync(this: *T, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue {
const arguments = callframe.argumentsUndef(7).slice();
if (arguments.len != 7) {
globalThis.ERR_MISSING_ARGS("writeSync(flush, in, in_off, in_len, out, out_off, out_len)", .{}).throw();
return .zero;
}
var in_off: u32 = 0;
var in_len: u32 = 0;
var out_off: u32 = 0;
var out_len: u32 = 0;
var flush: u32 = 0;
var in: ?[]const u8 = null;
var out: ?[]u8 = null;
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
if (arguments[1].isNull()) {
// just a flush
in = null;
in_len = 0;
in_off = 0;
} else {
const in_buf = arguments[1].asArrayBuffer(globalThis).?;
in_off = arguments[2].toU32();
in_len = arguments[3].toU32();
bun.assert(in_buf.byte_len >= in_off + in_len);
in = in_buf.byteSlice()[in_off..][0..in_len];
}
const out_buf = arguments[4].asArrayBuffer(globalThis).?;
out_off = arguments[5].toU32();
out_len = arguments[6].toU32();
bun.assert(out_buf.byte_len >= out_off + out_len);
out = out_buf.byteSlice()[out_off..][0..out_len];
bun.assert(!this.write_in_progress);
bun.assert(!this.pending_close);
this.write_in_progress = true;
this.ref();
this.stream.setBuffers(in, out);
this.stream.setFlush(@intCast(flush));
//
this.stream.doWork();
if (this.checkError(globalThis) catch return .zero) {
this.stream.updateWriteResult(&this.write_result.?[1], &this.write_result.?[0]);
this.write_in_progress = false;
}
this.deref();
return .undefined;
}
pub fn reset(this: *T, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue {
_ = callframe;
const err = this.stream.reset();
if (err.isError()) {
this.emitError(globalThis, err) catch return .zero;
}
return .undefined;
}
pub fn close(this: *T, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue {
_ = globalThis;
_ = callframe;
this._close();
return .undefined;
}
fn _close(this: *T) void {
if (this.write_in_progress) {
this.pending_close = true;
return;
}
this.pending_close = false;
this.closed = true;
this.this_value.deinit();
this.stream.close();
}
pub fn setOnError(this: *T, globalThis: *JSC.JSGlobalObject, value: JSC.JSValue) bool {
if (value.isFunction()) {
this.onerror_value.set(globalThis, value);
}
return true;
}
pub fn getOnError(this: *T, globalThis: *JSC.JSGlobalObject) JSC.JSValue {
_ = globalThis;
return this.onerror_value.get() orelse .undefined;
}
/// returns true if no error was detected/emitted
fn checkError(this: *T, globalThis: *JSC.JSGlobalObject) !bool {
const err = this.stream.getErrorInfo();
if (!err.isError()) return true;
try this.emitError(globalThis, err);
return false;
}
fn emitError(this: *T, globalThis: *JSC.JSGlobalObject, 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 });
this.write_in_progress = false;
if (this.pending_close) _ = this._close();
}
pub fn finalize(this: *T) void {
this.deref();
}
};
}
pub const NativeZlib = JSC.Codegen.JSNativeZlib.getConstructor;
const CountedKeepAlive = struct {
keep_alive: bun.Async.KeepAlive = .{},
ref_count: u32 = 0,
pub fn ref(this: *@This(), vm: *JSC.VirtualMachine) void {
if (this.ref_count == 0) {
this.keep_alive.ref(vm);
}
this.ref_count += 1;
}
pub fn unref(this: *@This(), vm: *JSC.VirtualMachine) void {
this.ref_count -= 1;
if (this.ref_count == 0) {
this.keep_alive.unref(vm);
}
}
pub fn deinit(this: *@This()) void {
this.keep_alive.disable();
}
};
pub const SNativeZlib = struct {
pub usingnamespace bun.NewRefCounted(@This(), deinit);
pub usingnamespace JSC.Codegen.JSNativeZlib;
pub usingnamespace CompressionStream(@This());
ref_count: u32 = 1,
mode: bun.zlib.NodeMode,
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,
pending_close: bool = false,
closed: bool = false,
task: JSC.WorkPoolTask = .{ .callback = undefined },
pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) ?*@This() {
const arguments = callframe.argumentsUndef(4).ptr;
var mode = arguments[0];
if (!mode.isNumber()) {
_ = globalThis.throwInvalidArgumentTypeValue("mode", "number", mode);
return null;
}
const mode_double = mode.asNumber();
if (@mod(mode_double, 1.0) != 0.0) {
_ = globalThis.throwInvalidArgumentTypeValue("mode", "integer", mode);
return null;
}
const mode_int: i64 = @intFromFloat(mode_double);
if (mode_int < 1 or mode_int > 7) {
_ = globalThis.throwRangeError(mode_int, .{ .field_name = "mode", .min = 1, .max = 7 });
return null;
}
const ptr = SNativeZlib.new(.{
.mode = @enumFromInt(mode_int),
.globalThis = globalThis,
});
ptr.stream.mode = ptr.mode;
return ptr;
}
//// 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 init(this: *SNativeZlib, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue {
const arguments = callframe.argumentsUndef(7).slice();
if (arguments.len != 7) {
globalThis.ERR_MISSING_ARGS("init(windowBits, level, memLevel, strategy, writeResult, writeCallback, dictionary)", .{}).throw();
return .zero;
}
const windowBits = validators.validateInt32(globalThis, arguments[0], "windowBits", .{}, null, null) catch return .zero;
const level = validators.validateInt32(globalThis, arguments[1], "level", .{}, null, null) catch return .zero;
const memLevel = validators.validateInt32(globalThis, arguments[2], "memLevel", .{}, null, null) catch return .zero;
const strategy = validators.validateInt32(globalThis, arguments[3], "strategy", .{}, null, null) catch return .zero;
// 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[4].asArrayBuffer(globalThis).?.asU32().ptr;
const writeCallback = validators.validateFunction(globalThis, arguments[5], "writeCallback", .{}) catch return .zero;
const dictionary = if (arguments[6].isUndefined()) null else arguments[6].asArrayBuffer(globalThis).?.byteSlice();
this.write_result = writeResult;
this.write_callback.set(globalThis, writeCallback);
this.stream.init(level, windowBits, memLevel, strategy, dictionary);
return .undefined;
}
pub fn params(this: *SNativeZlib, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue {
const arguments = callframe.argumentsUndef(2).slice();
if (arguments.len != 2) {
globalThis.ERR_MISSING_ARGS("params(level, strategy)", .{}).throw();
return .zero;
}
const level = validators.validateInt32(globalThis, arguments[0], "level", .{}, null, null) catch return .zero;
const strategy = validators.validateInt32(globalThis, arguments[1], "strategy", .{}, null, null) catch return .zero;
const err = this.stream.setParams(level, strategy);
if (err.isError()) {
this.emitError(globalThis, err) catch return .zero;
}
return .undefined;
}
pub fn deinit(this: *@This()) void {
this.write_callback.deinit();
this.onerror_value.deinit();
this.poll_ref.deinit();
this.destroy();
}
};
const Error = struct {
msg: ?[*:0]const u8,
err: c_int,
code: ?[*:0]const u8,
pub const ok: Error = init(null, 0, null);
pub fn init(msg: ?[*:0]const u8, err: c_int, code: ?[*:0]const u8) Error {
return .{
.msg = msg,
.err = err,
.code = code,
};
}
pub fn isError(this: Error) bool {
return this.msg != null;
}
};
const ZlibContext = struct {
const c = bun.zlib;
const GZIP_HEADER_ID1: u8 = 0x1f;
const GZIP_HEADER_ID2: u8 = 0x8b;
mode: c.NodeMode = .NONE,
state: c.z_stream = std.mem.zeroes(c.z_stream),
err: c.ReturnCode = .Ok,
flush: c.FlushValue = .NoFlush,
dictionary: []const u8 = "",
gzip_id_bytes_read: u8 = 0,
pub fn init(this: *ZlibContext, level: c_int, windowBits: c_int, memLevel: c_int, strategy: c_int, dictionary: ?[]const u8) void {
this.flush = .NoFlush;
this.err = .Ok;
const windowBitsActual = switch (this.mode) {
.NONE => unreachable,
.DEFLATE, .INFLATE => windowBits,
.GZIP, .GUNZIP => windowBits + 16,
.UNZIP => windowBits + 32,
.DEFLATERAW, .INFLATERAW => windowBits * -1,
.BROTLI_DECODE, .BROTLI_ENCODE => unreachable,
};
this.dictionary = dictionary orelse "";
switch (this.mode) {
.NONE => unreachable,
.DEFLATE, .GZIP, .DEFLATERAW => this.err = c.deflateInit2_(&this.state, level, 8, windowBitsActual, memLevel, strategy, c.zlibVersion(), @sizeOf(c.z_stream)),
.INFLATE, .GUNZIP, .UNZIP, .INFLATERAW => this.err = c.inflateInit2_(&this.state, windowBitsActual, c.zlibVersion(), @sizeOf(c.z_stream)),
.BROTLI_DECODE => @panic("TODO"),
.BROTLI_ENCODE => @panic("TODO"),
}
if (this.err != .Ok) {
this.mode = .NONE;
return;
}
_ = this.setDictionary();
}
pub fn setDictionary(this: *ZlibContext) Error {
const dict = this.dictionary;
if (dict.len == 0) return Error.ok;
this.err = .Ok;
switch (this.mode) {
.DEFLATE, .DEFLATERAW => {
this.err = c.deflateSetDictionary(&this.state, dict.ptr, @intCast(dict.len));
},
.INFLATERAW => {
this.err = c.inflateSetDictionary(&this.state, dict.ptr, @intCast(dict.len));
},
else => {},
}
if (this.err != .Ok) {
return this.error_for_message("Failed to set dictionary");
}
return Error.ok;
}
pub fn setParams(this: *ZlibContext, level: c_int, strategy: c_int) Error {
this.err = .Ok;
switch (this.mode) {
.DEFLATE, .DEFLATERAW => {
this.err = c.deflateParams(&this.state, level, strategy);
},
else => {},
}
if (this.err != .Ok and this.err != .BufError) {
return this.error_for_message("Failed to set parameters");
}
return Error.ok;
}
fn error_for_message(this: *ZlibContext, default: [*:0]const u8) Error {
var message = default;
if (this.state.err_msg) |msg| message = msg;
return .{
.msg = message,
.err = @intFromEnum(this.err),
.code = @tagName(this.err),
};
}
pub fn reset(this: *ZlibContext) Error {
this.err = .Ok;
switch (this.mode) {
.DEFLATE, .DEFLATERAW, .GZIP => {
this.err = c.deflateReset(&this.state);
},
.INFLATE, .INFLATERAW, .GUNZIP => {
this.err = c.inflateReset(&this.state);
},
else => {},
}
if (this.err != .Ok) {
return this.error_for_message("Failed to reset stream");
}
return this.setDictionary();
}
pub fn setBuffers(this: *ZlibContext, in: ?[]const u8, out: ?[]u8) void {
this.state.avail_in = if (in) |p| @intCast(p.len) else 0;
this.state.next_in = if (in) |p| p.ptr else null;
this.state.avail_out = if (out) |p| @intCast(p.len) else 0;
this.state.next_out = if (out) |p| p.ptr else null;
}
pub fn setFlush(this: *ZlibContext, flush: c_int) void {
this.flush = @enumFromInt(flush);
}
pub fn doWork(this: *ZlibContext) void {
var next_expected_header_byte: ?[*]const u8 = null;
// If the avail_out is left at 0, then it means that it ran out
// of room. If there was avail_out left over, then it means
// that all of the input was consumed.
switch (this.mode) {
.DEFLATE, .GZIP, .DEFLATERAW => {
return this.doWorkDeflate();
},
.UNZIP => {
if (this.state.avail_in > 0) {
next_expected_header_byte = this.state.next_in.?;
}
if (this.gzip_id_bytes_read == 0) {
if (next_expected_header_byte == null) {
return this.doWorkInflate();
}
if (next_expected_header_byte.?[0] == GZIP_HEADER_ID1) {
this.gzip_id_bytes_read = 1;
next_expected_header_byte.? += 1;
if (this.state.avail_in == 1) { // The only available byte was already read.
return this.doWorkInflate();
}
} else {
this.mode = .INFLATE;
return this.doWorkInflate();
}
}
if (this.gzip_id_bytes_read == 1) {
if (next_expected_header_byte == null) {
return this.doWorkInflate();
}
if (next_expected_header_byte.?[0] == GZIP_HEADER_ID2) {
this.gzip_id_bytes_read = 2;
this.mode = .GUNZIP;
} else {
this.mode = .INFLATE;
}
return this.doWorkInflate();
}
bun.assert(false); // invalid number of gzip magic number bytes read
},
.INFLATE, .GUNZIP, .INFLATERAW => {
return this.doWorkInflate();
},
.NONE => {},
.BROTLI_ENCODE, .BROTLI_DECODE => {},
}
}
fn doWorkDeflate(this: *ZlibContext) void {
this.err = c.deflate(&this.state, this.flush);
}
fn doWorkInflate(this: *ZlibContext) void {
this.err = c.inflate(&this.state, this.flush);
if (this.mode != .INFLATERAW and this.err == .NeedDict and this.dictionary.len > 0) {
this.err = c.inflateSetDictionary(&this.state, this.dictionary.ptr, @intCast(this.dictionary.len));
if (this.err == .Ok) {
this.err = c.inflate(&this.state, this.flush);
} else if (this.err == .DataError) {
this.err = .NeedDict;
}
}
while (this.state.avail_in > 0 and this.mode == .GUNZIP and this.err == .StreamEnd and this.state.next_in.?[0] != 0) {
// Bytes remain in input buffer. Perhaps this is another compressed member in the same archive, or just trailing garbage.
// Trailing zero bytes are okay, though, since they are frequently used for padding.
_ = this.reset();
this.err = c.inflate(&this.state, this.flush);
}
}
pub fn updateWriteResult(this: *ZlibContext, avail_in: *u32, avail_out: *u32) void {
avail_in.* = this.state.avail_in;
avail_out.* = this.state.avail_out;
}
pub fn getErrorInfo(this: *ZlibContext) Error {
switch (this.err) {
.Ok, .BufError => {
if (this.state.avail_out != 0 and this.flush == .Finish) {
return this.error_for_message("unexpected end of file");
}
},
.StreamEnd => {},
.NeedDict => {
if (this.dictionary.len == 0) {
return this.error_for_message("Missing dictionary");
} else {
return this.error_for_message("Bad dictionary");
}
},
else => {
return this.error_for_message("Zlib error");
},
}
return Error.ok;
}
pub fn close(this: *ZlibContext) void {
var status = c.ReturnCode.Ok;
switch (this.mode) {
.DEFLATE, .DEFLATERAW, .GZIP => {
status = c.deflateEnd(&this.state);
},
.INFLATE, .INFLATERAW, .GUNZIP, .UNZIP => {
status = c.inflateEnd(&this.state);
},
.NONE => {},
.BROTLI_ENCODE, .BROTLI_DECODE => {},
}
bun.assert(status == .Ok or status == .DataError);
this.mode = .NONE;
}
};
pub const NativeBrotli = JSC.Codegen.JSNativeBrotli.getConstructor;
pub const SNativeBrotli = struct {
pub usingnamespace bun.NewRefCounted(@This(), deinit);
pub usingnamespace JSC.Codegen.JSNativeZlib;
pub usingnamespace CompressionStream(@This());
ref_count: u32 = 1,
mode: bun.zlib.NodeMode,
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,
pending_close: bool = false,
closed: bool = false,
task: JSC.WorkPoolTask = .{
.callback = undefined,
},
pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) ?*@This() {
const arguments = callframe.argumentsUndef(1).ptr;
var mode = arguments[0];
if (!mode.isNumber()) {
_ = globalThis.throwInvalidArgumentTypeValue("mode", "number", mode);
return null;
}
const mode_double = mode.asNumber();
if (@mod(mode_double, 1.0) != 0.0) {
_ = globalThis.throwInvalidArgumentTypeValue("mode", "integer", mode);
return null;
}
const mode_int: i64 = @intFromFloat(mode_double);
if (mode_int < 8 or mode_int > 9) {
_ = globalThis.throwRangeError(mode_int, .{ .field_name = "mode", .min = 8, .max = 9 });
return null;
}
const ptr = @This().new(.{
.mode = @enumFromInt(mode_int),
.globalThis = globalThis,
});
ptr.stream.mode = ptr.mode;
ptr.stream.mode_ = ptr.mode;
return ptr;
}
pub fn estimatedSize(this: *const SNativeBrotli) usize {
const encoder_state_size: usize = 5143; // @sizeOf(@cImport(@cInclude("brotli/encode.h")).BrotliEncoderStateStruct)
const decoder_state_size: usize = 855; // @sizeOf(@cImport(@cInclude("brotli/decode.h")).BrotliDecoderStateStruct)
return @sizeOf(SNativeBrotli) + switch (this.mode) {
.BROTLI_ENCODE => encoder_state_size,
.BROTLI_DECODE => decoder_state_size,
else => 0,
};
}
pub fn init(this: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue {
const arguments = callframe.argumentsUndef(3).slice();
if (arguments.len != 3) {
globalThis.ERR_MISSING_ARGS("init(params, writeResult, writeCallback)", .{}).throw();
return .zero;
}
// 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 = validators.validateFunction(globalThis, arguments[2], "writeCallback", .{}) catch return .zero;
this.write_result = writeResult;
this.write_callback.set(globalThis, writeCallback);
var err = this.stream.init();
if (err.isError()) {
this.emitError(globalThis, err) catch return .zero;
return JSC.jsBoolean(false);
}
const params_ = arguments[0].asArrayBuffer(globalThis).?.asU32();
for (params_, 0..) |d, i| {
// (d == -1) {
if (d == std.math.maxInt(u32)) {
continue;
}
err = this.stream.setParams(@intCast(i), d);
if (err.isError()) {
// this.emitError(globalThis, err) catch return .zero; //XXX: onerror isn't set yet
return JSC.jsBoolean(false);
}
}
return JSC.jsBoolean(true);
}
pub fn params(this: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSC.JSValue {
_ = this;
_ = globalThis;
_ = callframe;
// intentionally left empty
return .undefined;
}
pub fn deinit(this: *@This()) void {
this.write_callback.deinit();
this.onerror_value.deinit();
this.poll_ref.deinit();
this.destroy();
}
};
const BrotliContext = struct {
const c = bun.brotli.c;
const Op = bun.brotli.c.BrotliEncoder.Operation;
mode: bun.zlib.NodeMode = .NONE,
mode_: bun.zlib.NodeMode = .NONE,
state: *anyopaque = undefined,
next_in: ?[*]const u8 = null,
next_out: ?[*]u8 = null,
avail_in: usize = 0,
avail_out: usize = 0,
flush: Op = .process,
last_result: extern union { e: c_int, d: c.BrotliDecoderResult } = @bitCast(@as(u32, 0)),
error_: c.BrotliDecoderErrorCode2 = .NO_ERROR,
pub fn init(this: *BrotliContext) Error {
switch (this.mode_) {
.BROTLI_ENCODE => {
const alloc = &bun.brotli.BrotliAllocator.alloc;
const free = &bun.brotli.BrotliAllocator.free;
const state = c.BrotliEncoderCreateInstance(alloc, free, null);
if (state == null) {
return Error.init("Could not initialize Brotli instance", -1, "ERR_ZLIB_INITIALIZATION_FAILED");
}
this.state = @ptrCast(state.?);
return Error.ok;
},
.BROTLI_DECODE => {
const alloc = &bun.brotli.BrotliAllocator.alloc;
const free = &bun.brotli.BrotliAllocator.free;
const state = c.BrotliDecoderCreateInstance(alloc, free, null);
if (state == null) {
return Error.init("Could not initialize Brotli instance", -1, "ERR_ZLIB_INITIALIZATION_FAILED");
}
this.state = @ptrCast(state.?);
return Error.ok;
},
else => unreachable,
}
}
pub fn setParams(this: *BrotliContext, key: c_uint, value: u32) Error {
switch (this.mode_) {
.BROTLI_ENCODE => {
if (c.BrotliEncoderSetParameter(@ptrCast(this.state), key, value) == 0) {
return Error.init("Setting parameter failed", -1, "ERR_BROTLI_PARAM_SET_FAILED");
}
return Error.ok;
},
.BROTLI_DECODE => {
if (c.BrotliDecoderSetParameter(@ptrCast(this.state), key, value) == 0) {
return Error.init("Setting parameter failed", -1, "ERR_BROTLI_PARAM_SET_FAILED");
}
return Error.ok;
},
else => unreachable,
}
}
pub fn reset(this: *BrotliContext) Error {
return this.init();
}
pub fn setBuffers(this: *BrotliContext, in: ?[]const u8, out: ?[]u8) void {
this.next_in = if (in) |p| p.ptr else null;
this.next_out = if (out) |p| p.ptr else null;
this.avail_in = if (in) |p| p.len else 0;
this.avail_out = if (out) |p| p.len else 0;
}
pub fn setFlush(this: *BrotliContext, flush: c_int) void {
this.flush = @enumFromInt(flush);
}
pub fn doWork(this: *BrotliContext) void {
switch (this.mode_) {
.BROTLI_ENCODE => {
var next_in = this.next_in;
this.last_result.e = c.BrotliEncoderCompressStream(@ptrCast(this.state), this.flush, &this.avail_in, &next_in, &this.avail_out, &this.next_out, null);
this.next_in.? += @intFromPtr(next_in.?) - @intFromPtr(this.next_in.?);
},
.BROTLI_DECODE => {
var next_in = this.next_in;
this.last_result.d = c.BrotliDecoderDecompressStream(@ptrCast(this.state), &this.avail_in, &next_in, &this.avail_out, &this.next_out, null);
this.next_in.? += @intFromPtr(next_in.?) - @intFromPtr(this.next_in.?);
if (this.last_result.d == .err) {
this.error_ = c.BrotliDecoderGetErrorCode(@ptrCast(this.state));
}
},
else => unreachable,
}
}
pub fn updateWriteResult(this: *BrotliContext, avail_in: *u32, avail_out: *u32) void {
avail_in.* = @intCast(this.avail_in);
avail_out.* = @intCast(this.avail_out);
}
pub fn getErrorInfo(this: *BrotliContext) Error {
switch (this.mode_) {
.BROTLI_ENCODE => {
if (this.last_result.e == 0) {
return Error.init("Compression failed", -1, "ERR_BROTLI_COMPRESSION_FAILED");
}
return Error.ok;
},
.BROTLI_DECODE => {
if (this.error_ != .NO_ERROR) {
return Error.init("Decompression failed", @intFromEnum(this.error_), code_for_error(this.error_));
} else if (this.flush == .finish and this.last_result.d == .needs_more_input) {
return Error.init("unexpected end of file", @intFromEnum(bun.zlib.ReturnCode.BufError), "Z_BUF_ERROR");
}
return Error.ok;
},
else => unreachable,
}
}
pub fn close(this: *BrotliContext) void {
switch (this.mode_) {
.BROTLI_ENCODE => c.BrotliEncoderDestroyInstance(@ptrCast(@alignCast(this.state))),
.BROTLI_DECODE => c.BrotliDecoderDestroyInstance(@ptrCast(@alignCast(this.state))),
else => unreachable,
}
this.mode = .NONE;
}
fn code_for_error(err: c.BrotliDecoderErrorCode2) [:0]const u8 {
const E = c.BrotliDecoderErrorCode2;
const names = comptime std.meta.fieldNames(E);
const values = comptime std.enums.values(E);
inline for (names, values) |n, v| {
if (err == v) {
return "ERR_BROTLI_DECODER_" ++ n;
}
}
unreachable;
}
};