diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index d6c8f28b42..5105068633 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -1,5 +1,3 @@ -const conv = std.builtin.CallingConvention.Unspecified; -const S3File = @import("../webcore/S3File.zig"); /// How to add a new function or property to the Bun global /// /// - Add a callback or property to the below struct @@ -7,7 +5,7 @@ const S3File = @import("../webcore/S3File.zig"); /// - Update "@begin bunObjectTable" in BunObject.cpp /// - Getters use a generated wrapper function `BunObject_getter_wrap_` /// - Update "BunObject+exports.h" -/// - Run "make dev" +/// - Run `bun run build` pub const BunObject = struct { // --- Callbacks --- pub const allocUnsafe = toJSCallback(Bun.allocUnsafe); @@ -170,88 +168,6 @@ pub const BunObject = struct { } }; -const Bun = @This(); -const default_allocator = bun.default_allocator; -const bun = @import("root").bun; -const uv = bun.windows.libuv; -const Environment = bun.Environment; - -const Global = bun.Global; -const strings = bun.strings; -const string = bun.string; -const Output = bun.Output; -const MutableString = bun.MutableString; -const std = @import("std"); -const Allocator = std.mem.Allocator; -const IdentityContext = @import("../../identity_context.zig").IdentityContext; -const Fs = @import("../../fs.zig"); -const Resolver = @import("../../resolver/resolver.zig"); -const ast = @import("../../import_record.zig"); - -const MacroEntryPoint = bun.transpiler.MacroEntryPoint; -const logger = bun.logger; -const Api = @import("../../api/schema.zig").Api; -const options = @import("../../options.zig"); -const ServerEntryPoint = bun.transpiler.ServerEntryPoint; -const js_printer = bun.js_printer; -const js_parser = bun.js_parser; -const js_ast = bun.JSAst; -const NodeFallbackModules = @import("../../node_fallbacks.zig"); -const ImportKind = ast.ImportKind; -const Analytics = @import("../../analytics/analytics_thread.zig"); -const ZigString = bun.JSC.ZigString; -const Runtime = @import("../../runtime.zig"); -const Router = @import("./filesystem_router.zig"); -const ImportRecord = ast.ImportRecord; -const DotEnv = @import("../../env_loader.zig"); -const ParseResult = bun.transpiler.ParseResult; -const PackageJSON = @import("../../resolver/package_json.zig").PackageJSON; -const MacroRemap = @import("../../resolver/package_json.zig").MacroMap; -const WebCore = bun.JSC.WebCore; -const Request = WebCore.Request; -const Response = WebCore.Response; -const Headers = WebCore.Headers; -const Fetch = WebCore.Fetch; -const js = bun.JSC.C; -const JSC = bun.JSC; -const JSError = @import("../base.zig").JSError; - -const MarkedArrayBuffer = @import("../base.zig").MarkedArrayBuffer; -const getAllocator = @import("../base.zig").getAllocator; -const JSValue = bun.JSC.JSValue; - -const JSGlobalObject = bun.JSC.JSGlobalObject; -const ExceptionValueRef = bun.JSC.ExceptionValueRef; -const JSPrivateDataPtr = bun.JSC.JSPrivateDataPtr; -const ConsoleObject = bun.JSC.ConsoleObject; -const Node = bun.JSC.Node; -const ZigException = bun.JSC.ZigException; -const ZigStackTrace = bun.JSC.ZigStackTrace; -const ErrorableResolvedSource = bun.JSC.ErrorableResolvedSource; -const ResolvedSource = bun.JSC.ResolvedSource; -const JSPromise = bun.JSC.JSPromise; -const JSInternalPromise = bun.JSC.JSInternalPromise; -const JSModuleLoader = bun.JSC.JSModuleLoader; -const JSPromiseRejectionOperation = bun.JSC.JSPromiseRejectionOperation; -const ErrorableZigString = bun.JSC.ErrorableZigString; -const VM = bun.JSC.VM; -const JSFunction = bun.JSC.JSFunction; -const Config = @import("../config.zig"); -const URL = @import("../../url.zig").URL; -const Transpiler = bun.JSC.API.JSTranspiler; -const JSBundler = bun.JSC.API.JSBundler; -const VirtualMachine = JSC.VirtualMachine; -const IOTask = JSC.IOTask; -const zlib = @import("../../zlib.zig"); -const Which = @import("../../which.zig"); -const ErrorableString = JSC.ErrorableString; -const max_addressable_memory = std.math.maxInt(u56); -const glob = @import("../../glob.zig"); -const Async = bun.Async; -const SemverObject = bun.Semver.SemverObject; -const Braces = @import("../../shell/braces.zig"); -const Shell = @import("../../shell/shell.zig"); - pub fn shellEscape(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.arguments_old(1); if (arguments.len < 1) { @@ -994,16 +910,6 @@ export fn Bun__resolveSyncWithSource(global: *JSGlobalObject, specifier: JSValue return JSC.toJSHostValue(global, doResolveWithArgs(global, specifier_str, source.*, is_esm, true, is_user_require_resolve)); } -extern fn dump_zone_malloc_stats() void; - -fn dump_mimalloc(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { - globalObject.bunVM().arena.dumpStats(); - if (bun.heap_breakdown.enabled) { - dump_zone_malloc_stats(); - } - return .undefined; -} - pub fn indexOfLine(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments_ = callframe.arguments_old(2); const arguments = arguments_.slice(); @@ -1051,2154 +957,7 @@ pub fn indexOfLine(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) b return JSC.JSValue.jsNumberFromInt32(-1); } -pub const Crypto = struct { - const Hashers = @import("../../sha.zig"); - - const BoringSSL = bun.BoringSSL.c; - - pub const HMAC = struct { - ctx: BoringSSL.HMAC_CTX, - algorithm: EVP.Algorithm, - - pub usingnamespace bun.New(@This()); - - pub fn init(algorithm: EVP.Algorithm, key: []const u8) ?*HMAC { - const md = algorithm.md() orelse return null; - var ctx: BoringSSL.HMAC_CTX = undefined; - BoringSSL.HMAC_CTX_init(&ctx); - if (BoringSSL.HMAC_Init_ex(&ctx, key.ptr, @intCast(key.len), md, null) != 1) { - BoringSSL.HMAC_CTX_cleanup(&ctx); - return null; - } - return HMAC.new(.{ - .ctx = ctx, - .algorithm = algorithm, - }); - } - - pub fn update(this: *HMAC, data: []const u8) void { - _ = BoringSSL.HMAC_Update(&this.ctx, data.ptr, data.len); - } - - pub fn size(this: *const HMAC) usize { - return BoringSSL.HMAC_size(&this.ctx); - } - - pub fn copy(this: *HMAC) !*HMAC { - var ctx: BoringSSL.HMAC_CTX = undefined; - BoringSSL.HMAC_CTX_init(&ctx); - if (BoringSSL.HMAC_CTX_copy(&ctx, &this.ctx) != 1) { - BoringSSL.HMAC_CTX_cleanup(&ctx); - return error.BoringSSLError; - } - return HMAC.new(.{ - .ctx = ctx, - .algorithm = this.algorithm, - }); - } - - pub fn final(this: *HMAC, out: []u8) []u8 { - var outlen: c_uint = undefined; - _ = BoringSSL.HMAC_Final(&this.ctx, out.ptr, &outlen); - return out[0..outlen]; - } - - pub fn deinit(this: *HMAC) void { - BoringSSL.HMAC_CTX_cleanup(&this.ctx); - this.destroy(); - } - }; - - pub const EVP = struct { - ctx: BoringSSL.EVP_MD_CTX = undefined, - md: *const BoringSSL.EVP_MD = undefined, - algorithm: Algorithm, - - // we do this to avoid asking BoringSSL what the digest name is - // because that API is confusing - pub const Algorithm = enum { - // @"DSA-SHA", - // @"DSA-SHA1", - // @"MD5-SHA1", - // @"RSA-MD5", - // @"RSA-RIPEMD160", - // @"RSA-SHA1", - // @"RSA-SHA1-2", - // @"RSA-SHA224", - // @"RSA-SHA256", - // @"RSA-SHA384", - // @"RSA-SHA512", - // @"ecdsa-with-SHA1", - blake2b256, - blake2b512, - md4, - md5, - ripemd160, - sha1, - sha224, - sha256, - sha384, - sha512, - @"sha512-224", - @"sha512-256", - - @"sha3-224", - @"sha3-256", - @"sha3-384", - @"sha3-512", - shake128, - shake256, - - pub fn md(this: Algorithm) ?*const BoringSSL.EVP_MD { - return switch (this) { - .blake2b256 => BoringSSL.EVP_blake2b256(), - .blake2b512 => BoringSSL.EVP_blake2b512(), - .md4 => BoringSSL.EVP_md4(), - .md5 => BoringSSL.EVP_md5(), - .ripemd160 => BoringSSL.EVP_ripemd160(), - .sha1 => BoringSSL.EVP_sha1(), - .sha224 => BoringSSL.EVP_sha224(), - .sha256 => BoringSSL.EVP_sha256(), - .sha384 => BoringSSL.EVP_sha384(), - .sha512 => BoringSSL.EVP_sha512(), - .@"sha512-224" => BoringSSL.EVP_sha512_224(), - .@"sha512-256" => BoringSSL.EVP_sha512_256(), - else => null, - }; - } - - pub const names: std.EnumArray(Algorithm, bun.String) = brk: { - var all = std.EnumArray(Algorithm, bun.String).initUndefined(); - var iter = all.iterator(); - while (iter.next()) |entry| { - entry.value.* = bun.String.init(@tagName(entry.key)); - } - break :brk all; - }; - - pub const map = bun.ComptimeStringMap(Algorithm, .{ - .{ "blake2b256", .blake2b256 }, - .{ "blake2b512", .blake2b512 }, - .{ "ripemd160", .ripemd160 }, - .{ "rmd160", .ripemd160 }, - .{ "md4", .md4 }, - .{ "md5", .md5 }, - .{ "sha1", .sha1 }, - .{ "sha128", .sha1 }, - .{ "sha224", .sha224 }, - .{ "sha256", .sha256 }, - .{ "sha384", .sha384 }, - .{ "sha512", .sha512 }, - .{ "sha-1", .sha1 }, - .{ "sha-224", .sha224 }, - .{ "sha-256", .sha256 }, - .{ "sha-384", .sha384 }, - .{ "sha-512", .sha512 }, - .{ "sha-512/224", .@"sha512-224" }, - .{ "sha-512_224", .@"sha512-224" }, - .{ "sha-512224", .@"sha512-224" }, - .{ "sha512-224", .@"sha512-224" }, - .{ "sha-512/256", .@"sha512-256" }, - .{ "sha-512_256", .@"sha512-256" }, - .{ "sha-512256", .@"sha512-256" }, - .{ "sha512-256", .@"sha512-256" }, - .{ "sha384", .sha384 }, - .{ "sha3-224", .@"sha3-224" }, - .{ "sha3-256", .@"sha3-256" }, - .{ "sha3-384", .@"sha3-384" }, - .{ "sha3-512", .@"sha3-512" }, - .{ "shake128", .shake128 }, - .{ "shake256", .shake256 }, - // .{ "md5-sha1", .@"MD5-SHA1" }, - // .{ "dsa-sha", .@"DSA-SHA" }, - // .{ "dsa-sha1", .@"DSA-SHA1" }, - // .{ "ecdsa-with-sha1", .@"ecdsa-with-SHA1" }, - // .{ "rsa-md5", .@"RSA-MD5" }, - // .{ "rsa-sha1", .@"RSA-SHA1" }, - // .{ "rsa-sha1-2", .@"RSA-SHA1-2" }, - // .{ "rsa-sha224", .@"RSA-SHA224" }, - // .{ "rsa-sha256", .@"RSA-SHA256" }, - // .{ "rsa-sha384", .@"RSA-SHA384" }, - // .{ "rsa-sha512", .@"RSA-SHA512" }, - // .{ "rsa-ripemd160", .@"RSA-RIPEMD160" }, - }); - }; - - pub const Digest = [BoringSSL.EVP_MAX_MD_SIZE]u8; - - /// For usage in Zig - pub fn pbkdf2( - output: []u8, - password: []const u8, - salt: []const u8, - iteration_count: u32, - algorithm: Algorithm, - ) ?[]const u8 { - var pbk = PBKDF2{ - .algorithm = algorithm, - .password = JSC.Node.StringOrBuffer{ .encoded_slice = JSC.ZigString.Slice.fromUTF8NeverFree(password) }, - .salt = JSC.Node.StringOrBuffer{ .encoded_slice = JSC.ZigString.Slice.fromUTF8NeverFree(salt) }, - .iteration_count = iteration_count, - .length = @intCast(output.len), - }; - - if (!pbk.run(output)) { - return null; - } - - return output; - } - - pub const PBKDF2 = struct { - password: JSC.Node.StringOrBuffer = JSC.Node.StringOrBuffer.empty, - salt: JSC.Node.StringOrBuffer = JSC.Node.StringOrBuffer.empty, - iteration_count: u32 = 1, - length: i32 = 0, - algorithm: EVP.Algorithm, - - pub fn run(this: *PBKDF2, output: []u8) bool { - const password = this.password.slice(); - const salt = this.salt.slice(); - const algorithm = this.algorithm; - const iteration_count = this.iteration_count; - const length = this.length; - - @memset(output, 0); - assert(this.length <= @as(i32, @intCast(output.len))); - BoringSSL.ERR_clear_error(); - const rc = BoringSSL.PKCS5_PBKDF2_HMAC( - if (password.len > 0) password.ptr else null, - @intCast(password.len), - salt.ptr, - @intCast(salt.len), - @intCast(iteration_count), - algorithm.md().?, - @intCast(length), - output.ptr, - ); - - if (rc <= 0) { - return false; - } - - return true; - } - - pub const Job = struct { - pbkdf2: PBKDF2, - output: []u8 = &[_]u8{}, - task: JSC.WorkPoolTask = .{ .callback = &runTask }, - promise: JSC.JSPromise.Strong = .{}, - vm: *JSC.VirtualMachine, - err: ?u32 = null, - any_task: JSC.AnyTask = undefined, - poll: Async.KeepAlive = .{}, - - pub usingnamespace bun.New(@This()); - - pub fn runTask(task: *JSC.WorkPoolTask) void { - const job: *PBKDF2.Job = @fieldParentPtr("task", task); - defer job.vm.enqueueTaskConcurrent(JSC.ConcurrentTask.create(job.any_task.task())); - job.output = bun.default_allocator.alloc(u8, @as(usize, @intCast(job.pbkdf2.length))) catch { - job.err = BoringSSL.EVP_R_MEMORY_LIMIT_EXCEEDED; - return; - }; - if (!job.pbkdf2.run(job.output)) { - job.err = BoringSSL.ERR_get_error(); - BoringSSL.ERR_clear_error(); - - bun.default_allocator.free(job.output); - job.output = &[_]u8{}; - } - } - - pub fn runFromJS(this: *Job) void { - defer this.deinit(); - if (this.vm.isShuttingDown()) { - return; - } - - const globalThis = this.vm.global; - const promise = this.promise.swap(); - if (this.err) |err| { - promise.reject(globalThis, createCryptoError(globalThis, err)); - return; - } - - const output_slice = this.output; - assert(output_slice.len == @as(usize, @intCast(this.pbkdf2.length))); - const buffer_value = JSC.JSValue.createBuffer(globalThis, output_slice, bun.default_allocator); - if (buffer_value == .zero) { - promise.reject(globalThis, ZigString.init("Failed to create buffer").toErrorInstance(globalThis)); - return; - } - - this.output = &[_]u8{}; - promise.resolve(globalThis, buffer_value); - } - - pub fn deinit(this: *Job) void { - this.poll.unref(this.vm); - this.pbkdf2.deinitAndUnprotect(); - this.promise.deinit(); - bun.default_allocator.free(this.output); - this.destroy(); - } - - pub fn create(vm: *JSC.VirtualMachine, globalThis: *JSC.JSGlobalObject, data: *const PBKDF2) *Job { - var job = Job.new(.{ - .pbkdf2 = data.*, - .vm = vm, - .any_task = undefined, - }); - - job.promise = JSC.JSPromise.Strong.init(globalThis); - job.any_task = JSC.AnyTask.New(@This(), &runFromJS).init(job); - job.poll.ref(vm); - JSC.WorkPool.schedule(&job.task); - - return job; - } - }; - - pub fn deinitAndUnprotect(this: *PBKDF2) void { - this.password.deinitAndUnprotect(); - this.salt.deinitAndUnprotect(); - } - - pub fn deinit(this: *PBKDF2) void { - this.password.deinit(); - this.salt.deinit(); - } - - pub fn fromJS(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame, is_async: bool) bun.JSError!PBKDF2 { - const arg0, const arg1, const arg2, const arg3, const arg4, const arg5 = callFrame.argumentsAsArray(6); - - if (!arg3.isNumber()) { - return globalThis.throwInvalidArgumentTypeValue("keylen", "number", arg3); - } - - const keylen_num = arg3.asNumber(); - - if (std.math.isInf(keylen_num) or std.math.isNan(keylen_num)) { - return globalThis.throwRangeError(keylen_num, .{ - .field_name = "keylen", - .msg = "an integer", - }); - } - - if (keylen_num < 0 or keylen_num > std.math.maxInt(i32)) { - return globalThis.throwRangeError(keylen_num, .{ .field_name = "keylen", .min = 0, .max = std.math.maxInt(i32) }); - } - - const keylen: i32 = @intFromFloat(keylen_num); - - if (globalThis.hasException()) { - return error.JSError; - } - - if (!arg2.isAnyInt()) { - return globalThis.throwInvalidArgumentTypeValue("iterations", "number", arg2); - } - - const iteration_count = arg2.coerce(i64, globalThis); - - if (!globalThis.hasException() and (iteration_count < 1 or iteration_count > std.math.maxInt(i32))) { - return globalThis.throwRangeError(iteration_count, .{ .field_name = "iterations", .min = 1, .max = std.math.maxInt(i32) + 1 }); - } - - if (globalThis.hasException()) { - return error.JSError; - } - - const algorithm = brk: { - if (!arg4.isString()) { - return globalThis.throwInvalidArgumentTypeValue("digest", "string", arg4); - } - - invalid: { - switch (try EVP.Algorithm.map.fromJSCaseInsensitive(globalThis, arg4) orelse break :invalid) { - .shake128, .shake256, .@"sha3-224", .@"sha3-256", .@"sha3-384", .@"sha3-512" => break :invalid, - else => |alg| break :brk alg, - } - } - - if (!globalThis.hasException()) { - const slice = try arg4.toSlice(globalThis, bun.default_allocator); - defer slice.deinit(); - const name = slice.slice(); - return globalThis.ERR_CRYPTO_INVALID_DIGEST("Invalid digest: {s}", .{name}).throw(); - } - return error.JSError; - }; - - var out = PBKDF2{ - .iteration_count = @intCast(iteration_count), - .length = keylen, - .algorithm = algorithm, - }; - defer { - if (globalThis.hasException()) { - if (is_async) - out.deinitAndUnprotect() - else - out.deinit(); - } - } - - const allow_string_object = true; - out.salt = try JSC.Node.StringOrBuffer.fromJSMaybeAsync(globalThis, bun.default_allocator, arg1, is_async, allow_string_object) orelse { - return globalThis.throwInvalidArgumentTypeValue("salt", "string or buffer", arg1); - }; - - if (out.salt.slice().len > std.math.maxInt(i32)) { - return globalThis.throwInvalidArguments("salt is too long", .{}); - } - - out.password = try JSC.Node.StringOrBuffer.fromJSMaybeAsync(globalThis, bun.default_allocator, arg0, is_async, allow_string_object) orelse { - return globalThis.throwInvalidArgumentTypeValue("password", "string or buffer", arg0); - }; - - if (out.password.slice().len > std.math.maxInt(i32)) { - return globalThis.throwInvalidArguments("password is too long", .{}); - } - - if (is_async) { - if (!arg5.isFunction()) { - return globalThis.throwInvalidArgumentTypeValue("callback", "function", arg5); - } - } - - return out; - } - }; - - pub fn init(algorithm: Algorithm, md: *const BoringSSL.EVP_MD, engine: *BoringSSL.ENGINE) EVP { - bun.BoringSSL.load(); - - var ctx: BoringSSL.EVP_MD_CTX = undefined; - BoringSSL.EVP_MD_CTX_init(&ctx); - _ = BoringSSL.EVP_DigestInit_ex(&ctx, md, engine); - return .{ - .ctx = ctx, - .md = md, - .algorithm = algorithm, - }; - } - - pub fn reset(this: *EVP, engine: *BoringSSL.ENGINE) void { - BoringSSL.ERR_clear_error(); - _ = BoringSSL.EVP_DigestInit_ex(&this.ctx, this.md, engine); - } - - pub fn hash(this: *EVP, engine: *BoringSSL.ENGINE, input: []const u8, output: []u8) ?u32 { - BoringSSL.ERR_clear_error(); - var outsize: c_uint = @min(@as(u16, @truncate(output.len)), this.size()); - if (BoringSSL.EVP_Digest(input.ptr, input.len, output.ptr, &outsize, this.md, engine) != 1) { - return null; - } - - return outsize; - } - - pub fn final(this: *EVP, engine: *BoringSSL.ENGINE, output: []u8) []u8 { - BoringSSL.ERR_clear_error(); - var outsize: u32 = @min(@as(u16, @truncate(output.len)), this.size()); - if (BoringSSL.EVP_DigestFinal_ex( - &this.ctx, - output.ptr, - &outsize, - ) != 1) { - return ""; - } - - this.reset(engine); - - return output[0..outsize]; - } - - pub fn update(this: *EVP, input: []const u8) void { - BoringSSL.ERR_clear_error(); - _ = BoringSSL.EVP_DigestUpdate(&this.ctx, input.ptr, input.len); - } - - pub fn size(this: *const EVP) u16 { - return @as(u16, @truncate(BoringSSL.EVP_MD_CTX_size(&this.ctx))); - } - - pub fn copy(this: *const EVP, engine: *BoringSSL.ENGINE) error{OutOfMemory}!EVP { - BoringSSL.ERR_clear_error(); - var new = init(this.algorithm, this.md, engine); - if (BoringSSL.EVP_MD_CTX_copy_ex(&new.ctx, &this.ctx) == 0) { - return error.OutOfMemory; - } - return new; - } - - pub fn byNameAndEngine(engine: *BoringSSL.ENGINE, name: []const u8) ?EVP { - if (Algorithm.map.getWithEql(name, strings.eqlCaseInsensitiveASCIIIgnoreLength)) |algorithm| { - if (algorithm.md()) |md| { - return EVP.init(algorithm, md, engine); - } - - if (BoringSSL.EVP_get_digestbyname(@tagName(algorithm))) |md| { - return EVP.init(algorithm, md, engine); - } - } - - return null; - } - - pub fn byName(name: ZigString, global: *JSC.JSGlobalObject) ?EVP { - var name_str = name.toSlice(global.allocator()); - defer name_str.deinit(); - return byNameAndEngine(global.bunVM().rareData().boringEngine(), name_str.slice()); - } - - pub fn deinit(this: *EVP) void { - // https://github.com/oven-sh/bun/issues/3250 - _ = BoringSSL.EVP_MD_CTX_cleanup(&this.ctx); - } - }; - - pub fn createCryptoError(globalThis: *JSC.JSGlobalObject, err_code: u32) JSValue { - return bun.BoringSSL.ERR_toJS(globalThis, err_code); - } - const unknown_password_algorithm_message = "unknown algorithm, expected one of: \"bcrypt\", \"argon2id\", \"argon2d\", \"argon2i\" (default is \"argon2id\")"; - - pub const PasswordObject = struct { - pub const pwhash = std.crypto.pwhash; - pub const Algorithm = enum { - argon2i, - argon2d, - argon2id, - bcrypt, - - pub const Value = union(Algorithm) { - argon2i: Argon2Params, - argon2d: Argon2Params, - argon2id: Argon2Params, - // bcrypt only accepts "cost" - bcrypt: u6, - - pub const bcrpyt_default = 10; - - pub const default = Algorithm.Value{ - .argon2id = .{}, - }; - - pub fn fromJS(globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) bun.JSError!Value { - if (value.isObject()) { - if (try value.getTruthy(globalObject, "algorithm")) |algorithm_value| { - if (!algorithm_value.isString()) { - return globalObject.throwInvalidArgumentType("hash", "algorithm", "string"); - } - - const algorithm_string = try algorithm_value.getZigString(globalObject); - - switch (PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse { - return globalObject.throwInvalidArgumentType("hash", "algorithm", unknown_password_algorithm_message); - }) { - .bcrypt => { - var algorithm = PasswordObject.Algorithm.Value{ - .bcrypt = PasswordObject.Algorithm.Value.bcrpyt_default, - }; - - if (try value.getTruthy(globalObject, "cost")) |rounds_value| { - if (!rounds_value.isNumber()) { - return globalObject.throwInvalidArgumentType("hash", "cost", "number"); - } - - const rounds = rounds_value.coerce(i32, globalObject); - - if (rounds < 4 or rounds > 31) { - return globalObject.throwInvalidArguments("Rounds must be between 4 and 31", .{}); - } - - algorithm.bcrypt = @as(u6, @intCast(rounds)); - } - - return algorithm; - }, - inline .argon2id, .argon2d, .argon2i => |tag| { - var argon = Algorithm.Argon2Params{}; - - if (try value.getTruthy(globalObject, "timeCost")) |time_value| { - if (!time_value.isNumber()) { - return globalObject.throwInvalidArgumentType("hash", "timeCost", "number"); - } - - const time_cost = time_value.coerce(i32, globalObject); - - if (time_cost < 1) { - return globalObject.throwInvalidArguments("Time cost must be greater than 0", .{}); - } - - argon.time_cost = @as(u32, @intCast(time_cost)); - } - - if (try value.getTruthy(globalObject, "memoryCost")) |memory_value| { - if (!memory_value.isNumber()) { - return globalObject.throwInvalidArgumentType("hash", "memoryCost", "number"); - } - - const memory_cost = memory_value.coerce(i32, globalObject); - - if (memory_cost < 1) { - return globalObject.throwInvalidArguments("Memory cost must be greater than 0", .{}); - } - - argon.memory_cost = @as(u32, @intCast(memory_cost)); - } - - return @unionInit(Algorithm.Value, @tagName(tag), argon); - }, - } - - unreachable; - } else { - return globalObject.throwInvalidArgumentType("hash", "options.algorithm", "string"); - } - } else if (value.isString()) { - const algorithm_string = try value.getZigString(globalObject); - - switch (PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse { - return globalObject.throwInvalidArgumentType("hash", "algorithm", unknown_password_algorithm_message); - }) { - .bcrypt => { - return PasswordObject.Algorithm.Value{ - .bcrypt = PasswordObject.Algorithm.Value.bcrpyt_default, - }; - }, - .argon2id => { - return PasswordObject.Algorithm.Value{ - .argon2id = .{}, - }; - }, - .argon2d => { - return PasswordObject.Algorithm.Value{ - .argon2d = .{}, - }; - }, - .argon2i => { - return PasswordObject.Algorithm.Value{ - .argon2i = .{}, - }; - }, - } - } else { - return globalObject.throwInvalidArgumentType("hash", "algorithm", "string"); - } - - unreachable; - } - }; - - pub const Argon2Params = struct { - // we don't support the other options right now, but can add them later if someone asks - memory_cost: u32 = pwhash.argon2.Params.interactive_2id.m, - time_cost: u32 = pwhash.argon2.Params.interactive_2id.t, - - pub fn toParams(this: Argon2Params) pwhash.argon2.Params { - return pwhash.argon2.Params{ - .t = this.time_cost, - .m = this.memory_cost, - .p = 1, - }; - } - }; - - pub const argon2 = Algorithm.argon2id; - - pub const label = bun.ComptimeStringMap( - Algorithm, - .{ - .{ "argon2i", .argon2i }, - .{ "argon2d", .argon2d }, - .{ "argon2id", .argon2id }, - .{ "bcrypt", .bcrypt }, - }, - ); - - pub const default = Algorithm.argon2; - - pub fn get(pw: []const u8) ?Algorithm { - if (pw[0] != '$') { - return null; - } - - // PHC format looks like $$$$ - if (strings.hasPrefixComptime(pw[1..], "argon2d$")) { - return .argon2d; - } - if (strings.hasPrefixComptime(pw[1..], "argon2i$")) { - return .argon2i; - } - if (strings.hasPrefixComptime(pw[1..], "argon2id$")) { - return .argon2id; - } - - if (strings.hasPrefixComptime(pw[1..], "bcrypt")) { - return .bcrypt; - } - - // https://en.wikipedia.org/wiki/Crypt_(C) - if (strings.hasPrefixComptime(pw[1..], "2")) { - return .bcrypt; - } - - return null; - } - }; - - pub const HashError = pwhash.Error || error{UnsupportedAlgorithm}; - - // This is purposely simple because nobody asked to make it more complicated - pub fn hash( - allocator: std.mem.Allocator, - password: []const u8, - algorithm: Algorithm.Value, - ) HashError![]const u8 { - switch (algorithm) { - inline .argon2i, .argon2d, .argon2id => |argon| { - var outbuf: [4096]u8 = undefined; - const hash_options = pwhash.argon2.HashOptions{ - .params = argon.toParams(), - .allocator = allocator, - .mode = switch (algorithm) { - .argon2i => .argon2i, - .argon2d => .argon2d, - .argon2id => .argon2id, - else => unreachable, - }, - .encoding = .phc, - }; - // warning: argon2's code may spin up threads if paralellism is set to > 0 - // we don't expose this option - // but since it parses from phc format, it's possible that it will be set - // eventually we should do something that about that. - const out_bytes = try pwhash.argon2.strHash(password, hash_options, &outbuf); - return try allocator.dupe(u8, out_bytes); - }, - .bcrypt => |cost| { - var outbuf: [4096]u8 = undefined; - var outbuf_slice: []u8 = outbuf[0..]; - var password_to_use = password; - // bcrypt silently truncates passwords longer than 72 bytes - // we use SHA512 to hash the password if it's longer than 72 bytes - if (password.len > 72) { - var sha_512 = bun.sha.SHA512.init(); - defer sha_512.deinit(); - sha_512.update(password); - sha_512.final(outbuf[0..bun.sha.SHA512.digest]); - password_to_use = outbuf[0..bun.sha.SHA512.digest]; - outbuf_slice = outbuf[bun.sha.SHA512.digest..]; - } - - const hash_options = pwhash.bcrypt.HashOptions{ - .params = pwhash.bcrypt.Params{ - .rounds_log = cost, - .silently_truncate_password = true, - }, - .allocator = allocator, - .encoding = .crypt, - }; - const out_bytes = try pwhash.bcrypt.strHash(password_to_use, hash_options, outbuf_slice); - return try allocator.dupe(u8, out_bytes); - }, - } - } - - pub fn verify( - allocator: std.mem.Allocator, - password: []const u8, - previous_hash: []const u8, - algorithm: ?Algorithm, - ) HashError!bool { - if (previous_hash.len == 0) { - return false; - } - - return verifyWithAlgorithm( - allocator, - password, - previous_hash, - algorithm orelse Algorithm.get(previous_hash) orelse return error.UnsupportedAlgorithm, - ); - } - - pub fn verifyWithAlgorithm( - allocator: std.mem.Allocator, - password: []const u8, - previous_hash: []const u8, - algorithm: Algorithm, - ) HashError!bool { - switch (algorithm) { - .argon2id, .argon2d, .argon2i => { - pwhash.argon2.strVerify(previous_hash, password, .{ .allocator = allocator }) catch |err| { - if (err == error.PasswordVerificationFailed) { - return false; - } - - return err; - }; - return true; - }, - .bcrypt => { - var password_to_use = password; - var outbuf: [bun.sha.SHA512.digest]u8 = undefined; - - // bcrypt silently truncates passwords longer than 72 bytes - // we use SHA512 to hash the password if it's longer than 72 bytes - if (password.len > 72) { - var sha_512 = bun.sha.SHA512.init(); - defer sha_512.deinit(); - sha_512.update(password); - sha_512.final(&outbuf); - password_to_use = &outbuf; - } - pwhash.bcrypt.strVerify(previous_hash, password_to_use, .{ - .allocator = allocator, - .silently_truncate_password = true, - }) catch |err| { - if (err == error.PasswordVerificationFailed) { - return false; - } - - return err; - }; - return true; - }, - } - } - }; - - pub const JSPasswordObject = struct { - const PascalToUpperUnderscoreCaseFormatter = struct { - input: []const u8, - pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - for (self.input) |c| { - if (std.ascii.isUpper(c)) { - try writer.writeByte('_'); - try writer.writeByte(c); - } else if (std.ascii.isLower(c)) { - try writer.writeByte(std.ascii.toUpper(c)); - } else { - try writer.writeByte(c); - } - } - } - }; - - pub export fn JSPasswordObject__create(globalObject: *JSC.JSGlobalObject) JSC.JSValue { - var object = JSValue.createEmptyObject(globalObject, 4); - object.put( - globalObject, - ZigString.static("hash"), - JSC.createCallback(globalObject, ZigString.static("hash"), 2, JSPasswordObject__hash), - ); - object.put( - globalObject, - ZigString.static("hashSync"), - JSC.createCallback(globalObject, ZigString.static("hashSync"), 2, JSPasswordObject__hashSync), - ); - object.put( - globalObject, - ZigString.static("verify"), - JSC.createCallback(globalObject, ZigString.static("verify"), 2, JSPasswordObject__verify), - ); - object.put( - globalObject, - ZigString.static("verifySync"), - JSC.createCallback(globalObject, ZigString.static("verifySync"), 2, JSPasswordObject__verifySync), - ); - return object; - } - - const HashJob = struct { - algorithm: PasswordObject.Algorithm.Value, - password: []const u8, - promise: JSC.JSPromise.Strong, - event_loop: *JSC.EventLoop, - global: *JSC.JSGlobalObject, - ref: Async.KeepAlive = .{}, - task: JSC.WorkPoolTask = .{ .callback = &run }, - - pub usingnamespace bun.New(@This()); - - pub const Result = struct { - value: Value, - ref: Async.KeepAlive = .{}, - - task: JSC.AnyTask = undefined, - promise: JSC.JSPromise.Strong, - global: *JSC.JSGlobalObject, - - pub usingnamespace bun.New(@This()); - - pub const Value = union(enum) { - err: PasswordObject.HashError, - hash: []const u8, - - pub fn toErrorInstance(this: Value, globalObject: *JSC.JSGlobalObject) JSC.JSValue { - const error_code = std.fmt.allocPrint(bun.default_allocator, "PASSWORD{}", .{PascalToUpperUnderscoreCaseFormatter{ .input = @errorName(this.err) }}) catch bun.outOfMemory(); - defer bun.default_allocator.free(error_code); - const instance = globalObject.createErrorInstance("Password hashing failed with error \"{s}\"", .{@errorName(this.err)}); - instance.put(globalObject, ZigString.static("code"), JSC.ZigString.init(error_code).toJS(globalObject)); - return instance; - } - }; - - pub fn runFromJS(this: *Result) void { - var promise = this.promise; - defer promise.deinit(); - this.promise = .{}; - this.ref.unref(this.global.bunVM()); - const global = this.global; - switch (this.value) { - .err => { - const error_instance = this.value.toErrorInstance(global); - this.destroy(); - promise.reject(global, error_instance); - }, - .hash => |value| { - const js_string = JSC.ZigString.init(value).toJS(global); - this.destroy(); - promise.resolve(global, js_string); - }, - } - } - }; - - pub fn deinit(this: *HashJob) void { - this.promise.deinit(); - bun.freeSensitive(bun.default_allocator, this.password); - this.destroy(); - } - - pub fn getValue(password: []const u8, algorithm: PasswordObject.Algorithm.Value) Result.Value { - const value = PasswordObject.hash(bun.default_allocator, password, algorithm) catch |err| { - return Result.Value{ .err = err }; - }; - return Result.Value{ .hash = value }; - } - - pub fn run(task: *bun.ThreadPool.Task) void { - var this: *HashJob = @fieldParentPtr("task", task); - - var result = Result.new(.{ - .value = getValue(this.password, this.algorithm), - .task = undefined, - .promise = this.promise, - .global = this.global, - .ref = this.ref, - }); - this.promise = .empty; - - result.task = JSC.AnyTask.New(Result, Result.runFromJS).init(result); - this.ref = .{}; - this.event_loop.enqueueTaskConcurrent(JSC.ConcurrentTask.createFrom(&result.task)); - this.deinit(); - } - }; - - pub fn hash(globalObject: *JSC.JSGlobalObject, password: []const u8, algorithm: PasswordObject.Algorithm.Value, comptime sync: bool) bun.JSError!JSC.JSValue { - assert(password.len > 0); // caller must check - - if (comptime sync) { - const value = HashJob.getValue(password, algorithm); - switch (value) { - .err => { - const error_instance = value.toErrorInstance(globalObject); - return globalObject.throwValue(error_instance); - }, - .hash => |h| { - return JSC.ZigString.init(h).toJS(globalObject); - }, - } - - unreachable; - } - - const promise = JSC.JSPromise.Strong.init(globalObject); - - var job = HashJob.new(.{ - .algorithm = algorithm, - .password = password, - .promise = promise, - .event_loop = globalObject.bunVM().eventLoop(), - .global = globalObject, - }); - job.ref.ref(globalObject.bunVM()); - JSC.WorkPool.schedule(&job.task); - - return promise.value(); - } - - pub fn verify(globalObject: *JSC.JSGlobalObject, password: []const u8, prev_hash: []const u8, algorithm: ?PasswordObject.Algorithm, comptime sync: bool) bun.JSError!JSC.JSValue { - assert(password.len > 0); // caller must check - - if (comptime sync) { - const value = VerifyJob.getValue(password, prev_hash, algorithm); - switch (value) { - .err => { - const error_instance = value.toErrorInstance(globalObject); - return globalObject.throwValue(error_instance); - }, - .pass => |pass| { - return JSC.JSValue.jsBoolean(pass); - }, - } - - unreachable; - } - - var promise = JSC.JSPromise.Strong.init(globalObject); - - const job = VerifyJob.new(.{ - .algorithm = algorithm, - .password = password, - .prev_hash = prev_hash, - .promise = promise, - .event_loop = globalObject.bunVM().eventLoop(), - .global = globalObject, - }); - job.ref.ref(globalObject.bunVM()); - JSC.WorkPool.schedule(&job.task); - - return promise.value(); - } - - // Once we have bindings generator, this should be replaced with a generated function - pub fn JSPasswordObject__hash(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const arguments_ = callframe.arguments_old(2); - const arguments = arguments_.ptr[0..arguments_.len]; - - if (arguments.len < 1) { - return globalObject.throwNotEnoughArguments("hash", 1, 0); - } - - var algorithm = PasswordObject.Algorithm.Value.default; - - if (arguments.len > 1 and !arguments[1].isEmptyOrUndefinedOrNull()) { - algorithm = try PasswordObject.Algorithm.Value.fromJS(globalObject, arguments[1]); - } - - // TODO: this most likely should error like `hashSync` instead of stringifying. - // - // fromJS(...) orelse { - // return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray"); - // } - const password_to_hash = try JSC.Node.StringOrBuffer.fromJSToOwnedSlice(globalObject, arguments[0], bun.default_allocator); - errdefer bun.default_allocator.free(password_to_hash); - - if (password_to_hash.len == 0) { - return globalObject.throwInvalidArguments("password must not be empty", .{}); - } - - return hash(globalObject, password_to_hash, algorithm, false); - } - - // Once we have bindings generator, this should be replaced with a generated function - pub fn JSPasswordObject__hashSync(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const arguments_ = callframe.arguments_old(2); - const arguments = arguments_.ptr[0..arguments_.len]; - - if (arguments.len < 1) { - return globalObject.throwNotEnoughArguments("hash", 1, 0); - } - - var algorithm = PasswordObject.Algorithm.Value.default; - - if (arguments.len > 1 and !arguments[1].isEmptyOrUndefinedOrNull()) { - algorithm = try PasswordObject.Algorithm.Value.fromJS(globalObject, arguments[1]); - } - - var string_or_buffer = try JSC.Node.StringOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse { - return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray"); - }; - defer string_or_buffer.deinit(); - - if (string_or_buffer.slice().len == 0) { - return globalObject.throwInvalidArguments("password must not be empty", .{}); - } - - return hash(globalObject, string_or_buffer.slice(), algorithm, true); - } - - const VerifyJob = struct { - algorithm: ?PasswordObject.Algorithm = null, - password: []const u8, - prev_hash: []const u8, - promise: JSC.JSPromise.Strong, - event_loop: *JSC.EventLoop, - global: *JSC.JSGlobalObject, - ref: Async.KeepAlive = .{}, - task: JSC.WorkPoolTask = .{ .callback = &run }, - - pub usingnamespace bun.New(@This()); - - pub const Result = struct { - value: Value, - ref: Async.KeepAlive = .{}, - - task: JSC.AnyTask = undefined, - promise: JSC.JSPromise.Strong, - global: *JSC.JSGlobalObject, - - pub usingnamespace bun.New(@This()); - - pub const Value = union(enum) { - err: PasswordObject.HashError, - pass: bool, - - pub fn toErrorInstance(this: Value, globalObject: *JSC.JSGlobalObject) JSC.JSValue { - const error_code = std.fmt.allocPrint(bun.default_allocator, "PASSWORD{}", .{PascalToUpperUnderscoreCaseFormatter{ .input = @errorName(this.err) }}) catch bun.outOfMemory(); - defer bun.default_allocator.free(error_code); - const instance = globalObject.createErrorInstance("Password verification failed with error \"{s}\"", .{@errorName(this.err)}); - instance.put(globalObject, ZigString.static("code"), JSC.ZigString.init(error_code).toJS(globalObject)); - return instance; - } - }; - - pub fn runFromJS(this: *Result) void { - var promise = this.promise; - defer promise.deinit(); - this.promise = .{}; - this.ref.unref(this.global.bunVM()); - const global = this.global; - switch (this.value) { - .err => { - const error_instance = this.value.toErrorInstance(global); - this.destroy(); - promise.reject(global, error_instance); - }, - .pass => |pass| { - this.destroy(); - promise.resolve(global, JSC.JSValue.jsBoolean(pass)); - }, - } - } - }; - - pub fn deinit(this: *VerifyJob) void { - this.promise.deinit(); - - bun.freeSensitive(bun.default_allocator, this.password); - bun.freeSensitive(bun.default_allocator, this.prev_hash); - - this.destroy(); - } - - pub fn getValue(password: []const u8, prev_hash: []const u8, algorithm: ?PasswordObject.Algorithm) Result.Value { - const pass = PasswordObject.verify(bun.default_allocator, password, prev_hash, algorithm) catch |err| { - return Result.Value{ .err = err }; - }; - return Result.Value{ .pass = pass }; - } - - pub fn run(task: *bun.ThreadPool.Task) void { - var this: *VerifyJob = @fieldParentPtr("task", task); - - var result = Result.new(.{ - .value = getValue(this.password, this.prev_hash, this.algorithm), - .task = undefined, - .promise = this.promise, - .global = this.global, - .ref = this.ref, - }); - this.promise = .empty; - - result.task = JSC.AnyTask.New(Result, Result.runFromJS).init(result); - this.ref = .{}; - this.event_loop.enqueueTaskConcurrent(JSC.ConcurrentTask.createFrom(&result.task)); - this.deinit(); - } - }; - - // Once we have bindings generator, this should be replaced with a generated function - pub fn JSPasswordObject__verify(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const arguments_ = callframe.arguments_old(3); - const arguments = arguments_.ptr[0..arguments_.len]; - - if (arguments.len < 2) { - return globalObject.throwNotEnoughArguments("verify", 2, 0); - } - - var algorithm: ?PasswordObject.Algorithm = null; - - if (arguments.len > 2 and !arguments[2].isEmptyOrUndefinedOrNull()) { - if (!arguments[2].isString()) { - return globalObject.throwInvalidArgumentType("verify", "algorithm", "string"); - } - - const algorithm_string = try arguments[2].getZigString(globalObject); - - algorithm = PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse { - if (!globalObject.hasException()) { - return globalObject.throwInvalidArgumentType("verify", "algorithm", unknown_password_algorithm_message); - } - return error.JSError; - }; - } - - // TODO: this most likely should error like `verifySync` instead of stringifying. - // - // fromJS(...) orelse { - // return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray"); - // } - const owned_password = try JSC.Node.StringOrBuffer.fromJSToOwnedSlice(globalObject, arguments[0], bun.default_allocator); - - // TODO: this most likely should error like `verifySync` instead of stringifying. - // - // fromJS(...) orelse { - // return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray"); - // } - const owned_hash = JSC.Node.StringOrBuffer.fromJSToOwnedSlice(globalObject, arguments[1], bun.default_allocator) catch |err| { - bun.default_allocator.free(owned_password); - return err; - }; - - if (owned_hash.len == 0) { - bun.default_allocator.free(owned_password); - return JSC.JSPromise.resolvedPromiseValue(globalObject, JSC.JSValue.jsBoolean(false)); - } - - if (owned_password.len == 0) { - bun.default_allocator.free(owned_hash); - return JSC.JSPromise.resolvedPromiseValue(globalObject, JSC.JSValue.jsBoolean(false)); - } - - return verify(globalObject, owned_password, owned_hash, algorithm, false); - } - - // Once we have bindings generator, this should be replaced with a generated function - pub fn JSPasswordObject__verifySync(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const arguments_ = callframe.arguments_old(3); - const arguments = arguments_.ptr[0..arguments_.len]; - - if (arguments.len < 2) { - return globalObject.throwNotEnoughArguments("verify", 2, 0); - } - - var algorithm: ?PasswordObject.Algorithm = null; - - if (arguments.len > 2 and !arguments[2].isEmptyOrUndefinedOrNull()) { - if (!arguments[2].isString()) { - return globalObject.throwInvalidArgumentType("verify", "algorithm", "string"); - } - - const algorithm_string = try arguments[2].getZigString(globalObject); - - algorithm = PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse { - if (!globalObject.hasException()) { - return globalObject.throwInvalidArgumentType("verify", "algorithm", unknown_password_algorithm_message); - } - return .zero; - }; - } - - var password = try JSC.Node.StringOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse { - return globalObject.throwInvalidArgumentType("verify", "password", "string or TypedArray"); - }; - - var hash_ = try JSC.Node.StringOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[1]) orelse { - password.deinit(); - return globalObject.throwInvalidArgumentType("verify", "hash", "string or TypedArray"); - }; - - defer password.deinit(); - defer hash_.deinit(); - - if (hash_.slice().len == 0) { - return JSC.JSValue.jsBoolean(false); - } - - if (password.slice().len == 0) { - return JSC.JSValue.jsBoolean(false); - } - - return verify(globalObject, password.slice(), hash_.slice(), algorithm, true); - } - }; - - pub const CryptoHasher = union(enum) { - // HMAC_CTX contains 3 EVP_CTX, so let's store it as a pointer. - hmac: ?*HMAC, - - evp: EVP, - zig: CryptoHasherZig, - - const Digest = EVP.Digest; - - pub usingnamespace JSC.Codegen.JSCryptoHasher; - usingnamespace bun.New(@This()); - - // For using only CryptoHasherZig in c++ - pub const Extern = struct { - fn getByName(global: *JSGlobalObject, name_bytes: [*:0]const u8, name_len: usize) callconv(.C) ?*CryptoHasher { - const name = name_bytes[0..name_len]; - - if (CryptoHasherZig.init(name)) |inner| { - return CryptoHasher.new(.{ - .zig = inner, - }); - } - - const algorithm = EVP.Algorithm.map.get(name) orelse { - return null; - }; - - switch (algorithm) { - .ripemd160, - .blake2b256, - .blake2b512, - - .@"sha512-224", - => { - if (algorithm.md()) |md| { - return CryptoHasher.new(.{ - .evp = EVP.init(algorithm, md, global.bunVM().rareData().boringEngine()), - }); - } - }, - else => { - return null; - }, - } - - return null; - } - - fn getFromOther(global: *JSGlobalObject, other_handle: *CryptoHasher) callconv(.C) ?*CryptoHasher { - switch (other_handle.*) { - .zig => |other| { - const hasher = CryptoHasher.new(.{ - .zig = other.copy(), - }); - return hasher; - }, - .evp => |other| { - return CryptoHasher.new(.{ - .evp = other.copy(global.bunVM().rareData().boringEngine()) catch { - return null; - }, - }); - }, - else => { - return null; - }, - } - } - - fn destroy(handle: *CryptoHasher) callconv(.C) void { - handle.finalize(); - } - - fn update(handle: *CryptoHasher, input_bytes: [*]const u8, input_len: usize) callconv(.C) bool { - const input = input_bytes[0..input_len]; - - switch (handle.*) { - .zig => { - handle.zig.update(input); - return true; - }, - .evp => { - handle.evp.update(input); - return true; - }, - else => { - return false; - }, - } - } - - fn digest(handle: *CryptoHasher, global: *JSGlobalObject, buf: [*]u8, buf_len: usize) callconv(.C) u32 { - const digest_buf = buf[0..buf_len]; - switch (handle.*) { - .zig => { - const res = handle.zig.finalWithLen(digest_buf, buf_len); - return @intCast(res.len); - }, - .evp => { - const res = handle.evp.final(global.bunVM().rareData().boringEngine(), digest_buf); - return @intCast(res.len); - }, - else => { - return 0; - }, - } - } - - fn getDigestSize(handle: *CryptoHasher) callconv(.C) u32 { - return switch (handle.*) { - .zig => |inner| inner.digest_length, - .evp => |inner| inner.size(), - else => 0, - }; - } - }; - - pub const digest = JSC.wrapInstanceMethod(CryptoHasher, "digest_", false); - pub const hash = JSC.wrapStaticMethod(CryptoHasher, "hash_", false); - - fn throwHmacConsumed(globalThis: *JSC.JSGlobalObject) bun.JSError { - return globalThis.throw("HMAC has been consumed and is no longer usable", .{}); - } - - pub fn getByteLength(this: *CryptoHasher, globalThis: *JSC.JSGlobalObject) JSC.JSValue { - return JSC.JSValue.jsNumber(switch (this.*) { - .evp => |*inner| inner.size(), - .hmac => |inner| if (inner) |hmac| hmac.size() else { - throwHmacConsumed(globalThis) catch return .zero; - }, - .zig => |*inner| inner.digest_length, - }); - } - - pub fn getAlgorithm(this: *CryptoHasher, globalObject: *JSC.JSGlobalObject) JSC.JSValue { - return switch (this.*) { - inline .evp, .zig => |*inner| ZigString.fromUTF8(bun.asByteSlice(@tagName(inner.algorithm))).toJS(globalObject), - .hmac => |inner| if (inner) |hmac| ZigString.fromUTF8(bun.asByteSlice(@tagName(hmac.algorithm))).toJS(globalObject) else { - throwHmacConsumed(globalObject) catch return .zero; - }, - }; - } - - pub fn getAlgorithms( - globalThis_: *JSC.JSGlobalObject, - _: JSValue, - _: JSValue, - ) JSC.JSValue { - return bun.String.toJSArray(globalThis_, &EVP.Algorithm.names.values); - } - - fn hashToEncoding(globalThis: *JSGlobalObject, evp: *EVP, input: JSC.Node.BlobOrStringOrBuffer, encoding: JSC.Node.Encoding) bun.JSError!JSC.JSValue { - var output_digest_buf: Digest = undefined; - defer input.deinit(); - - if (input == .blob and input.blob.isBunFile()) { - return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{}); - } - - const len = evp.hash(globalThis.bunVM().rareData().boringEngine(), input.slice(), &output_digest_buf) orelse { - const err = BoringSSL.ERR_get_error(); - const instance = createCryptoError(globalThis, err); - BoringSSL.ERR_clear_error(); - return globalThis.throwValue(instance); - }; - return encoding.encodeWithMaxSize(globalThis, BoringSSL.EVP_MAX_MD_SIZE, output_digest_buf[0..len]); - } - - fn hashToBytes(globalThis: *JSGlobalObject, evp: *EVP, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue { - var output_digest_buf: Digest = undefined; - var output_digest_slice: []u8 = &output_digest_buf; - defer input.deinit(); - - if (input == .blob and input.blob.isBunFile()) { - return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{}); - } - - if (output) |output_buf| { - const size = evp.size(); - var bytes = output_buf.byteSlice(); - if (bytes.len < size) { - return globalThis.throwInvalidArguments("TypedArray must be at least {d} bytes", .{size}); - } - output_digest_slice = bytes[0..size]; - } - - const len = evp.hash(globalThis.bunVM().rareData().boringEngine(), input.slice(), output_digest_slice) orelse { - const err = BoringSSL.ERR_get_error(); - const instance = createCryptoError(globalThis, err); - BoringSSL.ERR_clear_error(); - return globalThis.throwValue(instance); - }; - - if (output) |output_buf| { - return output_buf.value; - } else { - // Clone to GC-managed memory - return JSC.ArrayBuffer.createBuffer(globalThis, output_digest_slice[0..len]); - } - } - - pub fn hash_( - globalThis: *JSGlobalObject, - algorithm: ZigString, - input: JSC.Node.BlobOrStringOrBuffer, - output: ?JSC.Node.StringOrBuffer, - ) bun.JSError!JSC.JSValue { - var evp = EVP.byName(algorithm, globalThis) orelse return try CryptoHasherZig.hashByName(globalThis, algorithm, input, output) orelse { - return globalThis.throwInvalidArguments("Unsupported algorithm \"{any}\"", .{algorithm}); - }; - defer evp.deinit(); - - if (output) |string_or_buffer| { - switch (string_or_buffer) { - inline else => |*str| { - defer str.deinit(); - const encoding = JSC.Node.Encoding.from(str.slice()) orelse { - return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw(); - }; - - return hashToEncoding(globalThis, &evp, input, encoding); - }, - .buffer => |buffer| { - return hashToBytes(globalThis, &evp, input, buffer.buffer); - }, - } - } else { - return hashToBytes(globalThis, &evp, input, null); - } - } - - // Bun.CryptoHasher(algorithm, hmacKey?: string | Buffer) - pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!*CryptoHasher { - const arguments = callframe.arguments_old(2); - if (arguments.len == 0) { - return globalThis.throwInvalidArguments("Expected an algorithm name as an argument", .{}); - } - - const algorithm_name = arguments.ptr[0]; - if (algorithm_name.isEmptyOrUndefinedOrNull() or !algorithm_name.isString()) { - return globalThis.throwInvalidArguments("algorithm must be a string", .{}); - } - - const algorithm = try algorithm_name.getZigString(globalThis); - - if (algorithm.len == 0) { - return globalThis.throwInvalidArguments("Invalid algorithm name", .{}); - } - - const hmac_value = arguments.ptr[1]; - var hmac_key: ?JSC.Node.StringOrBuffer = null; - defer { - if (hmac_key) |*key| { - key.deinit(); - } - } - - if (!hmac_value.isEmptyOrUndefinedOrNull()) { - hmac_key = try JSC.Node.StringOrBuffer.fromJS(globalThis, bun.default_allocator, hmac_value) orelse { - return globalThis.throwInvalidArguments("key must be a string or buffer", .{}); - }; - } - - return CryptoHasher.new(brk: { - if (hmac_key) |*key| { - const chosen_algorithm = try algorithm_name.toEnumFromMap(globalThis, "algorithm", EVP.Algorithm, EVP.Algorithm.map); - if (chosen_algorithm == .ripemd160) { - // crashes at runtime. - return globalThis.throw("ripemd160 is not supported", .{}); - } - - break :brk .{ - .hmac = HMAC.init(chosen_algorithm, key.slice()) orelse { - if (!globalThis.hasException()) { - const err = BoringSSL.ERR_get_error(); - if (err != 0) { - const instance = createCryptoError(globalThis, err); - BoringSSL.ERR_clear_error(); - return globalThis.throwValue(instance); - } else { - return globalThis.throwTODO("HMAC is not supported for this algorithm yet"); - } - } - return error.JSError; - }, - }; - } - - break :brk .{ - .evp = EVP.byName(algorithm, globalThis) orelse return CryptoHasherZig.constructor(algorithm) orelse { - return globalThis.throwInvalidArguments("Unsupported algorithm {any}", .{algorithm}); - }, - }; - }); - } - - pub fn getter( - globalObject: *JSC.JSGlobalObject, - _: *JSC.JSObject, - ) JSC.JSValue { - return CryptoHasher.getConstructor(globalObject); - } - - pub fn update(this: *CryptoHasher, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const thisValue = callframe.this(); - const arguments = callframe.arguments_old(2); - const input = arguments.ptr[0]; - if (input.isEmptyOrUndefinedOrNull()) { - return globalThis.throwInvalidArguments("expected blob, string or buffer", .{}); - } - const encoding = arguments.ptr[1]; - const buffer = try JSC.Node.BlobOrStringOrBuffer.fromJSWithEncodingValue(globalThis, globalThis.bunVM().allocator, input, encoding) orelse { - if (!globalThis.hasException()) return globalThis.throwInvalidArguments("expected blob, string or buffer", .{}); - return error.JSError; - }; - defer buffer.deinit(); - if (buffer == .blob and buffer.blob.isBunFile()) { - return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{}); - } - - switch (this.*) { - .evp => |*inner| { - inner.update(buffer.slice()); - const err = BoringSSL.ERR_get_error(); - if (err != 0) { - const instance = createCryptoError(globalThis, err); - BoringSSL.ERR_clear_error(); - return globalThis.throwValue(instance); - } - }, - .hmac => |inner| { - const hmac = inner orelse { - return throwHmacConsumed(globalThis); - }; - - hmac.update(buffer.slice()); - const err = BoringSSL.ERR_get_error(); - if (err != 0) { - const instance = createCryptoError(globalThis, err); - BoringSSL.ERR_clear_error(); - return globalThis.throwValue(instance); - } - }, - .zig => |*inner| { - inner.update(buffer.slice()); - return thisValue; - }, - } - - return thisValue; - } - - pub fn copy( - this: *CryptoHasher, - globalObject: *JSC.JSGlobalObject, - _: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - var new: CryptoHasher = undefined; - switch (this.*) { - .evp => |*inner| { - new = .{ .evp = inner.copy(globalObject.bunVM().rareData().boringEngine()) catch bun.outOfMemory() }; - }, - .hmac => |inner| { - const hmac = inner orelse { - return throwHmacConsumed(globalObject); - }; - new = .{ - .hmac = hmac.copy() catch { - const err = createCryptoError(globalObject, BoringSSL.ERR_get_error()); - BoringSSL.ERR_clear_error(); - return globalObject.throwValue(err); - }, - }; - }, - .zig => |*inner| { - new = .{ .zig = inner.copy() }; - }, - } - return CryptoHasher.new(new).toJS(globalObject); - } - - pub fn digest_(this: *CryptoHasher, globalThis: *JSGlobalObject, output: ?JSC.Node.StringOrBuffer) bun.JSError!JSC.JSValue { - if (output) |string_or_buffer| { - switch (string_or_buffer) { - inline else => |*str| { - defer str.deinit(); - const encoding = JSC.Node.Encoding.from(str.slice()) orelse { - return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw(); - }; - - return this.digestToEncoding(globalThis, encoding); - }, - .buffer => |buffer| { - return this.digestToBytes( - globalThis, - buffer.buffer, - ); - }, - } - } else { - return this.digestToBytes(globalThis, null); - } - } - - fn digestToBytes(this: *CryptoHasher, globalThis: *JSGlobalObject, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue { - var output_digest_buf: EVP.Digest = undefined; - var output_digest_slice: []u8 = &output_digest_buf; - if (output) |output_buf| { - var bytes = output_buf.byteSlice(); - if (bytes.len < output_digest_buf.len) { - return globalThis.throwInvalidArguments(comptime std.fmt.comptimePrint("TypedArray must be at least {d} bytes", .{output_digest_buf.len}), .{}); - } - output_digest_slice = bytes[0..bytes.len]; - } else { - output_digest_buf = std.mem.zeroes(EVP.Digest); - } - - const result = this.final(globalThis, output_digest_slice) catch return .zero; - if (globalThis.hasException()) { - return error.JSError; - } - - if (output) |output_buf| { - return output_buf.value; - } else { - // Clone to GC-managed memory - return JSC.ArrayBuffer.createBuffer(globalThis, result); - } - } - - fn digestToEncoding(this: *CryptoHasher, globalThis: *JSGlobalObject, encoding: JSC.Node.Encoding) bun.JSError!JSC.JSValue { - var output_digest_buf: EVP.Digest = std.mem.zeroes(EVP.Digest); - const output_digest_slice: []u8 = &output_digest_buf; - const out = this.final(globalThis, output_digest_slice) catch return .zero; - if (globalThis.hasException()) { - return error.JSError; - } - return encoding.encodeWithMaxSize(globalThis, BoringSSL.EVP_MAX_MD_SIZE, out); - } - - fn final(this: *CryptoHasher, globalThis: *JSGlobalObject, output_digest_slice: []u8) bun.JSError![]u8 { - return switch (this.*) { - .hmac => |inner| brk: { - const hmac: *HMAC = inner orelse { - return throwHmacConsumed(globalThis); - }; - this.hmac = null; - defer hmac.deinit(); - break :brk hmac.final(output_digest_slice); - }, - .evp => |*inner| inner.final(globalThis.bunVM().rareData().boringEngine(), output_digest_slice), - .zig => |*inner| inner.final(output_digest_slice), - }; - } - - pub fn finalize(this: *CryptoHasher) void { - switch (this.*) { - .evp => |*inner| { - // https://github.com/oven-sh/bun/issues/3250 - inner.deinit(); - }, - .zig => |*inner| { - inner.deinit(); - }, - .hmac => |inner| { - if (inner) |hmac| { - hmac.deinit(); - } - }, - } - this.destroy(); - } - }; - - const CryptoHasherZig = struct { - algorithm: EVP.Algorithm, - state: *anyopaque, - digest_length: u8, - - const algo_map = [_]struct { string, type }{ - .{ "sha3-224", std.crypto.hash.sha3.Sha3_224 }, - .{ "sha3-256", std.crypto.hash.sha3.Sha3_256 }, - .{ "sha3-384", std.crypto.hash.sha3.Sha3_384 }, - .{ "sha3-512", std.crypto.hash.sha3.Sha3_512 }, - .{ "shake128", std.crypto.hash.sha3.Shake128 }, - .{ "shake256", std.crypto.hash.sha3.Shake256 }, - }; - - inline fn digestLength(Algorithm: type) comptime_int { - return switch (Algorithm) { - std.crypto.hash.sha3.Shake128 => 16, - std.crypto.hash.sha3.Shake256 => 32, - else => Algorithm.digest_length, - }; - } - - pub fn hashByName(globalThis: *JSGlobalObject, algorithm: ZigString, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.Node.StringOrBuffer) bun.JSError!?JSC.JSValue { - inline for (algo_map) |item| { - if (bun.strings.eqlComptime(algorithm.slice(), item[0])) { - return try hashByNameInner(globalThis, item[1], input, output); - } - } - return null; - } - - fn hashByNameInner(globalThis: *JSGlobalObject, comptime Algorithm: type, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.Node.StringOrBuffer) bun.JSError!JSC.JSValue { - if (output) |string_or_buffer| { - switch (string_or_buffer) { - inline else => |*str| { - defer str.deinit(); - const encoding = JSC.Node.Encoding.from(str.slice()) orelse { - return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw(); - }; - - if (encoding == .buffer) { - return hashByNameInnerToBytes(globalThis, Algorithm, input, null); - } - - return hashByNameInnerToString(globalThis, Algorithm, input, encoding); - }, - .buffer => |buffer| { - return hashByNameInnerToBytes(globalThis, Algorithm, input, buffer.buffer); - }, - } - } - return hashByNameInnerToBytes(globalThis, Algorithm, input, null); - } - - fn hashByNameInnerToString(globalThis: *JSGlobalObject, comptime Algorithm: type, input: JSC.Node.BlobOrStringOrBuffer, encoding: JSC.Node.Encoding) bun.JSError!JSC.JSValue { - defer input.deinit(); - - if (input == .blob and input.blob.isBunFile()) { - return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{}); - } - - var h = Algorithm.init(.{}); - h.update(input.slice()); - - var out: [digestLength(Algorithm)]u8 = undefined; - h.final(&out); - - return encoding.encodeWithSize(globalThis, digestLength(Algorithm), &out); - } - - fn hashByNameInnerToBytes(globalThis: *JSGlobalObject, comptime Algorithm: type, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue { - defer input.deinit(); - - if (input == .blob and input.blob.isBunFile()) { - return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{}); - } - - var h = Algorithm.init(.{}); - const digest_length_comptime = digestLength(Algorithm); - - if (output) |output_buf| { - if (output_buf.byteSlice().len < digest_length_comptime) { - return globalThis.throwInvalidArguments("TypedArray must be at least {d} bytes", .{digest_length_comptime}); - } - } - - h.update(input.slice()); - - if (output) |output_buf| { - h.final(output_buf.slice()[0..digest_length_comptime]); - return output_buf.value; - } else { - var out: [digestLength(Algorithm)]u8 = undefined; - h.final(&out); - // Clone to GC-managed memory - return JSC.ArrayBuffer.createBuffer(globalThis, &out); - } - } - - fn constructor(algorithm: ZigString) ?*CryptoHasher { - inline for (algo_map) |item| { - if (bun.strings.eqlComptime(algorithm.slice(), item[0])) { - return CryptoHasher.new(.{ .zig = .{ - .algorithm = @field(EVP.Algorithm, item[0]), - .state = bun.new(item[1], item[1].init(.{})), - .digest_length = digestLength(item[1]), - } }); - } - } - return null; - } - - pub fn init(algorithm: []const u8) ?CryptoHasherZig { - inline for (algo_map) |item| { - const name, const T = item; - if (bun.strings.eqlComptime(algorithm, name)) { - const handle: CryptoHasherZig = .{ - .algorithm = @field(EVP.Algorithm, name), - .state = bun.new(T, T.init(.{})), - .digest_length = digestLength(T), - }; - - return handle; - } - } - return null; - } - - fn update(self: *CryptoHasherZig, bytes: []const u8) void { - inline for (algo_map) |item| { - if (self.algorithm == @field(EVP.Algorithm, item[0])) { - return item[1].update(@ptrCast(@alignCast(self.state)), bytes); - } - } - @panic("unreachable"); - } - - fn copy(self: *const CryptoHasherZig) CryptoHasherZig { - inline for (algo_map) |item| { - if (self.algorithm == @field(EVP.Algorithm, item[0])) { - return .{ - .algorithm = self.algorithm, - .state = bun.dupe(item[1], @ptrCast(@alignCast(self.state))), - .digest_length = self.digest_length, - }; - } - } - @panic("unreachable"); - } - - fn finalWithLen(self: *CryptoHasherZig, output_digest_slice: []u8, res_len: usize) []u8 { - inline for (algo_map) |pair| { - const name, const T = pair; - if (self.algorithm == @field(EVP.Algorithm, name)) { - T.final(@ptrCast(@alignCast(self.state)), @ptrCast(output_digest_slice)); - const reset: *T = @ptrCast(@alignCast(self.state)); - reset.* = T.init(.{}); - return output_digest_slice[0..res_len]; - } - } - @panic("unreachable"); - } - - fn final(self: *CryptoHasherZig, output_digest_slice: []u8) []u8 { - return self.finalWithLen(output_digest_slice, self.digest_length); - } - - fn deinit(self: *CryptoHasherZig) void { - inline for (algo_map) |item| { - if (self.algorithm == @field(EVP.Algorithm, item[0])) { - return bun.destroy(@as(*item[1], @ptrCast(@alignCast(self.state)))); - } - } - @panic("unreachable"); - } - }; - - fn StaticCryptoHasher(comptime Hasher: type, comptime name: [:0]const u8) type { - return struct { - hashing: Hasher = Hasher{}, - digested: bool = false, - - const ThisHasher = @This(); - - pub usingnamespace @field(JSC.Codegen, "JS" ++ name); - - pub const digest = JSC.wrapInstanceMethod(ThisHasher, "digest_", false); - pub const hash = JSC.wrapStaticMethod(ThisHasher, "hash_", false); - - pub fn getByteLength( - _: *@This(), - _: *JSC.JSGlobalObject, - ) JSC.JSValue { - return JSC.JSValue.jsNumber(@as(u16, Hasher.digest)); - } - - pub fn getByteLengthStatic( - _: *JSC.JSGlobalObject, - _: JSValue, - _: JSValue, - ) JSC.JSValue { - return JSC.JSValue.jsNumber(@as(u16, Hasher.digest)); - } - - fn hashToEncoding(globalThis: *JSGlobalObject, input: JSC.Node.BlobOrStringOrBuffer, encoding: JSC.Node.Encoding) bun.JSError!JSC.JSValue { - var output_digest_buf: Hasher.Digest = undefined; - - if (input == .blob and input.blob.isBunFile()) { - return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{}); - } - - if (comptime @typeInfo(@TypeOf(Hasher.hash)).@"fn".params.len == 3) { - Hasher.hash(input.slice(), &output_digest_buf, JSC.VirtualMachine.get().rareData().boringEngine()); - } else { - Hasher.hash(input.slice(), &output_digest_buf); - } - - return encoding.encodeWithSize(globalThis, Hasher.digest, &output_digest_buf); - } - - fn hashToBytes(globalThis: *JSGlobalObject, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue { - var output_digest_buf: Hasher.Digest = undefined; - var output_digest_slice: *Hasher.Digest = &output_digest_buf; - if (output) |output_buf| { - var bytes = output_buf.byteSlice(); - if (bytes.len < Hasher.digest) { - return globalThis.throwInvalidArguments(comptime std.fmt.comptimePrint("TypedArray must be at least {d} bytes", .{Hasher.digest}), .{}); - } - output_digest_slice = bytes[0..Hasher.digest]; - } - - if (comptime @typeInfo(@TypeOf(Hasher.hash)).@"fn".params.len == 3) { - Hasher.hash(input.slice(), output_digest_slice, JSC.VirtualMachine.get().rareData().boringEngine()); - } else { - Hasher.hash(input.slice(), output_digest_slice); - } - - if (output) |output_buf| { - return output_buf.value; - } else { - var array_buffer_out = JSC.ArrayBuffer.fromBytes(bun.default_allocator.dupe(u8, output_digest_slice) catch unreachable, .Uint8Array); - return array_buffer_out.toJSUnchecked(globalThis, null); - } - } - - pub fn hash_( - globalThis: *JSGlobalObject, - input: JSC.Node.BlobOrStringOrBuffer, - output: ?JSC.Node.StringOrBuffer, - ) bun.JSError!JSC.JSValue { - defer input.deinit(); - - if (input == .blob and input.blob.isBunFile()) { - return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{}); - } - - if (output) |string_or_buffer| { - switch (string_or_buffer) { - inline else => |*str| { - defer str.deinit(); - const encoding = JSC.Node.Encoding.from(str.slice()) orelse { - return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw(); - }; - - return hashToEncoding(globalThis, input, encoding); - }, - .buffer => |buffer| { - return hashToBytes(globalThis, input, buffer.buffer); - }, - } - } else { - return hashToBytes(globalThis, input, null); - } - } - - pub fn constructor(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!*@This() { - const this = try bun.default_allocator.create(@This()); - this.* = .{ .hashing = Hasher.init() }; - return this; - } - - pub fn getter( - globalObject: *JSC.JSGlobalObject, - _: *JSC.JSObject, - ) JSC.JSValue { - return ThisHasher.getConstructor(globalObject); - } - - pub fn update(this: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - if (this.digested) { - return globalThis.ERR_INVALID_STATE(name ++ " hasher already digested, create a new instance to update", .{}).throw(); - } - const thisValue = callframe.this(); - const input = callframe.argument(0); - const buffer = try JSC.Node.BlobOrStringOrBuffer.fromJS(globalThis, globalThis.bunVM().allocator, input) orelse { - return globalThis.throwInvalidArguments("expected blob or string or buffer", .{}); - }; - defer buffer.deinit(); - - if (buffer == .blob and buffer.blob.isBunFile()) { - return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{}); - } - this.hashing.update(buffer.slice()); - return thisValue; - } - - pub fn digest_( - this: *@This(), - globalThis: *JSGlobalObject, - output: ?JSC.Node.StringOrBuffer, - ) bun.JSError!JSC.JSValue { - if (this.digested) { - return globalThis.ERR_INVALID_STATE(name ++ " hasher already digested, create a new instance to digest again", .{}).throw(); - } - if (output) |*string_or_buffer| { - switch (string_or_buffer.*) { - inline else => |*str| { - defer str.deinit(); - const encoding = JSC.Node.Encoding.from(str.slice()) orelse { - return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw(); - }; - - return this.digestToEncoding(globalThis, encoding); - }, - .buffer => |*buffer| { - return this.digestToBytes( - globalThis, - buffer.buffer, - ); - }, - } - } else { - return this.digestToBytes(globalThis, null); - } - } - - fn digestToBytes(this: *@This(), globalThis: *JSGlobalObject, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue { - var output_digest_buf: Hasher.Digest = undefined; - var output_digest_slice: *Hasher.Digest = &output_digest_buf; - if (output) |output_buf| { - var bytes = output_buf.byteSlice(); - if (bytes.len < Hasher.digest) { - return globalThis.throwInvalidArguments(comptime std.fmt.comptimePrint("TypedArray must be at least {d} bytes", .{Hasher.digest}), .{}); - } - output_digest_slice = bytes[0..Hasher.digest]; - } else { - output_digest_buf = std.mem.zeroes(Hasher.Digest); - } - - this.hashing.final(output_digest_slice); - this.digested = true; - - if (output) |output_buf| { - return output_buf.value; - } else { - var array_buffer_out = JSC.ArrayBuffer.fromBytes(bun.default_allocator.dupe(u8, &output_digest_buf) catch unreachable, .Uint8Array); - return array_buffer_out.toJSUnchecked(globalThis, null); - } - } - - fn digestToEncoding(this: *@This(), globalThis: *JSGlobalObject, encoding: JSC.Node.Encoding) JSC.JSValue { - var output_digest_buf: Hasher.Digest = comptime brk: { - var bytes: Hasher.Digest = undefined; - var i: usize = 0; - while (i < Hasher.digest) { - bytes[i] = 0; - i += 1; - } - break :brk bytes; - }; - - const output_digest_slice: *Hasher.Digest = &output_digest_buf; - - this.hashing.final(output_digest_slice); - this.digested = true; - - return encoding.encodeWithSize(globalThis, Hasher.digest, output_digest_slice); - } - - pub fn finalize(this: *@This()) void { - VirtualMachine.get().allocator.destroy(this); - } - }; - } - - pub const MD4 = StaticCryptoHasher(Hashers.MD4, "MD4"); - pub const MD5 = StaticCryptoHasher(Hashers.MD5, "MD5"); - pub const SHA1 = StaticCryptoHasher(Hashers.SHA1, "SHA1"); - pub const SHA224 = StaticCryptoHasher(Hashers.SHA224, "SHA224"); - pub const SHA256 = StaticCryptoHasher(Hashers.SHA256, "SHA256"); - pub const SHA384 = StaticCryptoHasher(Hashers.SHA384, "SHA384"); - pub const SHA512 = StaticCryptoHasher(Hashers.SHA512, "SHA512"); - pub const SHA512_256 = StaticCryptoHasher(Hashers.SHA512_256, "SHA512_256"); -}; - -comptime { - @export(&Crypto.CryptoHasher.Extern.getByName, .{ .name = "Bun__CryptoHasherExtern__getByName" }); - @export(&Crypto.CryptoHasher.Extern.getFromOther, .{ .name = "Bun__CryptoHasherExtern__getFromOther" }); - @export(&Crypto.CryptoHasher.Extern.destroy, .{ .name = "Bun__CryptoHasherExtern__destroy" }); - @export(&Crypto.CryptoHasher.Extern.update, .{ .name = "Bun__CryptoHasherExtern__update" }); - @export(&Crypto.CryptoHasher.Extern.digest, .{ .name = "Bun__CryptoHasherExtern__digest" }); - @export(&Crypto.CryptoHasher.Extern.getDigestSize, .{ .name = "Bun__CryptoHasherExtern__getDigestSize" }); -} +pub const Crypto = @import("./crypto.zig"); pub fn nanoseconds(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { const ns = globalThis.bunVM().origin_timer.read(); @@ -3460,143 +1219,6 @@ pub fn getHashObject(globalThis: *JSC.JSGlobalObject, _: *JSC.JSObject) JSC.JSVa return HashObject.create(globalThis); } -const HashObject = struct { - pub const wyhash = hashWrap(std.hash.Wyhash); - pub const adler32 = hashWrap(std.hash.Adler32); - pub const crc32 = hashWrap(std.hash.Crc32); - pub const cityHash32 = hashWrap(std.hash.CityHash32); - pub const cityHash64 = hashWrap(std.hash.CityHash64); - pub const xxHash32 = hashWrap(struct { - pub fn hash(seed: u32, bytes: []const u8) u32 { - // sidestep .hash taking in anytype breaking ArgTuple - // downstream by forcing a type signature on the input - return std.hash.XxHash32.hash(seed, bytes); - } - }); - pub const xxHash64 = hashWrap(struct { - pub fn hash(seed: u32, bytes: []const u8) u64 { - // sidestep .hash taking in anytype breaking ArgTuple - // downstream by forcing a type signature on the input - return std.hash.XxHash64.hash(seed, bytes); - } - }); - pub const xxHash3 = hashWrap(struct { - pub fn hash(seed: u32, bytes: []const u8) u64 { - // sidestep .hash taking in anytype breaking ArgTuple - // downstream by forcing a type signature on the input - return std.hash.XxHash3.hash(seed, bytes); - } - }); - pub const murmur32v2 = hashWrap(std.hash.murmur.Murmur2_32); - pub const murmur32v3 = hashWrap(std.hash.murmur.Murmur3_32); - pub const murmur64v2 = hashWrap(std.hash.murmur.Murmur2_64); - - pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue { - const function = JSC.createCallback(globalThis, ZigString.static("hash"), 1, wyhash); - const fns = comptime .{ - "wyhash", - "adler32", - "crc32", - "cityHash32", - "cityHash64", - "xxHash32", - "xxHash64", - "xxHash3", - "murmur32v2", - "murmur32v3", - "murmur64v2", - }; - inline for (fns) |name| { - const value = JSC.createCallback( - globalThis, - ZigString.static(name), - 1, - @field(HashObject, name), - ); - function.put(globalThis, comptime ZigString.static(name), value); - } - - return function; - } - - fn hashWrap(comptime Hasher_: anytype) JSC.JSHostZigFunction { - return struct { - const Hasher = Hasher_; - pub fn hash(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const arguments = callframe.arguments_old(2).slice(); - var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); - defer args.deinit(); - - var input: []const u8 = ""; - var input_slice = ZigString.Slice.empty; - defer input_slice.deinit(); - if (args.nextEat()) |arg| { - if (arg.as(JSC.WebCore.Blob)) |blob| { - // TODO: files - input = blob.sharedView(); - } else { - switch (arg.jsTypeLoose()) { - .ArrayBuffer, - .Int8Array, - .Uint8Array, - .Uint8ClampedArray, - .Int16Array, - .Uint16Array, - .Int32Array, - .Uint32Array, - .Float16Array, - .Float32Array, - .Float64Array, - .BigInt64Array, - .BigUint64Array, - .DataView, - => { - var array_buffer = arg.asArrayBuffer(globalThis) orelse { - return globalThis.throwInvalidArguments("ArrayBuffer conversion error", .{}); - }; - input = array_buffer.byteSlice(); - }, - else => { - input_slice = try arg.toSlice(globalThis, bun.default_allocator); - input = input_slice.slice(); - }, - } - } - } - - // std.hash has inconsistent interfaces - // - const Function = if (@hasDecl(Hasher, "hashWithSeed")) Hasher.hashWithSeed else Hasher.hash; - var function_args: std.meta.ArgsTuple(@TypeOf(Function)) = undefined; - if (comptime std.meta.fields(std.meta.ArgsTuple(@TypeOf(Function))).len == 1) { - return JSC.JSValue.jsNumber(Function(input)); - } else { - var seed: u64 = 0; - if (args.nextEat()) |arg| { - if (arg.isNumber() or arg.isBigInt()) { - seed = arg.toUInt64NoTruncate(); - } - } - if (comptime bun.trait.isNumber(@TypeOf(function_args[0]))) { - function_args[0] = @as(@TypeOf(function_args[0]), @truncate(seed)); - function_args[1] = input; - } else { - function_args[0] = input; - function_args[1] = @as(@TypeOf(function_args[1]), @truncate(seed)); - } - - const value = @call(.auto, Function, function_args); - - if (@TypeOf(value) == u32) { - return JSC.JSValue.jsNumber(@as(u32, @bitCast(value))); - } - return JSC.JSValue.fromUInt64NoTruncate(globalThis, value); - } - } - }.hash; - } -}; - pub fn getTOMLObject(globalThis: *JSC.JSGlobalObject, _: *JSC.JSObject) JSC.JSValue { return TOMLObject.create(globalThis); } @@ -3651,761 +1273,14 @@ pub fn getUnsafe(globalThis: *JSC.JSGlobalObject, _: *JSC.JSObject) JSC.JSValue return UnsafeObject.create(globalThis); } -const UnsafeObject = struct { - pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue { - const object = JSValue.createEmptyObject(globalThis, 3); - const fields = comptime .{ - .gcAggressionLevel = gcAggressionLevel, - .arrayBufferToString = arrayBufferToString, - .mimallocDump = dump_mimalloc, - }; - inline for (comptime std.meta.fieldNames(@TypeOf(fields))) |name| { - object.put( - globalThis, - comptime ZigString.static(name), - JSC.createCallback(globalThis, comptime ZigString.static(name), 1, comptime @field(fields, name)), - ); - } - return object; - } - - pub fn gcAggressionLevel( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - const ret = JSValue.jsNumber(@as(i32, @intFromEnum(globalThis.bunVM().aggressive_garbage_collection))); - const value = callframe.arguments_old(1).ptr[0]; - - if (!value.isEmptyOrUndefinedOrNull()) { - switch (value.coerce(i32, globalThis)) { - 1 => globalThis.bunVM().aggressive_garbage_collection = .mild, - 2 => globalThis.bunVM().aggressive_garbage_collection = .aggressive, - 0 => globalThis.bunVM().aggressive_garbage_collection = .none, - else => {}, - } - } - return ret; - } - - pub fn arrayBufferToString( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - const args = callframe.arguments_old(2).slice(); - if (args.len < 1 or !args[0].isCell() or !args[0].jsType().isTypedArrayOrArrayBuffer()) { - return globalThis.throwInvalidArguments("Expected an ArrayBuffer", .{}); - } - - const array_buffer = JSC.ArrayBuffer.fromTypedArray(globalThis, args[0]); - switch (array_buffer.typed_array_type) { - .Uint16Array, .Int16Array => { - var zig_str = ZigString.init(""); - zig_str._unsafe_ptr_do_not_use = @as([*]const u8, @ptrCast(@alignCast(array_buffer.ptr))); - zig_str.len = array_buffer.len; - zig_str.markUTF16(); - return zig_str.toJS(globalThis); - }, - else => { - return ZigString.init(array_buffer.slice()).toJS(globalThis); - }, - } - } -}; - -const TOMLObject = struct { - const TOMLParser = @import("../../toml/toml_parser.zig").TOML; - - pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue { - const object = JSValue.createEmptyObject(globalThis, 1); - object.put( - globalThis, - ZigString.static("parse"), - JSC.createCallback( - globalThis, - ZigString.static("parse"), - 1, - parse, - ), - ); - - return object; - } - - pub fn parse( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - var arena = bun.ArenaAllocator.init(globalThis.allocator()); - const allocator = arena.allocator(); - defer arena.deinit(); - var log = logger.Log.init(default_allocator); - const arguments = callframe.arguments_old(1).slice(); - if (arguments.len == 0 or arguments[0].isEmptyOrUndefinedOrNull()) { - return globalThis.throwInvalidArguments("Expected a string to parse", .{}); - } - - var input_slice = try arguments[0].toSlice(globalThis, bun.default_allocator); - defer input_slice.deinit(); - var source = logger.Source.initPathString("input.toml", input_slice.slice()); - const parse_result = TOMLParser.parse(&source, &log, allocator, false) catch { - return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to parse toml")); - }; - - // for now... - const buffer_writer = js_printer.BufferWriter.init(allocator) catch { - return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml")); - }; - var writer = js_printer.BufferPrinter.init(buffer_writer); - _ = js_printer.printJSON( - *js_printer.BufferPrinter, - &writer, - parse_result, - &source, - .{ - .mangled_props = null, - }, - ) catch { - return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml")); - }; - - const slice = writer.ctx.buffer.slice(); - var out = bun.String.fromUTF8(slice); - defer out.deref(); - - return out.toJSByParseJSON(globalThis); - } -}; +pub const HashObject = @import("./HashObject.zig"); +pub const UnsafeObject = @import("./UnsafeObject.zig"); +pub const TOMLObject = @import("./TOMLObject.zig"); const Debugger = JSC.Debugger; pub const Timer = @import("./Timer.zig"); - -pub const FFIObject = struct { - const fields = .{ - .viewSource = JSC.wrapStaticMethod( - JSC.FFI, - "print", - false, - ), - .dlopen = JSC.wrapStaticMethod(JSC.FFI, "open", false), - .callback = JSC.wrapStaticMethod(JSC.FFI, "callback", false), - .linkSymbols = JSC.wrapStaticMethod(JSC.FFI, "linkSymbols", false), - .toBuffer = JSC.wrapStaticMethod(@This(), "toBuffer", false), - .toArrayBuffer = JSC.wrapStaticMethod(@This(), "toArrayBuffer", false), - .closeCallback = JSC.wrapStaticMethod(JSC.FFI, "closeCallback", false), - .CString = JSC.wrapStaticMethod(Bun.FFIObject, "newCString", false), - }; - - pub fn newCString(globalThis: *JSGlobalObject, value: JSValue, byteOffset: ?JSValue, lengthValue: ?JSValue) JSC.JSValue { - switch (FFIObject.getPtrSlice(globalThis, value, byteOffset, lengthValue)) { - .err => |err| { - return err; - }, - .slice => |slice| { - return bun.String.createUTF8ForJS(globalThis, slice); - }, - } - } - - pub const dom_call = JSC.DOMCall("FFI", @This(), "ptr", JSC.DOMEffect.forRead(.TypedArrayProperties)); - - pub fn toJS(globalObject: *JSC.JSGlobalObject) JSC.JSValue { - const object = JSC.JSValue.createEmptyObject(globalObject, comptime std.meta.fieldNames(@TypeOf(fields)).len + 2); - inline for (comptime std.meta.fieldNames(@TypeOf(fields))) |field| { - object.put( - globalObject, - comptime ZigString.static(field), - JSC.createCallback(globalObject, comptime ZigString.static(field), 1, comptime @field(fields, field)), - ); - } - - dom_call.put(globalObject, object); - object.put(globalObject, ZigString.static("read"), Reader.toJS(globalObject)); - - return object; - } - - pub const Reader = struct { - pub const DOMCalls = .{ - .u8 = JSC.DOMCall("Reader", @This(), "u8", JSC.DOMEffect.forRead(.World)), - .u16 = JSC.DOMCall("Reader", @This(), "u16", JSC.DOMEffect.forRead(.World)), - .u32 = JSC.DOMCall("Reader", @This(), "u32", JSC.DOMEffect.forRead(.World)), - .ptr = JSC.DOMCall("Reader", @This(), "ptr", JSC.DOMEffect.forRead(.World)), - .i8 = JSC.DOMCall("Reader", @This(), "i8", JSC.DOMEffect.forRead(.World)), - .i16 = JSC.DOMCall("Reader", @This(), "i16", JSC.DOMEffect.forRead(.World)), - .i32 = JSC.DOMCall("Reader", @This(), "i32", JSC.DOMEffect.forRead(.World)), - .i64 = JSC.DOMCall("Reader", @This(), "i64", JSC.DOMEffect.forRead(.World)), - .u64 = JSC.DOMCall("Reader", @This(), "u64", JSC.DOMEffect.forRead(.World)), - .intptr = JSC.DOMCall("Reader", @This(), "intptr", JSC.DOMEffect.forRead(.World)), - .f32 = JSC.DOMCall("Reader", @This(), "f32", JSC.DOMEffect.forRead(.World)), - .f64 = JSC.DOMCall("Reader", @This(), "f64", JSC.DOMEffect.forRead(.World)), - }; - - pub fn toJS(globalThis: *JSC.JSGlobalObject) JSC.JSValue { - const obj = JSC.JSValue.createEmptyObject(globalThis, std.meta.fieldNames(@TypeOf(Reader.DOMCalls)).len); - - inline for (comptime std.meta.fieldNames(@TypeOf(Reader.DOMCalls))) |field| { - @field(Reader.DOMCalls, field).put(globalThis, obj); - } - - return obj; - } - - pub fn @"u8"( - globalObject: *JSGlobalObject, - _: JSValue, - arguments: []const JSValue, - ) bun.JSError!JSValue { - if (arguments.len == 0 or !arguments[0].isNumber()) { - return globalObject.throwInvalidArguments("Expected a pointer", .{}); - } - const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); - const value = @as(*align(1) u8, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - pub fn @"u16"( - globalObject: *JSGlobalObject, - _: JSValue, - arguments: []const JSValue, - ) bun.JSError!JSValue { - if (arguments.len == 0 or !arguments[0].isNumber()) { - return globalObject.throwInvalidArguments("Expected a pointer", .{}); - } - const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); - const value = @as(*align(1) u16, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - pub fn @"u32"( - globalObject: *JSGlobalObject, - _: JSValue, - arguments: []const JSValue, - ) bun.JSError!JSValue { - if (arguments.len == 0 or !arguments[0].isNumber()) { - return globalObject.throwInvalidArguments("Expected a pointer", .{}); - } - const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); - const value = @as(*align(1) u32, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - pub fn ptr( - globalObject: *JSGlobalObject, - _: JSValue, - arguments: []const JSValue, - ) bun.JSError!JSValue { - if (arguments.len == 0 or !arguments[0].isNumber()) { - return globalObject.throwInvalidArguments("Expected a pointer", .{}); - } - const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); - const value = @as(*align(1) u64, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - pub fn @"i8"( - globalObject: *JSGlobalObject, - _: JSValue, - arguments: []const JSValue, - ) bun.JSError!JSValue { - if (arguments.len == 0 or !arguments[0].isNumber()) { - return globalObject.throwInvalidArguments("Expected a pointer", .{}); - } - const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); - const value = @as(*align(1) i8, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - pub fn @"i16"( - globalObject: *JSGlobalObject, - _: JSValue, - arguments: []const JSValue, - ) bun.JSError!JSValue { - if (arguments.len == 0 or !arguments[0].isNumber()) { - return globalObject.throwInvalidArguments("Expected a pointer", .{}); - } - const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); - const value = @as(*align(1) i16, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - pub fn @"i32"( - globalObject: *JSGlobalObject, - _: JSValue, - arguments: []const JSValue, - ) bun.JSError!JSValue { - if (arguments.len == 0 or !arguments[0].isNumber()) { - return globalObject.throwInvalidArguments("Expected a pointer", .{}); - } - const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); - const value = @as(*align(1) i32, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - pub fn intptr( - globalObject: *JSGlobalObject, - _: JSValue, - arguments: []const JSValue, - ) bun.JSError!JSValue { - if (arguments.len == 0 or !arguments[0].isNumber()) { - return globalObject.throwInvalidArguments("Expected a pointer", .{}); - } - const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); - const value = @as(*align(1) i64, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - - pub fn @"f32"( - globalObject: *JSGlobalObject, - _: JSValue, - arguments: []const JSValue, - ) bun.JSError!JSValue { - if (arguments.len == 0 or !arguments[0].isNumber()) { - return globalObject.throwInvalidArguments("Expected a pointer", .{}); - } - const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); - const value = @as(*align(1) f32, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - - pub fn @"f64"( - globalObject: *JSGlobalObject, - _: JSValue, - arguments: []const JSValue, - ) bun.JSError!JSValue { - if (arguments.len == 0 or !arguments[0].isNumber()) { - return globalObject.throwInvalidArguments("Expected a pointer", .{}); - } - const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); - const value = @as(*align(1) f64, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - - pub fn @"i64"( - globalObject: *JSGlobalObject, - _: JSValue, - arguments: []const JSValue, - ) bun.JSError!JSValue { - if (arguments.len == 0 or !arguments[0].isNumber()) { - return globalObject.throwInvalidArguments("Expected a pointer", .{}); - } - const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); - const value = @as(*align(1) i64, @ptrFromInt(addr)).*; - return JSValue.fromInt64NoTruncate(globalObject, value); - } - - pub fn @"u64"( - globalObject: *JSGlobalObject, - _: JSValue, - arguments: []const JSValue, - ) bun.JSError!JSValue { - if (arguments.len == 0 or !arguments[0].isNumber()) { - return globalObject.throwInvalidArguments("Expected a pointer", .{}); - } - const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); - const value = @as(*align(1) u64, @ptrFromInt(addr)).*; - return JSValue.fromUInt64NoTruncate(globalObject, value); - } - - pub fn u8WithoutTypeChecks( - _: *JSGlobalObject, - _: *anyopaque, - raw_addr: i64, - offset: i32, - ) callconv(JSC.conv) JSValue { - const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); - const value = @as(*align(1) u8, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - pub fn u16WithoutTypeChecks( - _: *JSGlobalObject, - _: *anyopaque, - raw_addr: i64, - offset: i32, - ) callconv(JSC.conv) JSValue { - const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); - const value = @as(*align(1) u16, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - pub fn u32WithoutTypeChecks( - _: *JSGlobalObject, - _: *anyopaque, - raw_addr: i64, - offset: i32, - ) callconv(JSC.conv) JSValue { - const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); - const value = @as(*align(1) u32, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - pub fn ptrWithoutTypeChecks( - _: *JSGlobalObject, - _: *anyopaque, - raw_addr: i64, - offset: i32, - ) callconv(JSC.conv) JSValue { - const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); - const value = @as(*align(1) u64, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - pub fn i8WithoutTypeChecks( - _: *JSGlobalObject, - _: *anyopaque, - raw_addr: i64, - offset: i32, - ) callconv(JSC.conv) JSValue { - const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); - const value = @as(*align(1) i8, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - pub fn i16WithoutTypeChecks( - _: *JSGlobalObject, - _: *anyopaque, - raw_addr: i64, - offset: i32, - ) callconv(JSC.conv) JSValue { - const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); - const value = @as(*align(1) i16, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - pub fn i32WithoutTypeChecks( - _: *JSGlobalObject, - _: *anyopaque, - raw_addr: i64, - offset: i32, - ) callconv(JSC.conv) JSValue { - const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); - const value = @as(*align(1) i32, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - pub fn intptrWithoutTypeChecks( - _: *JSGlobalObject, - _: *anyopaque, - raw_addr: i64, - offset: i32, - ) callconv(JSC.conv) JSValue { - const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); - const value = @as(*align(1) i64, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - - pub fn f32WithoutTypeChecks( - _: *JSGlobalObject, - _: *anyopaque, - raw_addr: i64, - offset: i32, - ) callconv(JSC.conv) JSValue { - const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); - const value = @as(*align(1) f32, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - - pub fn f64WithoutTypeChecks( - _: *JSGlobalObject, - _: *anyopaque, - raw_addr: i64, - offset: i32, - ) callconv(JSC.conv) JSValue { - const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); - const value = @as(*align(1) f64, @ptrFromInt(addr)).*; - return JSValue.jsNumber(value); - } - - pub fn u64WithoutTypeChecks( - global: *JSGlobalObject, - _: *anyopaque, - raw_addr: i64, - offset: i32, - ) callconv(JSC.conv) JSValue { - const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); - const value = @as(*align(1) u64, @ptrFromInt(addr)).*; - return JSValue.fromUInt64NoTruncate(global, value); - } - - pub fn i64WithoutTypeChecks( - global: *JSGlobalObject, - _: *anyopaque, - raw_addr: i64, - offset: i32, - ) callconv(JSC.conv) JSValue { - const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); - const value = @as(*align(1) i64, @ptrFromInt(addr)).*; - return JSValue.fromInt64NoTruncate(global, value); - } - }; - - pub fn ptr( - globalThis: *JSGlobalObject, - _: JSValue, - arguments: []const JSValue, - ) JSValue { - return switch (arguments.len) { - 0 => ptr_(globalThis, JSValue.zero, null), - 1 => ptr_(globalThis, arguments[0], null), - else => ptr_(globalThis, arguments[0], arguments[1]), - }; - } - - pub fn ptrWithoutTypeChecks( - _: *JSGlobalObject, - _: *anyopaque, - array: *JSC.JSUint8Array, - ) callconv(JSC.conv) JSValue { - return JSValue.fromPtrAddress(@intFromPtr(array.ptr())); - } - - fn ptr_( - globalThis: *JSGlobalObject, - value: JSValue, - byteOffset: ?JSValue, - ) JSValue { - if (value == .zero) { - return JSC.JSValue.jsNull(); - } - - const array_buffer = value.asArrayBuffer(globalThis) orelse { - return JSC.toInvalidArguments("Expected ArrayBufferView but received {s}", .{@tagName(value.jsType())}, globalThis); - }; - - if (array_buffer.len == 0) { - return JSC.toInvalidArguments("ArrayBufferView must have a length > 0. A pointer to empty memory doesn't work", .{}, globalThis); - } - - var addr: usize = @intFromPtr(array_buffer.ptr); - // const Sizes = @import("../bindings/sizes.zig"); - // assert(addr == @intFromPtr(value.asEncoded().ptr) + Sizes.Bun_FFI_PointerOffsetToTypedArrayVector); - - if (byteOffset) |off| { - if (!off.isEmptyOrUndefinedOrNull()) { - if (!off.isNumber()) { - return JSC.toInvalidArguments("Expected number for byteOffset", .{}, globalThis); - } - } - - const bytei64 = off.toInt64(); - if (bytei64 < 0) { - addr -|= @as(usize, @intCast(bytei64 * -1)); - } else { - addr += @as(usize, @intCast(bytei64)); - } - - if (addr > @intFromPtr(array_buffer.ptr) + @as(usize, array_buffer.byte_len)) { - return JSC.toInvalidArguments("byteOffset out of bounds", .{}, globalThis); - } - } - - if (addr > max_addressable_memory) { - return JSC.toInvalidArguments("Pointer is outside max addressible memory, which usually means a bug in your program.", .{}, globalThis); - } - - if (addr == 0) { - return JSC.toInvalidArguments("Pointer must not be 0", .{}, globalThis); - } - - if (addr == 0xDEADBEEF or addr == 0xaaaaaaaa or addr == 0xAAAAAAAA) { - return JSC.toInvalidArguments("ptr to invalid memory, that would segfault Bun :(", .{}, globalThis); - } - - if (comptime Environment.allow_assert) { - assert(JSC.JSValue.fromPtrAddress(addr).asPtrAddress() == addr); - } - - return JSC.JSValue.fromPtrAddress(addr); - } - - const ValueOrError = union(enum) { - err: JSValue, - slice: []u8, - }; - - pub fn getPtrSlice(globalThis: *JSGlobalObject, value: JSValue, byteOffset: ?JSValue, byteLength: ?JSValue) ValueOrError { - if (!value.isNumber()) { - return .{ .err = JSC.toInvalidArguments("ptr must be a number.", .{}, globalThis) }; - } - - const num = value.asPtrAddress(); - if (num == 0) { - return .{ .err = JSC.toInvalidArguments("ptr cannot be zero, that would segfault Bun :(", .{}, globalThis) }; - } - - // if (!std.math.isFinite(num)) { - // return .{ .err = JSC.toInvalidArguments("ptr must be a finite number.", .{}, globalThis) }; - // } - - var addr = @as(usize, @bitCast(num)); - - if (byteOffset) |byte_off| { - if (byte_off.isNumber()) { - const off = byte_off.toInt64(); - if (off < 0) { - addr -|= @as(usize, @intCast(off * -1)); - } else { - addr +|= @as(usize, @intCast(off)); - } - - if (addr == 0) { - return .{ .err = JSC.toInvalidArguments("ptr cannot be zero, that would segfault Bun :(", .{}, globalThis) }; - } - - if (!std.math.isFinite(byte_off.asNumber())) { - return .{ .err = JSC.toInvalidArguments("ptr must be a finite number.", .{}, globalThis) }; - } - } else if (!byte_off.isEmptyOrUndefinedOrNull()) { - // do nothing - } else { - return .{ .err = JSC.toInvalidArguments("Expected number for byteOffset", .{}, globalThis) }; - } - } - - if (addr == 0xDEADBEEF or addr == 0xaaaaaaaa or addr == 0xAAAAAAAA) { - return .{ .err = JSC.toInvalidArguments("ptr to invalid memory, that would segfault Bun :(", .{}, globalThis) }; - } - - if (byteLength) |valueLength| { - if (!valueLength.isEmptyOrUndefinedOrNull()) { - if (!valueLength.isNumber()) { - return .{ .err = JSC.toInvalidArguments("length must be a number.", .{}, globalThis) }; - } - - if (valueLength.asNumber() == 0.0) { - return .{ .err = JSC.toInvalidArguments("length must be > 0. This usually means a bug in your code.", .{}, globalThis) }; - } - - const length_i = valueLength.toInt64(); - if (length_i < 0) { - return .{ .err = JSC.toInvalidArguments("length must be > 0. This usually means a bug in your code.", .{}, globalThis) }; - } - - if (length_i > max_addressable_memory) { - return .{ .err = JSC.toInvalidArguments("length exceeds max addressable memory. This usually means a bug in your code.", .{}, globalThis) }; - } - - const length = @as(usize, @intCast(length_i)); - return .{ .slice = @as([*]u8, @ptrFromInt(addr))[0..length] }; - } - } - - return .{ .slice = bun.span(@as([*:0]u8, @ptrFromInt(addr))) }; - } - - fn getCPtr(value: JSValue) ?usize { - // pointer to C function - if (value.isNumber()) { - const addr = value.asPtrAddress(); - if (addr > 0) return addr; - } else if (value.isBigInt()) { - const addr = @as(u64, @bitCast(value.toUInt64NoTruncate())); - if (addr > 0) { - return addr; - } - } - - return null; - } - - pub fn toArrayBuffer( - globalThis: *JSGlobalObject, - value: JSValue, - byteOffset: ?JSValue, - valueLength: ?JSValue, - finalizationCtxOrPtr: ?JSValue, - finalizationCallback: ?JSValue, - ) JSC.JSValue { - switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) { - .err => |erro| { - return erro; - }, - .slice => |slice| { - var callback: JSC.C.JSTypedArrayBytesDeallocator = null; - var ctx: ?*anyopaque = null; - if (finalizationCallback) |callback_value| { - if (getCPtr(callback_value)) |callback_ptr| { - callback = @as(JSC.C.JSTypedArrayBytesDeallocator, @ptrFromInt(callback_ptr)); - - if (finalizationCtxOrPtr) |ctx_value| { - if (getCPtr(ctx_value)) |ctx_ptr| { - ctx = @as(*anyopaque, @ptrFromInt(ctx_ptr)); - } else if (!ctx_value.isUndefinedOrNull()) { - return JSC.toInvalidArguments("Expected user data to be a C pointer (number or BigInt)", .{}, globalThis); - } - } - } else if (!callback_value.isEmptyOrUndefinedOrNull()) { - return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis); - } - } else if (finalizationCtxOrPtr) |callback_value| { - if (getCPtr(callback_value)) |callback_ptr| { - callback = @as(JSC.C.JSTypedArrayBytesDeallocator, @ptrFromInt(callback_ptr)); - } else if (!callback_value.isEmptyOrUndefinedOrNull()) { - return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis); - } - } - - return JSC.ArrayBuffer.fromBytes(slice, JSC.JSValue.JSType.ArrayBuffer).toJSWithContext(globalThis, ctx, callback, null); - }, - } - } - - pub fn toBuffer( - globalThis: *JSGlobalObject, - value: JSValue, - byteOffset: ?JSValue, - valueLength: ?JSValue, - finalizationCtxOrPtr: ?JSValue, - finalizationCallback: ?JSValue, - ) JSC.JSValue { - switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) { - .err => |err| { - return err; - }, - .slice => |slice| { - var callback: JSC.C.JSTypedArrayBytesDeallocator = null; - var ctx: ?*anyopaque = null; - if (finalizationCallback) |callback_value| { - if (getCPtr(callback_value)) |callback_ptr| { - callback = @as(JSC.C.JSTypedArrayBytesDeallocator, @ptrFromInt(callback_ptr)); - - if (finalizationCtxOrPtr) |ctx_value| { - if (getCPtr(ctx_value)) |ctx_ptr| { - ctx = @as(*anyopaque, @ptrFromInt(ctx_ptr)); - } else if (!ctx_value.isEmptyOrUndefinedOrNull()) { - return JSC.toInvalidArguments("Expected user data to be a C pointer (number or BigInt)", .{}, globalThis); - } - } - } else if (!callback_value.isEmptyOrUndefinedOrNull()) { - return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis); - } - } else if (finalizationCtxOrPtr) |callback_value| { - if (getCPtr(callback_value)) |callback_ptr| { - callback = @as(JSC.C.JSTypedArrayBytesDeallocator, @ptrFromInt(callback_ptr)); - } else if (!callback_value.isEmptyOrUndefinedOrNull()) { - return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis); - } - } - - if (callback != null or ctx != null) { - return JSC.JSValue.createBufferWithCtx(globalThis, slice, ctx, callback); - } - - return JSC.JSValue.createBuffer(globalThis, slice, null); - }, - } - } - - pub fn toCStringBuffer( - globalThis: *JSGlobalObject, - value: JSValue, - byteOffset: ?JSValue, - valueLength: ?JSValue, - ) JSC.JSValue { - switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) { - .err => |err| { - return err; - }, - .slice => |slice| { - return JSC.JSValue.createBuffer(globalThis, slice, null); - }, - } - } - - pub fn getter( - globalObject: *JSC.JSGlobalObject, - _: *JSC.JSObject, - ) JSC.JSValue { - return FFIObject.toJS(globalObject); - } -}; +pub const FFIObject = @import("./FFIObject.zig"); pub fn stringWidth(str: bun.String, opts: gen.StringWidthOptions) usize { if (str.length() == 0) @@ -4823,3 +1698,84 @@ comptime { } const assert = bun.assert; + +const conv = std.builtin.CallingConvention.Unspecified; +const S3File = @import("../webcore/S3File.zig"); +const Bun = @This(); +const default_allocator = bun.default_allocator; +const bun = @import("root").bun; +const uv = bun.windows.libuv; +const Environment = bun.Environment; +const Global = bun.Global; +const strings = bun.strings; +const string = bun.string; +const Output = bun.Output; +const MutableString = bun.MutableString; +const std = @import("std"); +const Allocator = std.mem.Allocator; +const IdentityContext = @import("../../identity_context.zig").IdentityContext; +const Fs = @import("../../fs.zig"); +const Resolver = @import("../../resolver/resolver.zig"); +const ast = @import("../../import_record.zig"); +const MacroEntryPoint = bun.transpiler.MacroEntryPoint; +const logger = bun.logger; +const Api = @import("../../api/schema.zig").Api; +const options = @import("../../options.zig"); +const ServerEntryPoint = bun.transpiler.ServerEntryPoint; +const js_printer = bun.js_printer; +const js_parser = bun.js_parser; +const js_ast = bun.JSAst; +const NodeFallbackModules = @import("../../node_fallbacks.zig"); +const ImportKind = ast.ImportKind; +const Analytics = @import("../../analytics/analytics_thread.zig"); +const ZigString = bun.JSC.ZigString; +const Runtime = @import("../../runtime.zig"); +const Router = @import("./filesystem_router.zig"); +const ImportRecord = ast.ImportRecord; +const DotEnv = @import("../../env_loader.zig"); +const ParseResult = bun.transpiler.ParseResult; +const PackageJSON = @import("../../resolver/package_json.zig").PackageJSON; +const MacroRemap = @import("../../resolver/package_json.zig").MacroMap; +const WebCore = bun.JSC.WebCore; +const Request = WebCore.Request; +const Response = WebCore.Response; +const Headers = WebCore.Headers; +const Fetch = WebCore.Fetch; +const js = bun.JSC.C; +const JSC = bun.JSC; +const JSError = @import("../base.zig").JSError; + +const MarkedArrayBuffer = @import("../base.zig").MarkedArrayBuffer; +const getAllocator = @import("../base.zig").getAllocator; +const JSValue = bun.JSC.JSValue; + +const JSGlobalObject = bun.JSC.JSGlobalObject; +const ExceptionValueRef = bun.JSC.ExceptionValueRef; +const JSPrivateDataPtr = bun.JSC.JSPrivateDataPtr; +const ConsoleObject = bun.JSC.ConsoleObject; +const Node = bun.JSC.Node; +const ZigException = bun.JSC.ZigException; +const ZigStackTrace = bun.JSC.ZigStackTrace; +const ErrorableResolvedSource = bun.JSC.ErrorableResolvedSource; +const ResolvedSource = bun.JSC.ResolvedSource; +const JSPromise = bun.JSC.JSPromise; +const JSInternalPromise = bun.JSC.JSInternalPromise; +const JSModuleLoader = bun.JSC.JSModuleLoader; +const JSPromiseRejectionOperation = bun.JSC.JSPromiseRejectionOperation; +const ErrorableZigString = bun.JSC.ErrorableZigString; +const VM = bun.JSC.VM; +const JSFunction = bun.JSC.JSFunction; +const Config = @import("../config.zig"); +const URL = @import("../../url.zig").URL; +const Transpiler = bun.JSC.API.JSTranspiler; +const JSBundler = bun.JSC.API.JSBundler; +const VirtualMachine = JSC.VirtualMachine; +const IOTask = JSC.IOTask; +const zlib = @import("../../zlib.zig"); +const Which = @import("../../which.zig"); +const ErrorableString = JSC.ErrorableString; +const glob = @import("../../glob.zig"); +const Async = bun.Async; +const SemverObject = bun.Semver.SemverObject; +const Braces = @import("../../shell/braces.zig"); +const Shell = @import("../../shell/shell.zig"); diff --git a/src/bun.js/api/FFIObject.zig b/src/bun.js/api/FFIObject.zig new file mode 100644 index 0000000000..a225e6e33d --- /dev/null +++ b/src/bun.js/api/FFIObject.zig @@ -0,0 +1,638 @@ +pub fn newCString(globalThis: *JSGlobalObject, value: JSValue, byteOffset: ?JSValue, lengthValue: ?JSValue) JSC.JSValue { + switch (FFIObject.getPtrSlice(globalThis, value, byteOffset, lengthValue)) { + .err => |err| { + return err; + }, + .slice => |slice| { + return bun.String.createUTF8ForJS(globalThis, slice); + }, + } +} + +pub const dom_call = JSC.DOMCall("FFI", @This(), "ptr", JSC.DOMEffect.forRead(.TypedArrayProperties)); + +pub fn toJS(globalObject: *JSC.JSGlobalObject) JSC.JSValue { + const object = JSC.JSValue.createEmptyObject(globalObject, comptime std.meta.fieldNames(@TypeOf(fields)).len + 2); + inline for (comptime std.meta.fieldNames(@TypeOf(fields))) |field| { + object.put( + globalObject, + comptime ZigString.static(field), + JSC.createCallback(globalObject, comptime ZigString.static(field), 1, comptime @field(fields, field)), + ); + } + + dom_call.put(globalObject, object); + object.put(globalObject, ZigString.static("read"), Reader.toJS(globalObject)); + + return object; +} + +pub const Reader = struct { + pub const DOMCalls = .{ + .u8 = JSC.DOMCall("Reader", @This(), "u8", JSC.DOMEffect.forRead(.World)), + .u16 = JSC.DOMCall("Reader", @This(), "u16", JSC.DOMEffect.forRead(.World)), + .u32 = JSC.DOMCall("Reader", @This(), "u32", JSC.DOMEffect.forRead(.World)), + .ptr = JSC.DOMCall("Reader", @This(), "ptr", JSC.DOMEffect.forRead(.World)), + .i8 = JSC.DOMCall("Reader", @This(), "i8", JSC.DOMEffect.forRead(.World)), + .i16 = JSC.DOMCall("Reader", @This(), "i16", JSC.DOMEffect.forRead(.World)), + .i32 = JSC.DOMCall("Reader", @This(), "i32", JSC.DOMEffect.forRead(.World)), + .i64 = JSC.DOMCall("Reader", @This(), "i64", JSC.DOMEffect.forRead(.World)), + .u64 = JSC.DOMCall("Reader", @This(), "u64", JSC.DOMEffect.forRead(.World)), + .intptr = JSC.DOMCall("Reader", @This(), "intptr", JSC.DOMEffect.forRead(.World)), + .f32 = JSC.DOMCall("Reader", @This(), "f32", JSC.DOMEffect.forRead(.World)), + .f64 = JSC.DOMCall("Reader", @This(), "f64", JSC.DOMEffect.forRead(.World)), + }; + + pub fn toJS(globalThis: *JSC.JSGlobalObject) JSC.JSValue { + const obj = JSC.JSValue.createEmptyObject(globalThis, std.meta.fieldNames(@TypeOf(Reader.DOMCalls)).len); + + inline for (comptime std.meta.fieldNames(@TypeOf(Reader.DOMCalls))) |field| { + @field(Reader.DOMCalls, field).put(globalThis, obj); + } + + return obj; + } + + pub fn @"u8"( + globalObject: *JSGlobalObject, + _: JSValue, + arguments: []const JSValue, + ) bun.JSError!JSValue { + if (arguments.len == 0 or !arguments[0].isNumber()) { + return globalObject.throwInvalidArguments("Expected a pointer", .{}); + } + const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); + const value = @as(*align(1) u8, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + pub fn @"u16"( + globalObject: *JSGlobalObject, + _: JSValue, + arguments: []const JSValue, + ) bun.JSError!JSValue { + if (arguments.len == 0 or !arguments[0].isNumber()) { + return globalObject.throwInvalidArguments("Expected a pointer", .{}); + } + const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); + const value = @as(*align(1) u16, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + pub fn @"u32"( + globalObject: *JSGlobalObject, + _: JSValue, + arguments: []const JSValue, + ) bun.JSError!JSValue { + if (arguments.len == 0 or !arguments[0].isNumber()) { + return globalObject.throwInvalidArguments("Expected a pointer", .{}); + } + const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); + const value = @as(*align(1) u32, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + pub fn ptr( + globalObject: *JSGlobalObject, + _: JSValue, + arguments: []const JSValue, + ) bun.JSError!JSValue { + if (arguments.len == 0 or !arguments[0].isNumber()) { + return globalObject.throwInvalidArguments("Expected a pointer", .{}); + } + const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); + const value = @as(*align(1) u64, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + pub fn @"i8"( + globalObject: *JSGlobalObject, + _: JSValue, + arguments: []const JSValue, + ) bun.JSError!JSValue { + if (arguments.len == 0 or !arguments[0].isNumber()) { + return globalObject.throwInvalidArguments("Expected a pointer", .{}); + } + const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); + const value = @as(*align(1) i8, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + pub fn @"i16"( + globalObject: *JSGlobalObject, + _: JSValue, + arguments: []const JSValue, + ) bun.JSError!JSValue { + if (arguments.len == 0 or !arguments[0].isNumber()) { + return globalObject.throwInvalidArguments("Expected a pointer", .{}); + } + const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); + const value = @as(*align(1) i16, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + pub fn @"i32"( + globalObject: *JSGlobalObject, + _: JSValue, + arguments: []const JSValue, + ) bun.JSError!JSValue { + if (arguments.len == 0 or !arguments[0].isNumber()) { + return globalObject.throwInvalidArguments("Expected a pointer", .{}); + } + const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); + const value = @as(*align(1) i32, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + pub fn intptr( + globalObject: *JSGlobalObject, + _: JSValue, + arguments: []const JSValue, + ) bun.JSError!JSValue { + if (arguments.len == 0 or !arguments[0].isNumber()) { + return globalObject.throwInvalidArguments("Expected a pointer", .{}); + } + const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); + const value = @as(*align(1) i64, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + + pub fn @"f32"( + globalObject: *JSGlobalObject, + _: JSValue, + arguments: []const JSValue, + ) bun.JSError!JSValue { + if (arguments.len == 0 or !arguments[0].isNumber()) { + return globalObject.throwInvalidArguments("Expected a pointer", .{}); + } + const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); + const value = @as(*align(1) f32, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + + pub fn @"f64"( + globalObject: *JSGlobalObject, + _: JSValue, + arguments: []const JSValue, + ) bun.JSError!JSValue { + if (arguments.len == 0 or !arguments[0].isNumber()) { + return globalObject.throwInvalidArguments("Expected a pointer", .{}); + } + const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); + const value = @as(*align(1) f64, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + + pub fn @"i64"( + globalObject: *JSGlobalObject, + _: JSValue, + arguments: []const JSValue, + ) bun.JSError!JSValue { + if (arguments.len == 0 or !arguments[0].isNumber()) { + return globalObject.throwInvalidArguments("Expected a pointer", .{}); + } + const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); + const value = @as(*align(1) i64, @ptrFromInt(addr)).*; + return JSValue.fromInt64NoTruncate(globalObject, value); + } + + pub fn @"u64"( + globalObject: *JSGlobalObject, + _: JSValue, + arguments: []const JSValue, + ) bun.JSError!JSValue { + if (arguments.len == 0 or !arguments[0].isNumber()) { + return globalObject.throwInvalidArguments("Expected a pointer", .{}); + } + const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0); + const value = @as(*align(1) u64, @ptrFromInt(addr)).*; + return JSValue.fromUInt64NoTruncate(globalObject, value); + } + + pub fn u8WithoutTypeChecks( + _: *JSGlobalObject, + _: *anyopaque, + raw_addr: i64, + offset: i32, + ) callconv(JSC.conv) JSValue { + const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); + const value = @as(*align(1) u8, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + pub fn u16WithoutTypeChecks( + _: *JSGlobalObject, + _: *anyopaque, + raw_addr: i64, + offset: i32, + ) callconv(JSC.conv) JSValue { + const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); + const value = @as(*align(1) u16, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + pub fn u32WithoutTypeChecks( + _: *JSGlobalObject, + _: *anyopaque, + raw_addr: i64, + offset: i32, + ) callconv(JSC.conv) JSValue { + const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); + const value = @as(*align(1) u32, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + pub fn ptrWithoutTypeChecks( + _: *JSGlobalObject, + _: *anyopaque, + raw_addr: i64, + offset: i32, + ) callconv(JSC.conv) JSValue { + const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); + const value = @as(*align(1) u64, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + pub fn i8WithoutTypeChecks( + _: *JSGlobalObject, + _: *anyopaque, + raw_addr: i64, + offset: i32, + ) callconv(JSC.conv) JSValue { + const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); + const value = @as(*align(1) i8, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + pub fn i16WithoutTypeChecks( + _: *JSGlobalObject, + _: *anyopaque, + raw_addr: i64, + offset: i32, + ) callconv(JSC.conv) JSValue { + const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); + const value = @as(*align(1) i16, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + pub fn i32WithoutTypeChecks( + _: *JSGlobalObject, + _: *anyopaque, + raw_addr: i64, + offset: i32, + ) callconv(JSC.conv) JSValue { + const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); + const value = @as(*align(1) i32, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + pub fn intptrWithoutTypeChecks( + _: *JSGlobalObject, + _: *anyopaque, + raw_addr: i64, + offset: i32, + ) callconv(JSC.conv) JSValue { + const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); + const value = @as(*align(1) i64, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + + pub fn f32WithoutTypeChecks( + _: *JSGlobalObject, + _: *anyopaque, + raw_addr: i64, + offset: i32, + ) callconv(JSC.conv) JSValue { + const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); + const value = @as(*align(1) f32, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + + pub fn f64WithoutTypeChecks( + _: *JSGlobalObject, + _: *anyopaque, + raw_addr: i64, + offset: i32, + ) callconv(JSC.conv) JSValue { + const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); + const value = @as(*align(1) f64, @ptrFromInt(addr)).*; + return JSValue.jsNumber(value); + } + + pub fn u64WithoutTypeChecks( + global: *JSGlobalObject, + _: *anyopaque, + raw_addr: i64, + offset: i32, + ) callconv(JSC.conv) JSValue { + const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); + const value = @as(*align(1) u64, @ptrFromInt(addr)).*; + return JSValue.fromUInt64NoTruncate(global, value); + } + + pub fn i64WithoutTypeChecks( + global: *JSGlobalObject, + _: *anyopaque, + raw_addr: i64, + offset: i32, + ) callconv(JSC.conv) JSValue { + const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset)); + const value = @as(*align(1) i64, @ptrFromInt(addr)).*; + return JSValue.fromInt64NoTruncate(global, value); + } +}; + +pub fn ptr( + globalThis: *JSGlobalObject, + _: JSValue, + arguments: []const JSValue, +) JSValue { + return switch (arguments.len) { + 0 => ptr_(globalThis, JSValue.zero, null), + 1 => ptr_(globalThis, arguments[0], null), + else => ptr_(globalThis, arguments[0], arguments[1]), + }; +} + +pub fn ptrWithoutTypeChecks( + _: *JSGlobalObject, + _: *anyopaque, + array: *JSC.JSUint8Array, +) callconv(JSC.conv) JSValue { + return JSValue.fromPtrAddress(@intFromPtr(array.ptr())); +} + +fn ptr_( + globalThis: *JSGlobalObject, + value: JSValue, + byteOffset: ?JSValue, +) JSValue { + if (value == .zero) { + return JSC.JSValue.jsNull(); + } + + const array_buffer = value.asArrayBuffer(globalThis) orelse { + return JSC.toInvalidArguments("Expected ArrayBufferView but received {s}", .{@tagName(value.jsType())}, globalThis); + }; + + if (array_buffer.len == 0) { + return JSC.toInvalidArguments("ArrayBufferView must have a length > 0. A pointer to empty memory doesn't work", .{}, globalThis); + } + + var addr: usize = @intFromPtr(array_buffer.ptr); + // const Sizes = @import("../bindings/sizes.zig"); + // assert(addr == @intFromPtr(value.asEncoded().ptr) + Sizes.Bun_FFI_PointerOffsetToTypedArrayVector); + + if (byteOffset) |off| { + if (!off.isEmptyOrUndefinedOrNull()) { + if (!off.isNumber()) { + return JSC.toInvalidArguments("Expected number for byteOffset", .{}, globalThis); + } + } + + const bytei64 = off.toInt64(); + if (bytei64 < 0) { + addr -|= @as(usize, @intCast(bytei64 * -1)); + } else { + addr += @as(usize, @intCast(bytei64)); + } + + if (addr > @intFromPtr(array_buffer.ptr) + @as(usize, array_buffer.byte_len)) { + return JSC.toInvalidArguments("byteOffset out of bounds", .{}, globalThis); + } + } + + if (addr > max_addressable_memory) { + return JSC.toInvalidArguments("Pointer is outside max addressible memory, which usually means a bug in your program.", .{}, globalThis); + } + + if (addr == 0) { + return JSC.toInvalidArguments("Pointer must not be 0", .{}, globalThis); + } + + if (addr == 0xDEADBEEF or addr == 0xaaaaaaaa or addr == 0xAAAAAAAA) { + return JSC.toInvalidArguments("ptr to invalid memory, that would segfault Bun :(", .{}, globalThis); + } + + if (comptime Environment.allow_assert) { + assert(JSC.JSValue.fromPtrAddress(addr).asPtrAddress() == addr); + } + + return JSC.JSValue.fromPtrAddress(addr); +} + +const ValueOrError = union(enum) { + err: JSValue, + slice: []u8, +}; + +pub fn getPtrSlice(globalThis: *JSGlobalObject, value: JSValue, byteOffset: ?JSValue, byteLength: ?JSValue) ValueOrError { + if (!value.isNumber()) { + return .{ .err = JSC.toInvalidArguments("ptr must be a number.", .{}, globalThis) }; + } + + const num = value.asPtrAddress(); + if (num == 0) { + return .{ .err = JSC.toInvalidArguments("ptr cannot be zero, that would segfault Bun :(", .{}, globalThis) }; + } + + // if (!std.math.isFinite(num)) { + // return .{ .err = JSC.toInvalidArguments("ptr must be a finite number.", .{}, globalThis) }; + // } + + var addr = @as(usize, @bitCast(num)); + + if (byteOffset) |byte_off| { + if (byte_off.isNumber()) { + const off = byte_off.toInt64(); + if (off < 0) { + addr -|= @as(usize, @intCast(off * -1)); + } else { + addr +|= @as(usize, @intCast(off)); + } + + if (addr == 0) { + return .{ .err = JSC.toInvalidArguments("ptr cannot be zero, that would segfault Bun :(", .{}, globalThis) }; + } + + if (!std.math.isFinite(byte_off.asNumber())) { + return .{ .err = JSC.toInvalidArguments("ptr must be a finite number.", .{}, globalThis) }; + } + } else if (!byte_off.isEmptyOrUndefinedOrNull()) { + // do nothing + } else { + return .{ .err = JSC.toInvalidArguments("Expected number for byteOffset", .{}, globalThis) }; + } + } + + if (addr == 0xDEADBEEF or addr == 0xaaaaaaaa or addr == 0xAAAAAAAA) { + return .{ .err = JSC.toInvalidArguments("ptr to invalid memory, that would segfault Bun :(", .{}, globalThis) }; + } + + if (byteLength) |valueLength| { + if (!valueLength.isEmptyOrUndefinedOrNull()) { + if (!valueLength.isNumber()) { + return .{ .err = JSC.toInvalidArguments("length must be a number.", .{}, globalThis) }; + } + + if (valueLength.asNumber() == 0.0) { + return .{ .err = JSC.toInvalidArguments("length must be > 0. This usually means a bug in your code.", .{}, globalThis) }; + } + + const length_i = valueLength.toInt64(); + if (length_i < 0) { + return .{ .err = JSC.toInvalidArguments("length must be > 0. This usually means a bug in your code.", .{}, globalThis) }; + } + + if (length_i > max_addressable_memory) { + return .{ .err = JSC.toInvalidArguments("length exceeds max addressable memory. This usually means a bug in your code.", .{}, globalThis) }; + } + + const length = @as(usize, @intCast(length_i)); + return .{ .slice = @as([*]u8, @ptrFromInt(addr))[0..length] }; + } + } + + return .{ .slice = bun.span(@as([*:0]u8, @ptrFromInt(addr))) }; +} + +fn getCPtr(value: JSValue) ?usize { + // pointer to C function + if (value.isNumber()) { + const addr = value.asPtrAddress(); + if (addr > 0) return addr; + } else if (value.isBigInt()) { + const addr = @as(u64, @bitCast(value.toUInt64NoTruncate())); + if (addr > 0) { + return addr; + } + } + + return null; +} + +pub fn toArrayBuffer( + globalThis: *JSGlobalObject, + value: JSValue, + byteOffset: ?JSValue, + valueLength: ?JSValue, + finalizationCtxOrPtr: ?JSValue, + finalizationCallback: ?JSValue, +) JSC.JSValue { + switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) { + .err => |erro| { + return erro; + }, + .slice => |slice| { + var callback: JSC.C.JSTypedArrayBytesDeallocator = null; + var ctx: ?*anyopaque = null; + if (finalizationCallback) |callback_value| { + if (getCPtr(callback_value)) |callback_ptr| { + callback = @as(JSC.C.JSTypedArrayBytesDeallocator, @ptrFromInt(callback_ptr)); + + if (finalizationCtxOrPtr) |ctx_value| { + if (getCPtr(ctx_value)) |ctx_ptr| { + ctx = @as(*anyopaque, @ptrFromInt(ctx_ptr)); + } else if (!ctx_value.isUndefinedOrNull()) { + return JSC.toInvalidArguments("Expected user data to be a C pointer (number or BigInt)", .{}, globalThis); + } + } + } else if (!callback_value.isEmptyOrUndefinedOrNull()) { + return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis); + } + } else if (finalizationCtxOrPtr) |callback_value| { + if (getCPtr(callback_value)) |callback_ptr| { + callback = @as(JSC.C.JSTypedArrayBytesDeallocator, @ptrFromInt(callback_ptr)); + } else if (!callback_value.isEmptyOrUndefinedOrNull()) { + return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis); + } + } + + return JSC.ArrayBuffer.fromBytes(slice, JSC.JSValue.JSType.ArrayBuffer).toJSWithContext(globalThis, ctx, callback, null); + }, + } +} + +pub fn toBuffer( + globalThis: *JSGlobalObject, + value: JSValue, + byteOffset: ?JSValue, + valueLength: ?JSValue, + finalizationCtxOrPtr: ?JSValue, + finalizationCallback: ?JSValue, +) JSC.JSValue { + switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) { + .err => |err| { + return err; + }, + .slice => |slice| { + var callback: JSC.C.JSTypedArrayBytesDeallocator = null; + var ctx: ?*anyopaque = null; + if (finalizationCallback) |callback_value| { + if (getCPtr(callback_value)) |callback_ptr| { + callback = @as(JSC.C.JSTypedArrayBytesDeallocator, @ptrFromInt(callback_ptr)); + + if (finalizationCtxOrPtr) |ctx_value| { + if (getCPtr(ctx_value)) |ctx_ptr| { + ctx = @as(*anyopaque, @ptrFromInt(ctx_ptr)); + } else if (!ctx_value.isEmptyOrUndefinedOrNull()) { + return JSC.toInvalidArguments("Expected user data to be a C pointer (number or BigInt)", .{}, globalThis); + } + } + } else if (!callback_value.isEmptyOrUndefinedOrNull()) { + return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis); + } + } else if (finalizationCtxOrPtr) |callback_value| { + if (getCPtr(callback_value)) |callback_ptr| { + callback = @as(JSC.C.JSTypedArrayBytesDeallocator, @ptrFromInt(callback_ptr)); + } else if (!callback_value.isEmptyOrUndefinedOrNull()) { + return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis); + } + } + + if (callback != null or ctx != null) { + return JSC.JSValue.createBufferWithCtx(globalThis, slice, ctx, callback); + } + + return JSC.JSValue.createBuffer(globalThis, slice, null); + }, + } +} + +pub fn toCStringBuffer( + globalThis: *JSGlobalObject, + value: JSValue, + byteOffset: ?JSValue, + valueLength: ?JSValue, +) JSC.JSValue { + switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) { + .err => |err| { + return err; + }, + .slice => |slice| { + return JSC.JSValue.createBuffer(globalThis, slice, null); + }, + } +} + +pub fn getter( + globalObject: *JSC.JSGlobalObject, + _: *JSC.JSObject, +) JSC.JSValue { + return FFIObject.toJS(globalObject); +} + +const fields = .{ + .viewSource = JSC.wrapStaticMethod( + JSC.FFI, + "print", + false, + ), + .dlopen = JSC.wrapStaticMethod(JSC.FFI, "open", false), + .callback = JSC.wrapStaticMethod(JSC.FFI, "callback", false), + .linkSymbols = JSC.wrapStaticMethod(JSC.FFI, "linkSymbols", false), + .toBuffer = JSC.wrapStaticMethod(@This(), "toBuffer", false), + .toArrayBuffer = JSC.wrapStaticMethod(@This(), "toArrayBuffer", false), + .closeCallback = JSC.wrapStaticMethod(JSC.FFI, "closeCallback", false), + .CString = JSC.wrapStaticMethod(Bun.FFIObject, "newCString", false), +}; +const max_addressable_memory = std.math.maxInt(u56); + +const JSGlobalObject = JSC.JSGlobalObject; +const JSObject = JSC.JSObject; +const JSValue = JSC.JSValue; +const JSC = bun.JSC; +const bun = @import("root").bun; +const FFIObject = @This(); +const Bun = JSC.API.Bun; + +const Environment = bun.Environment; +const std = @import("std"); +const assert = bun.assert; +const ZigString = JSC.ZigString; diff --git a/src/bun.js/api/HashObject.zig b/src/bun.js/api/HashObject.zig new file mode 100644 index 0000000000..6dc1567879 --- /dev/null +++ b/src/bun.js/api/HashObject.zig @@ -0,0 +1,144 @@ +pub const wyhash = hashWrap(std.hash.Wyhash); +pub const adler32 = hashWrap(std.hash.Adler32); +pub const crc32 = hashWrap(std.hash.Crc32); +pub const cityHash32 = hashWrap(std.hash.CityHash32); +pub const cityHash64 = hashWrap(std.hash.CityHash64); +pub const xxHash32 = hashWrap(struct { + pub fn hash(seed: u32, bytes: []const u8) u32 { + // sidestep .hash taking in anytype breaking ArgTuple + // downstream by forcing a type signature on the input + return std.hash.XxHash32.hash(seed, bytes); + } +}); +pub const xxHash64 = hashWrap(struct { + pub fn hash(seed: u32, bytes: []const u8) u64 { + // sidestep .hash taking in anytype breaking ArgTuple + // downstream by forcing a type signature on the input + return std.hash.XxHash64.hash(seed, bytes); + } +}); +pub const xxHash3 = hashWrap(struct { + pub fn hash(seed: u32, bytes: []const u8) u64 { + // sidestep .hash taking in anytype breaking ArgTuple + // downstream by forcing a type signature on the input + return std.hash.XxHash3.hash(seed, bytes); + } +}); +pub const murmur32v2 = hashWrap(std.hash.murmur.Murmur2_32); +pub const murmur32v3 = hashWrap(std.hash.murmur.Murmur3_32); +pub const murmur64v2 = hashWrap(std.hash.murmur.Murmur2_64); + +pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue { + const function = JSC.createCallback(globalThis, ZigString.static("hash"), 1, wyhash); + const fns = comptime .{ + "wyhash", + "adler32", + "crc32", + "cityHash32", + "cityHash64", + "xxHash32", + "xxHash64", + "xxHash3", + "murmur32v2", + "murmur32v3", + "murmur64v2", + }; + inline for (fns) |name| { + const value = JSC.createCallback( + globalThis, + ZigString.static(name), + 1, + @field(HashObject, name), + ); + function.put(globalThis, comptime ZigString.static(name), value); + } + + return function; +} + +fn hashWrap(comptime Hasher_: anytype) JSC.JSHostZigFunction { + return struct { + const Hasher = Hasher_; + pub fn hash(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const arguments = callframe.arguments_old(2).slice(); + var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments); + defer args.deinit(); + + var input: []const u8 = ""; + var input_slice = ZigString.Slice.empty; + defer input_slice.deinit(); + if (args.nextEat()) |arg| { + if (arg.as(JSC.WebCore.Blob)) |blob| { + // TODO: files + input = blob.sharedView(); + } else { + switch (arg.jsTypeLoose()) { + .ArrayBuffer, + .Int8Array, + .Uint8Array, + .Uint8ClampedArray, + .Int16Array, + .Uint16Array, + .Int32Array, + .Uint32Array, + .Float16Array, + .Float32Array, + .Float64Array, + .BigInt64Array, + .BigUint64Array, + .DataView, + => { + var array_buffer = arg.asArrayBuffer(globalThis) orelse { + return globalThis.throwInvalidArguments("ArrayBuffer conversion error", .{}); + }; + input = array_buffer.byteSlice(); + }, + else => { + input_slice = try arg.toSlice(globalThis, bun.default_allocator); + input = input_slice.slice(); + }, + } + } + } + + // std.hash has inconsistent interfaces + // + const Function = if (@hasDecl(Hasher, "hashWithSeed")) Hasher.hashWithSeed else Hasher.hash; + var function_args: std.meta.ArgsTuple(@TypeOf(Function)) = undefined; + if (comptime std.meta.fields(std.meta.ArgsTuple(@TypeOf(Function))).len == 1) { + return JSC.JSValue.jsNumber(Function(input)); + } else { + var seed: u64 = 0; + if (args.nextEat()) |arg| { + if (arg.isNumber() or arg.isBigInt()) { + seed = arg.toUInt64NoTruncate(); + } + } + if (comptime bun.trait.isNumber(@TypeOf(function_args[0]))) { + function_args[0] = @as(@TypeOf(function_args[0]), @truncate(seed)); + function_args[1] = input; + } else { + function_args[0] = input; + function_args[1] = @as(@TypeOf(function_args[1]), @truncate(seed)); + } + + const value = @call(.auto, Function, function_args); + + if (@TypeOf(value) == u32) { + return JSC.JSValue.jsNumber(@as(u32, @bitCast(value))); + } + return JSC.JSValue.fromUInt64NoTruncate(globalThis, value); + } + } + }.hash; +} + +const HashObject = @This(); + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const JSObject = JSC.JSObject; +const std = @import("std"); +const bun = @import("root").bun; +const ZigString = JSC.ZigString; diff --git a/src/bun.js/api/TOMLObject.zig b/src/bun.js/api/TOMLObject.zig new file mode 100644 index 0000000000..9273d972d0 --- /dev/null +++ b/src/bun.js/api/TOMLObject.zig @@ -0,0 +1,72 @@ +pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue { + const object = JSValue.createEmptyObject(globalThis, 1); + object.put( + globalThis, + ZigString.static("parse"), + JSC.createCallback( + globalThis, + ZigString.static("parse"), + 1, + parse, + ), + ); + + return object; +} + +pub fn parse( + globalThis: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, +) bun.JSError!JSC.JSValue { + var arena = bun.ArenaAllocator.init(globalThis.allocator()); + const allocator = arena.allocator(); + defer arena.deinit(); + var log = logger.Log.init(default_allocator); + const arguments = callframe.arguments_old(1).slice(); + if (arguments.len == 0 or arguments[0].isEmptyOrUndefinedOrNull()) { + return globalThis.throwInvalidArguments("Expected a string to parse", .{}); + } + + var input_slice = try arguments[0].toSlice(globalThis, bun.default_allocator); + defer input_slice.deinit(); + var source = logger.Source.initPathString("input.toml", input_slice.slice()); + const parse_result = TOMLParser.parse(&source, &log, allocator, false) catch { + return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to parse toml")); + }; + + // for now... + const buffer_writer = js_printer.BufferWriter.init(allocator) catch { + return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml")); + }; + var writer = js_printer.BufferPrinter.init(buffer_writer); + _ = js_printer.printJSON( + *js_printer.BufferPrinter, + &writer, + parse_result, + &source, + .{ + .mangled_props = null, + }, + ) catch { + return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml")); + }; + + const slice = writer.ctx.buffer.slice(); + var out = bun.String.fromUTF8(slice); + defer out.deref(); + + return out.toJSByParseJSON(globalThis); +} + +const TOMLObject = @This(); +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const JSObject = JSC.JSObject; +const std = @import("std"); +const ZigString = JSC.ZigString; +const logger = bun.logger; +const bun = @import("root").bun; +const js_printer = bun.js_printer; +const default_allocator = bun.default_allocator; +const TOMLParser = @import("../../toml/toml_parser.zig").TOML; diff --git a/src/bun.js/api/UnsafeObject.zig b/src/bun.js/api/UnsafeObject.zig new file mode 100644 index 0000000000..9e8d8a2c85 --- /dev/null +++ b/src/bun.js/api/UnsafeObject.zig @@ -0,0 +1,76 @@ +pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue { + const object = JSValue.createEmptyObject(globalThis, 3); + const fields = comptime .{ + .gcAggressionLevel = gcAggressionLevel, + .arrayBufferToString = arrayBufferToString, + .mimallocDump = dump_mimalloc, + }; + inline for (comptime std.meta.fieldNames(@TypeOf(fields))) |name| { + object.put( + globalThis, + comptime ZigString.static(name), + JSC.createCallback(globalThis, comptime ZigString.static(name), 1, comptime @field(fields, name)), + ); + } + return object; +} + +pub fn gcAggressionLevel( + globalThis: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, +) bun.JSError!JSC.JSValue { + const ret = JSValue.jsNumber(@as(i32, @intFromEnum(globalThis.bunVM().aggressive_garbage_collection))); + const value = callframe.arguments_old(1).ptr[0]; + + if (!value.isEmptyOrUndefinedOrNull()) { + switch (value.coerce(i32, globalThis)) { + 1 => globalThis.bunVM().aggressive_garbage_collection = .mild, + 2 => globalThis.bunVM().aggressive_garbage_collection = .aggressive, + 0 => globalThis.bunVM().aggressive_garbage_collection = .none, + else => {}, + } + } + return ret; +} + +pub fn arrayBufferToString( + globalThis: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, +) bun.JSError!JSC.JSValue { + const args = callframe.arguments_old(2).slice(); + if (args.len < 1 or !args[0].isCell() or !args[0].jsType().isTypedArrayOrArrayBuffer()) { + return globalThis.throwInvalidArguments("Expected an ArrayBuffer", .{}); + } + + const array_buffer = JSC.ArrayBuffer.fromTypedArray(globalThis, args[0]); + switch (array_buffer.typed_array_type) { + .Uint16Array, .Int16Array => { + var zig_str = ZigString.init(""); + zig_str._unsafe_ptr_do_not_use = @as([*]const u8, @ptrCast(@alignCast(array_buffer.ptr))); + zig_str.len = array_buffer.len; + zig_str.markUTF16(); + return zig_str.toJS(globalThis); + }, + else => { + return ZigString.init(array_buffer.slice()).toJS(globalThis); + }, + } +} + +extern fn dump_zone_malloc_stats() void; + +fn dump_mimalloc(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { + globalObject.bunVM().arena.dumpStats(); + if (bun.heap_breakdown.enabled) { + dump_zone_malloc_stats(); + } + return .undefined; +} + +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const JSObject = JSC.JSObject; +const std = @import("std"); +const bun = @import("root").bun; +const ZigString = JSC.ZigString; diff --git a/src/bun.js/api/crypto.zig b/src/bun.js/api/crypto.zig new file mode 100644 index 0000000000..549fcc18ef --- /dev/null +++ b/src/bun.js/api/crypto.zig @@ -0,0 +1,32 @@ +pub fn createCryptoError(globalThis: *JSC.JSGlobalObject, err_code: u32) JSValue { + return bun.BoringSSL.ERR_toJS(globalThis, err_code); +} + +pub const PasswordObject = @import("./crypto/PasswordObject.zig").PasswordObject; +pub const JSPasswordObject = @import("./crypto/PasswordObject.zig").JSPasswordObject; + +pub const CryptoHasher = @import("./crypto/CryptoHasher.zig").CryptoHasher; +pub const MD4 = @import("./crypto/CryptoHasher.zig").MD4; +pub const MD5 = @import("./crypto/CryptoHasher.zig").MD5; +pub const SHA1 = @import("./crypto/CryptoHasher.zig").SHA1; +pub const SHA224 = @import("./crypto/CryptoHasher.zig").SHA224; +pub const SHA256 = @import("./crypto/CryptoHasher.zig").SHA256; +pub const SHA384 = @import("./crypto/CryptoHasher.zig").SHA384; +pub const SHA512 = @import("./crypto/CryptoHasher.zig").SHA512; +pub const SHA512_256 = @import("./crypto/CryptoHasher.zig").SHA512_256; +pub const HMAC = @import("./crypto/HMAC.zig"); +pub const EVP = @import("./crypto/EVP.zig"); + +comptime { + CryptoHasher.Extern.@"export"(); +} + +const std = @import("std"); +const bun = @import("root").bun; +const string = bun.string; +const strings = bun.strings; +const MutableString = bun.MutableString; +const stringZ = bun.stringZ; +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; diff --git a/src/bun.js/api/crypto/CryptoHasher.zig b/src/bun.js/api/crypto/CryptoHasher.zig new file mode 100644 index 0000000000..e723652ac3 --- /dev/null +++ b/src/bun.js/api/crypto/CryptoHasher.zig @@ -0,0 +1,898 @@ +pub const CryptoHasher = union(enum) { + // HMAC_CTX contains 3 EVP_CTX, so let's store it as a pointer. + hmac: ?*HMAC, + + evp: EVP, + zig: CryptoHasherZig, + + const Digest = EVP.Digest; + + pub usingnamespace JSC.Codegen.JSCryptoHasher; + usingnamespace bun.New(@This()); + + // For using only CryptoHasherZig in c++ + pub const Extern = struct { + fn getByName(global: *JSGlobalObject, name_bytes: [*:0]const u8, name_len: usize) callconv(.C) ?*CryptoHasher { + const name = name_bytes[0..name_len]; + + if (CryptoHasherZig.init(name)) |inner| { + return CryptoHasher.new(.{ + .zig = inner, + }); + } + + const algorithm = EVP.Algorithm.map.get(name) orelse { + return null; + }; + + switch (algorithm) { + .ripemd160, + .blake2b256, + .blake2b512, + + .@"sha512-224", + => { + if (algorithm.md()) |md| { + return CryptoHasher.new(.{ + .evp = EVP.init(algorithm, md, global.bunVM().rareData().boringEngine()), + }); + } + }, + else => { + return null; + }, + } + + return null; + } + + fn getFromOther(global: *JSGlobalObject, other_handle: *CryptoHasher) callconv(.C) ?*CryptoHasher { + switch (other_handle.*) { + .zig => |other| { + const hasher = CryptoHasher.new(.{ + .zig = other.copy(), + }); + return hasher; + }, + .evp => |other| { + return CryptoHasher.new(.{ + .evp = other.copy(global.bunVM().rareData().boringEngine()) catch { + return null; + }, + }); + }, + else => { + return null; + }, + } + } + + fn destroy(handle: *CryptoHasher) callconv(.C) void { + handle.finalize(); + } + + fn update(handle: *CryptoHasher, input_bytes: [*]const u8, input_len: usize) callconv(.C) bool { + const input = input_bytes[0..input_len]; + + switch (handle.*) { + .zig => { + handle.zig.update(input); + return true; + }, + .evp => { + handle.evp.update(input); + return true; + }, + else => { + return false; + }, + } + } + + fn digest(handle: *CryptoHasher, global: *JSGlobalObject, buf: [*]u8, buf_len: usize) callconv(.C) u32 { + const digest_buf = buf[0..buf_len]; + switch (handle.*) { + .zig => { + const res = handle.zig.finalWithLen(digest_buf, buf_len); + return @intCast(res.len); + }, + .evp => { + const res = handle.evp.final(global.bunVM().rareData().boringEngine(), digest_buf); + return @intCast(res.len); + }, + else => { + return 0; + }, + } + } + + fn getDigestSize(handle: *CryptoHasher) callconv(.C) u32 { + return switch (handle.*) { + .zig => |inner| inner.digest_length, + .evp => |inner| inner.size(), + else => 0, + }; + } + + pub fn @"export"() void { + @export(&CryptoHasher.Extern.getByName, .{ .name = "Bun__CryptoHasherExtern__getByName" }); + @export(&CryptoHasher.Extern.getFromOther, .{ .name = "Bun__CryptoHasherExtern__getFromOther" }); + @export(&CryptoHasher.Extern.destroy, .{ .name = "Bun__CryptoHasherExtern__destroy" }); + @export(&CryptoHasher.Extern.update, .{ .name = "Bun__CryptoHasherExtern__update" }); + @export(&CryptoHasher.Extern.digest, .{ .name = "Bun__CryptoHasherExtern__digest" }); + @export(&CryptoHasher.Extern.getDigestSize, .{ .name = "Bun__CryptoHasherExtern__getDigestSize" }); + } + }; + + pub const digest = JSC.wrapInstanceMethod(CryptoHasher, "digest_", false); + pub const hash = JSC.wrapStaticMethod(CryptoHasher, "hash_", false); + + fn throwHmacConsumed(globalThis: *JSC.JSGlobalObject) bun.JSError { + return globalThis.throw("HMAC has been consumed and is no longer usable", .{}); + } + + pub fn getByteLength(this: *CryptoHasher, globalThis: *JSC.JSGlobalObject) JSC.JSValue { + return JSC.JSValue.jsNumber(switch (this.*) { + .evp => |*inner| inner.size(), + .hmac => |inner| if (inner) |hmac| hmac.size() else { + throwHmacConsumed(globalThis) catch return .zero; + }, + .zig => |*inner| inner.digest_length, + }); + } + + pub fn getAlgorithm(this: *CryptoHasher, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + return switch (this.*) { + inline .evp, .zig => |*inner| ZigString.fromUTF8(bun.asByteSlice(@tagName(inner.algorithm))).toJS(globalObject), + .hmac => |inner| if (inner) |hmac| ZigString.fromUTF8(bun.asByteSlice(@tagName(hmac.algorithm))).toJS(globalObject) else { + throwHmacConsumed(globalObject) catch return .zero; + }, + }; + } + + pub fn getAlgorithms( + globalThis_: *JSC.JSGlobalObject, + _: JSValue, + _: JSValue, + ) JSC.JSValue { + return bun.String.toJSArray(globalThis_, &EVP.Algorithm.names.values); + } + + fn hashToEncoding(globalThis: *JSGlobalObject, evp: *EVP, input: JSC.Node.BlobOrStringOrBuffer, encoding: JSC.Node.Encoding) bun.JSError!JSC.JSValue { + var output_digest_buf: Digest = undefined; + defer input.deinit(); + + if (input == .blob and input.blob.isBunFile()) { + return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{}); + } + + const len = evp.hash(globalThis.bunVM().rareData().boringEngine(), input.slice(), &output_digest_buf) orelse { + const err = BoringSSL.ERR_get_error(); + const instance = createCryptoError(globalThis, err); + BoringSSL.ERR_clear_error(); + return globalThis.throwValue(instance); + }; + return encoding.encodeWithMaxSize(globalThis, BoringSSL.EVP_MAX_MD_SIZE, output_digest_buf[0..len]); + } + + fn hashToBytes(globalThis: *JSGlobalObject, evp: *EVP, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue { + var output_digest_buf: Digest = undefined; + var output_digest_slice: []u8 = &output_digest_buf; + defer input.deinit(); + + if (input == .blob and input.blob.isBunFile()) { + return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{}); + } + + if (output) |output_buf| { + const size = evp.size(); + var bytes = output_buf.byteSlice(); + if (bytes.len < size) { + return globalThis.throwInvalidArguments("TypedArray must be at least {d} bytes", .{size}); + } + output_digest_slice = bytes[0..size]; + } + + const len = evp.hash(globalThis.bunVM().rareData().boringEngine(), input.slice(), output_digest_slice) orelse { + const err = BoringSSL.ERR_get_error(); + const instance = createCryptoError(globalThis, err); + BoringSSL.ERR_clear_error(); + return globalThis.throwValue(instance); + }; + + if (output) |output_buf| { + return output_buf.value; + } else { + // Clone to GC-managed memory + return JSC.ArrayBuffer.createBuffer(globalThis, output_digest_slice[0..len]); + } + } + + pub fn hash_( + globalThis: *JSGlobalObject, + algorithm: ZigString, + input: JSC.Node.BlobOrStringOrBuffer, + output: ?JSC.Node.StringOrBuffer, + ) bun.JSError!JSC.JSValue { + var evp = EVP.byName(algorithm, globalThis) orelse return try CryptoHasherZig.hashByName(globalThis, algorithm, input, output) orelse { + return globalThis.throwInvalidArguments("Unsupported algorithm \"{any}\"", .{algorithm}); + }; + defer evp.deinit(); + + if (output) |string_or_buffer| { + switch (string_or_buffer) { + inline else => |*str| { + defer str.deinit(); + const encoding = JSC.Node.Encoding.from(str.slice()) orelse { + return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw(); + }; + + return hashToEncoding(globalThis, &evp, input, encoding); + }, + .buffer => |buffer| { + return hashToBytes(globalThis, &evp, input, buffer.buffer); + }, + } + } else { + return hashToBytes(globalThis, &evp, input, null); + } + } + + // Bun.CryptoHasher(algorithm, hmacKey?: string | Buffer) + pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!*CryptoHasher { + const arguments = callframe.arguments_old(2); + if (arguments.len == 0) { + return globalThis.throwInvalidArguments("Expected an algorithm name as an argument", .{}); + } + + const algorithm_name = arguments.ptr[0]; + if (algorithm_name.isEmptyOrUndefinedOrNull() or !algorithm_name.isString()) { + return globalThis.throwInvalidArguments("algorithm must be a string", .{}); + } + + const algorithm = try algorithm_name.getZigString(globalThis); + + if (algorithm.len == 0) { + return globalThis.throwInvalidArguments("Invalid algorithm name", .{}); + } + + const hmac_value = arguments.ptr[1]; + var hmac_key: ?JSC.Node.StringOrBuffer = null; + defer { + if (hmac_key) |*key| { + key.deinit(); + } + } + + if (!hmac_value.isEmptyOrUndefinedOrNull()) { + hmac_key = try JSC.Node.StringOrBuffer.fromJS(globalThis, bun.default_allocator, hmac_value) orelse { + return globalThis.throwInvalidArguments("key must be a string or buffer", .{}); + }; + } + + return CryptoHasher.new(brk: { + if (hmac_key) |*key| { + const chosen_algorithm = try algorithm_name.toEnumFromMap(globalThis, "algorithm", EVP.Algorithm, EVP.Algorithm.map); + if (chosen_algorithm == .ripemd160) { + // crashes at runtime. + return globalThis.throw("ripemd160 is not supported", .{}); + } + + break :brk .{ + .hmac = HMAC.init(chosen_algorithm, key.slice()) orelse { + if (!globalThis.hasException()) { + const err = BoringSSL.ERR_get_error(); + if (err != 0) { + const instance = createCryptoError(globalThis, err); + BoringSSL.ERR_clear_error(); + return globalThis.throwValue(instance); + } else { + return globalThis.throwTODO("HMAC is not supported for this algorithm yet"); + } + } + return error.JSError; + }, + }; + } + + break :brk .{ + .evp = EVP.byName(algorithm, globalThis) orelse return CryptoHasherZig.constructor(algorithm) orelse { + return globalThis.throwInvalidArguments("Unsupported algorithm {any}", .{algorithm}); + }, + }; + }); + } + + pub fn getter( + globalObject: *JSC.JSGlobalObject, + _: *JSC.JSObject, + ) JSC.JSValue { + return CryptoHasher.getConstructor(globalObject); + } + + pub fn update(this: *CryptoHasher, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const thisValue = callframe.this(); + const arguments = callframe.arguments_old(2); + const input = arguments.ptr[0]; + if (input.isEmptyOrUndefinedOrNull()) { + return globalThis.throwInvalidArguments("expected blob, string or buffer", .{}); + } + const encoding = arguments.ptr[1]; + const buffer = try JSC.Node.BlobOrStringOrBuffer.fromJSWithEncodingValue(globalThis, globalThis.bunVM().allocator, input, encoding) orelse { + if (!globalThis.hasException()) return globalThis.throwInvalidArguments("expected blob, string or buffer", .{}); + return error.JSError; + }; + defer buffer.deinit(); + if (buffer == .blob and buffer.blob.isBunFile()) { + return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{}); + } + + switch (this.*) { + .evp => |*inner| { + inner.update(buffer.slice()); + const err = BoringSSL.ERR_get_error(); + if (err != 0) { + const instance = createCryptoError(globalThis, err); + BoringSSL.ERR_clear_error(); + return globalThis.throwValue(instance); + } + }, + .hmac => |inner| { + const hmac = inner orelse { + return throwHmacConsumed(globalThis); + }; + + hmac.update(buffer.slice()); + const err = BoringSSL.ERR_get_error(); + if (err != 0) { + const instance = createCryptoError(globalThis, err); + BoringSSL.ERR_clear_error(); + return globalThis.throwValue(instance); + } + }, + .zig => |*inner| { + inner.update(buffer.slice()); + return thisValue; + }, + } + + return thisValue; + } + + pub fn copy( + this: *CryptoHasher, + globalObject: *JSC.JSGlobalObject, + _: *JSC.CallFrame, + ) bun.JSError!JSC.JSValue { + var new: CryptoHasher = undefined; + switch (this.*) { + .evp => |*inner| { + new = .{ .evp = inner.copy(globalObject.bunVM().rareData().boringEngine()) catch bun.outOfMemory() }; + }, + .hmac => |inner| { + const hmac = inner orelse { + return throwHmacConsumed(globalObject); + }; + new = .{ + .hmac = hmac.copy() catch { + const err = createCryptoError(globalObject, BoringSSL.ERR_get_error()); + BoringSSL.ERR_clear_error(); + return globalObject.throwValue(err); + }, + }; + }, + .zig => |*inner| { + new = .{ .zig = inner.copy() }; + }, + } + return CryptoHasher.new(new).toJS(globalObject); + } + + pub fn digest_(this: *CryptoHasher, globalThis: *JSGlobalObject, output: ?JSC.Node.StringOrBuffer) bun.JSError!JSC.JSValue { + if (output) |string_or_buffer| { + switch (string_or_buffer) { + inline else => |*str| { + defer str.deinit(); + const encoding = JSC.Node.Encoding.from(str.slice()) orelse { + return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw(); + }; + + return this.digestToEncoding(globalThis, encoding); + }, + .buffer => |buffer| { + return this.digestToBytes( + globalThis, + buffer.buffer, + ); + }, + } + } else { + return this.digestToBytes(globalThis, null); + } + } + + fn digestToBytes(this: *CryptoHasher, globalThis: *JSGlobalObject, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue { + var output_digest_buf: EVP.Digest = undefined; + var output_digest_slice: []u8 = &output_digest_buf; + if (output) |output_buf| { + var bytes = output_buf.byteSlice(); + if (bytes.len < output_digest_buf.len) { + return globalThis.throwInvalidArguments(comptime std.fmt.comptimePrint("TypedArray must be at least {d} bytes", .{output_digest_buf.len}), .{}); + } + output_digest_slice = bytes[0..bytes.len]; + } else { + output_digest_buf = std.mem.zeroes(EVP.Digest); + } + + const result = this.final(globalThis, output_digest_slice) catch return .zero; + if (globalThis.hasException()) { + return error.JSError; + } + + if (output) |output_buf| { + return output_buf.value; + } else { + // Clone to GC-managed memory + return JSC.ArrayBuffer.createBuffer(globalThis, result); + } + } + + fn digestToEncoding(this: *CryptoHasher, globalThis: *JSGlobalObject, encoding: JSC.Node.Encoding) bun.JSError!JSC.JSValue { + var output_digest_buf: EVP.Digest = std.mem.zeroes(EVP.Digest); + const output_digest_slice: []u8 = &output_digest_buf; + const out = this.final(globalThis, output_digest_slice) catch return .zero; + if (globalThis.hasException()) { + return error.JSError; + } + return encoding.encodeWithMaxSize(globalThis, BoringSSL.EVP_MAX_MD_SIZE, out); + } + + fn final(this: *CryptoHasher, globalThis: *JSGlobalObject, output_digest_slice: []u8) bun.JSError![]u8 { + return switch (this.*) { + .hmac => |inner| brk: { + const hmac: *HMAC = inner orelse { + return throwHmacConsumed(globalThis); + }; + this.hmac = null; + defer hmac.deinit(); + break :brk hmac.final(output_digest_slice); + }, + .evp => |*inner| inner.final(globalThis.bunVM().rareData().boringEngine(), output_digest_slice), + .zig => |*inner| inner.final(output_digest_slice), + }; + } + + pub fn finalize(this: *CryptoHasher) void { + switch (this.*) { + .evp => |*inner| { + // https://github.com/oven-sh/bun/issues/3250 + inner.deinit(); + }, + .zig => |*inner| { + inner.deinit(); + }, + .hmac => |inner| { + if (inner) |hmac| { + hmac.deinit(); + } + }, + } + this.destroy(); + } +}; + +const CryptoHasherZig = struct { + algorithm: EVP.Algorithm, + state: *anyopaque, + digest_length: u8, + + const algo_map = [_]struct { string, type }{ + .{ "sha3-224", std.crypto.hash.sha3.Sha3_224 }, + .{ "sha3-256", std.crypto.hash.sha3.Sha3_256 }, + .{ "sha3-384", std.crypto.hash.sha3.Sha3_384 }, + .{ "sha3-512", std.crypto.hash.sha3.Sha3_512 }, + .{ "shake128", std.crypto.hash.sha3.Shake128 }, + .{ "shake256", std.crypto.hash.sha3.Shake256 }, + }; + + inline fn digestLength(Algorithm: type) comptime_int { + return switch (Algorithm) { + std.crypto.hash.sha3.Shake128 => 16, + std.crypto.hash.sha3.Shake256 => 32, + else => Algorithm.digest_length, + }; + } + + pub fn hashByName(globalThis: *JSGlobalObject, algorithm: ZigString, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.Node.StringOrBuffer) bun.JSError!?JSC.JSValue { + inline for (algo_map) |item| { + if (bun.strings.eqlComptime(algorithm.slice(), item[0])) { + return try hashByNameInner(globalThis, item[1], input, output); + } + } + return null; + } + + fn hashByNameInner(globalThis: *JSGlobalObject, comptime Algorithm: type, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.Node.StringOrBuffer) bun.JSError!JSC.JSValue { + if (output) |string_or_buffer| { + switch (string_or_buffer) { + inline else => |*str| { + defer str.deinit(); + const encoding = JSC.Node.Encoding.from(str.slice()) orelse { + return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw(); + }; + + if (encoding == .buffer) { + return hashByNameInnerToBytes(globalThis, Algorithm, input, null); + } + + return hashByNameInnerToString(globalThis, Algorithm, input, encoding); + }, + .buffer => |buffer| { + return hashByNameInnerToBytes(globalThis, Algorithm, input, buffer.buffer); + }, + } + } + return hashByNameInnerToBytes(globalThis, Algorithm, input, null); + } + + fn hashByNameInnerToString(globalThis: *JSGlobalObject, comptime Algorithm: type, input: JSC.Node.BlobOrStringOrBuffer, encoding: JSC.Node.Encoding) bun.JSError!JSC.JSValue { + defer input.deinit(); + + if (input == .blob and input.blob.isBunFile()) { + return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{}); + } + + var h = Algorithm.init(.{}); + h.update(input.slice()); + + var out: [digestLength(Algorithm)]u8 = undefined; + h.final(&out); + + return encoding.encodeWithSize(globalThis, digestLength(Algorithm), &out); + } + + fn hashByNameInnerToBytes(globalThis: *JSGlobalObject, comptime Algorithm: type, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue { + defer input.deinit(); + + if (input == .blob and input.blob.isBunFile()) { + return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{}); + } + + var h = Algorithm.init(.{}); + const digest_length_comptime = digestLength(Algorithm); + + if (output) |output_buf| { + if (output_buf.byteSlice().len < digest_length_comptime) { + return globalThis.throwInvalidArguments("TypedArray must be at least {d} bytes", .{digest_length_comptime}); + } + } + + h.update(input.slice()); + + if (output) |output_buf| { + h.final(output_buf.slice()[0..digest_length_comptime]); + return output_buf.value; + } else { + var out: [digestLength(Algorithm)]u8 = undefined; + h.final(&out); + // Clone to GC-managed memory + return JSC.ArrayBuffer.createBuffer(globalThis, &out); + } + } + + fn constructor(algorithm: ZigString) ?*CryptoHasher { + inline for (algo_map) |item| { + if (bun.strings.eqlComptime(algorithm.slice(), item[0])) { + return CryptoHasher.new(.{ .zig = .{ + .algorithm = @field(EVP.Algorithm, item[0]), + .state = bun.new(item[1], item[1].init(.{})), + .digest_length = digestLength(item[1]), + } }); + } + } + return null; + } + + pub fn init(algorithm: []const u8) ?CryptoHasherZig { + inline for (algo_map) |item| { + const name, const T = item; + if (bun.strings.eqlComptime(algorithm, name)) { + const handle: CryptoHasherZig = .{ + .algorithm = @field(EVP.Algorithm, name), + .state = bun.new(T, T.init(.{})), + .digest_length = digestLength(T), + }; + + return handle; + } + } + return null; + } + + fn update(self: *CryptoHasherZig, bytes: []const u8) void { + inline for (algo_map) |item| { + if (self.algorithm == @field(EVP.Algorithm, item[0])) { + return item[1].update(@ptrCast(@alignCast(self.state)), bytes); + } + } + @panic("unreachable"); + } + + fn copy(self: *const CryptoHasherZig) CryptoHasherZig { + inline for (algo_map) |item| { + if (self.algorithm == @field(EVP.Algorithm, item[0])) { + return .{ + .algorithm = self.algorithm, + .state = bun.dupe(item[1], @ptrCast(@alignCast(self.state))), + .digest_length = self.digest_length, + }; + } + } + @panic("unreachable"); + } + + fn finalWithLen(self: *CryptoHasherZig, output_digest_slice: []u8, res_len: usize) []u8 { + inline for (algo_map) |pair| { + const name, const T = pair; + if (self.algorithm == @field(EVP.Algorithm, name)) { + T.final(@ptrCast(@alignCast(self.state)), @ptrCast(output_digest_slice)); + const reset: *T = @ptrCast(@alignCast(self.state)); + reset.* = T.init(.{}); + return output_digest_slice[0..res_len]; + } + } + @panic("unreachable"); + } + + fn final(self: *CryptoHasherZig, output_digest_slice: []u8) []u8 { + return self.finalWithLen(output_digest_slice, self.digest_length); + } + + fn deinit(self: *CryptoHasherZig) void { + inline for (algo_map) |item| { + if (self.algorithm == @field(EVP.Algorithm, item[0])) { + return bun.destroy(@as(*item[1], @ptrCast(@alignCast(self.state)))); + } + } + @panic("unreachable"); + } +}; + +fn StaticCryptoHasher(comptime Hasher: type, comptime name: [:0]const u8) type { + return struct { + hashing: Hasher = Hasher{}, + digested: bool = false, + + const ThisHasher = @This(); + + pub usingnamespace @field(JSC.Codegen, "JS" ++ name); + + pub const digest = JSC.wrapInstanceMethod(ThisHasher, "digest_", false); + pub const hash = JSC.wrapStaticMethod(ThisHasher, "hash_", false); + + pub fn getByteLength( + _: *@This(), + _: *JSC.JSGlobalObject, + ) JSC.JSValue { + return JSC.JSValue.jsNumber(@as(u16, Hasher.digest)); + } + + pub fn getByteLengthStatic( + _: *JSC.JSGlobalObject, + _: JSValue, + _: JSValue, + ) JSC.JSValue { + return JSC.JSValue.jsNumber(@as(u16, Hasher.digest)); + } + + fn hashToEncoding(globalThis: *JSGlobalObject, input: JSC.Node.BlobOrStringOrBuffer, encoding: JSC.Node.Encoding) bun.JSError!JSC.JSValue { + var output_digest_buf: Hasher.Digest = undefined; + + if (input == .blob and input.blob.isBunFile()) { + return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{}); + } + + if (comptime @typeInfo(@TypeOf(Hasher.hash)).@"fn".params.len == 3) { + Hasher.hash(input.slice(), &output_digest_buf, JSC.VirtualMachine.get().rareData().boringEngine()); + } else { + Hasher.hash(input.slice(), &output_digest_buf); + } + + return encoding.encodeWithSize(globalThis, Hasher.digest, &output_digest_buf); + } + + fn hashToBytes(globalThis: *JSGlobalObject, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue { + var output_digest_buf: Hasher.Digest = undefined; + var output_digest_slice: *Hasher.Digest = &output_digest_buf; + if (output) |output_buf| { + var bytes = output_buf.byteSlice(); + if (bytes.len < Hasher.digest) { + return globalThis.throwInvalidArguments(comptime std.fmt.comptimePrint("TypedArray must be at least {d} bytes", .{Hasher.digest}), .{}); + } + output_digest_slice = bytes[0..Hasher.digest]; + } + + if (comptime @typeInfo(@TypeOf(Hasher.hash)).@"fn".params.len == 3) { + Hasher.hash(input.slice(), output_digest_slice, JSC.VirtualMachine.get().rareData().boringEngine()); + } else { + Hasher.hash(input.slice(), output_digest_slice); + } + + if (output) |output_buf| { + return output_buf.value; + } else { + var array_buffer_out = JSC.ArrayBuffer.fromBytes(bun.default_allocator.dupe(u8, output_digest_slice) catch unreachable, .Uint8Array); + return array_buffer_out.toJSUnchecked(globalThis, null); + } + } + + pub fn hash_( + globalThis: *JSGlobalObject, + input: JSC.Node.BlobOrStringOrBuffer, + output: ?JSC.Node.StringOrBuffer, + ) bun.JSError!JSC.JSValue { + defer input.deinit(); + + if (input == .blob and input.blob.isBunFile()) { + return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{}); + } + + if (output) |string_or_buffer| { + switch (string_or_buffer) { + inline else => |*str| { + defer str.deinit(); + const encoding = JSC.Node.Encoding.from(str.slice()) orelse { + return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw(); + }; + + return hashToEncoding(globalThis, input, encoding); + }, + .buffer => |buffer| { + return hashToBytes(globalThis, input, buffer.buffer); + }, + } + } else { + return hashToBytes(globalThis, input, null); + } + } + + pub fn constructor(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!*@This() { + const this = try bun.default_allocator.create(@This()); + this.* = .{ .hashing = Hasher.init() }; + return this; + } + + pub fn getter( + globalObject: *JSC.JSGlobalObject, + _: *JSC.JSObject, + ) JSC.JSValue { + return ThisHasher.getConstructor(globalObject); + } + + pub fn update(this: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + if (this.digested) { + return globalThis.ERR_INVALID_STATE(name ++ " hasher already digested, create a new instance to update", .{}).throw(); + } + const thisValue = callframe.this(); + const input = callframe.argument(0); + const buffer = try JSC.Node.BlobOrStringOrBuffer.fromJS(globalThis, globalThis.bunVM().allocator, input) orelse { + return globalThis.throwInvalidArguments("expected blob or string or buffer", .{}); + }; + defer buffer.deinit(); + + if (buffer == .blob and buffer.blob.isBunFile()) { + return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{}); + } + this.hashing.update(buffer.slice()); + return thisValue; + } + + pub fn digest_( + this: *@This(), + globalThis: *JSGlobalObject, + output: ?JSC.Node.StringOrBuffer, + ) bun.JSError!JSC.JSValue { + if (this.digested) { + return globalThis.ERR_INVALID_STATE(name ++ " hasher already digested, create a new instance to digest again", .{}).throw(); + } + if (output) |*string_or_buffer| { + switch (string_or_buffer.*) { + inline else => |*str| { + defer str.deinit(); + const encoding = JSC.Node.Encoding.from(str.slice()) orelse { + return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw(); + }; + + return this.digestToEncoding(globalThis, encoding); + }, + .buffer => |*buffer| { + return this.digestToBytes( + globalThis, + buffer.buffer, + ); + }, + } + } else { + return this.digestToBytes(globalThis, null); + } + } + + fn digestToBytes(this: *@This(), globalThis: *JSGlobalObject, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue { + var output_digest_buf: Hasher.Digest = undefined; + var output_digest_slice: *Hasher.Digest = &output_digest_buf; + if (output) |output_buf| { + var bytes = output_buf.byteSlice(); + if (bytes.len < Hasher.digest) { + return globalThis.throwInvalidArguments(comptime std.fmt.comptimePrint("TypedArray must be at least {d} bytes", .{Hasher.digest}), .{}); + } + output_digest_slice = bytes[0..Hasher.digest]; + } else { + output_digest_buf = std.mem.zeroes(Hasher.Digest); + } + + this.hashing.final(output_digest_slice); + this.digested = true; + + if (output) |output_buf| { + return output_buf.value; + } else { + var array_buffer_out = JSC.ArrayBuffer.fromBytes(bun.default_allocator.dupe(u8, &output_digest_buf) catch unreachable, .Uint8Array); + return array_buffer_out.toJSUnchecked(globalThis, null); + } + } + + fn digestToEncoding(this: *@This(), globalThis: *JSGlobalObject, encoding: JSC.Node.Encoding) JSC.JSValue { + var output_digest_buf: Hasher.Digest = comptime brk: { + var bytes: Hasher.Digest = undefined; + var i: usize = 0; + while (i < Hasher.digest) { + bytes[i] = 0; + i += 1; + } + break :brk bytes; + }; + + const output_digest_slice: *Hasher.Digest = &output_digest_buf; + + this.hashing.final(output_digest_slice); + this.digested = true; + + return encoding.encodeWithSize(globalThis, Hasher.digest, output_digest_slice); + } + + pub fn finalize(this: *@This()) void { + VirtualMachine.get().allocator.destroy(this); + } + }; +} + +pub const MD4 = StaticCryptoHasher(Hashers.MD4, "MD4"); +pub const MD5 = StaticCryptoHasher(Hashers.MD5, "MD5"); +pub const SHA1 = StaticCryptoHasher(Hashers.SHA1, "SHA1"); +pub const SHA224 = StaticCryptoHasher(Hashers.SHA224, "SHA224"); +pub const SHA256 = StaticCryptoHasher(Hashers.SHA256, "SHA256"); +pub const SHA384 = StaticCryptoHasher(Hashers.SHA384, "SHA384"); +pub const SHA512 = StaticCryptoHasher(Hashers.SHA512, "SHA512"); +pub const SHA512_256 = StaticCryptoHasher(Hashers.SHA512_256, "SHA512_256"); +const Crypto = JSC.API.Bun.Crypto; +const Hashers = @import("../../../sha.zig"); + +const std = @import("std"); +const bun = @import("root").bun; +const string = bun.string; +const strings = bun.strings; +const MutableString = bun.MutableString; +const stringZ = bun.stringZ; +const default_allocator = bun.default_allocator; +const JSC = bun.JSC; +const Async = bun.Async; +const ZigString = JSC.ZigString; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const CallFrame = JSC.CallFrame; +const assert = bun.assert; +const HMAC = Crypto.HMAC; +const EVP = Crypto.EVP; +const BoringSSL = bun.BoringSSL.c; +const createCryptoError = Crypto.createCryptoError; +const VirtualMachine = JSC.VirtualMachine; diff --git a/src/bun.js/api/crypto/EVP.zig b/src/bun.js/api/crypto/EVP.zig new file mode 100644 index 0000000000..1e8e4a1cee --- /dev/null +++ b/src/bun.js/api/crypto/EVP.zig @@ -0,0 +1,221 @@ +ctx: BoringSSL.EVP_MD_CTX = undefined, +md: *const BoringSSL.EVP_MD = undefined, +algorithm: Algorithm, + +// we do this to avoid asking BoringSSL what the digest name is +// because that API is confusing +pub const Algorithm = enum { + // @"DSA-SHA", + // @"DSA-SHA1", + // @"MD5-SHA1", + // @"RSA-MD5", + // @"RSA-RIPEMD160", + // @"RSA-SHA1", + // @"RSA-SHA1-2", + // @"RSA-SHA224", + // @"RSA-SHA256", + // @"RSA-SHA384", + // @"RSA-SHA512", + // @"ecdsa-with-SHA1", + blake2b256, + blake2b512, + md4, + md5, + ripemd160, + sha1, + sha224, + sha256, + sha384, + sha512, + @"sha512-224", + @"sha512-256", + + @"sha3-224", + @"sha3-256", + @"sha3-384", + @"sha3-512", + shake128, + shake256, + + pub fn md(this: Algorithm) ?*const BoringSSL.EVP_MD { + return switch (this) { + .blake2b256 => BoringSSL.EVP_blake2b256(), + .blake2b512 => BoringSSL.EVP_blake2b512(), + .md4 => BoringSSL.EVP_md4(), + .md5 => BoringSSL.EVP_md5(), + .ripemd160 => BoringSSL.EVP_ripemd160(), + .sha1 => BoringSSL.EVP_sha1(), + .sha224 => BoringSSL.EVP_sha224(), + .sha256 => BoringSSL.EVP_sha256(), + .sha384 => BoringSSL.EVP_sha384(), + .sha512 => BoringSSL.EVP_sha512(), + .@"sha512-224" => BoringSSL.EVP_sha512_224(), + .@"sha512-256" => BoringSSL.EVP_sha512_256(), + else => null, + }; + } + + pub const names: std.EnumArray(Algorithm, bun.String) = brk: { + var all = std.EnumArray(Algorithm, bun.String).initUndefined(); + var iter = all.iterator(); + while (iter.next()) |entry| { + entry.value.* = bun.String.init(@tagName(entry.key)); + } + break :brk all; + }; + + pub const map = bun.ComptimeStringMap(Algorithm, .{ + .{ "blake2b256", .blake2b256 }, + .{ "blake2b512", .blake2b512 }, + .{ "ripemd160", .ripemd160 }, + .{ "rmd160", .ripemd160 }, + .{ "md4", .md4 }, + .{ "md5", .md5 }, + .{ "sha1", .sha1 }, + .{ "sha128", .sha1 }, + .{ "sha224", .sha224 }, + .{ "sha256", .sha256 }, + .{ "sha384", .sha384 }, + .{ "sha512", .sha512 }, + .{ "sha-1", .sha1 }, + .{ "sha-224", .sha224 }, + .{ "sha-256", .sha256 }, + .{ "sha-384", .sha384 }, + .{ "sha-512", .sha512 }, + .{ "sha-512/224", .@"sha512-224" }, + .{ "sha-512_224", .@"sha512-224" }, + .{ "sha-512224", .@"sha512-224" }, + .{ "sha512-224", .@"sha512-224" }, + .{ "sha-512/256", .@"sha512-256" }, + .{ "sha-512_256", .@"sha512-256" }, + .{ "sha-512256", .@"sha512-256" }, + .{ "sha512-256", .@"sha512-256" }, + .{ "sha384", .sha384 }, + .{ "sha3-224", .@"sha3-224" }, + .{ "sha3-256", .@"sha3-256" }, + .{ "sha3-384", .@"sha3-384" }, + .{ "sha3-512", .@"sha3-512" }, + .{ "shake128", .shake128 }, + .{ "shake256", .shake256 }, + // .{ "md5-sha1", .@"MD5-SHA1" }, + // .{ "dsa-sha", .@"DSA-SHA" }, + // .{ "dsa-sha1", .@"DSA-SHA1" }, + // .{ "ecdsa-with-sha1", .@"ecdsa-with-SHA1" }, + // .{ "rsa-md5", .@"RSA-MD5" }, + // .{ "rsa-sha1", .@"RSA-SHA1" }, + // .{ "rsa-sha1-2", .@"RSA-SHA1-2" }, + // .{ "rsa-sha224", .@"RSA-SHA224" }, + // .{ "rsa-sha256", .@"RSA-SHA256" }, + // .{ "rsa-sha384", .@"RSA-SHA384" }, + // .{ "rsa-sha512", .@"RSA-SHA512" }, + // .{ "rsa-ripemd160", .@"RSA-RIPEMD160" }, + }); +}; + +pub fn init(algorithm: Algorithm, md: *const BoringSSL.EVP_MD, engine: *BoringSSL.ENGINE) EVP { + bun.BoringSSL.load(); + + var ctx: BoringSSL.EVP_MD_CTX = undefined; + BoringSSL.EVP_MD_CTX_init(&ctx); + _ = BoringSSL.EVP_DigestInit_ex(&ctx, md, engine); + return .{ + .ctx = ctx, + .md = md, + .algorithm = algorithm, + }; +} + +pub fn reset(this: *EVP, engine: *BoringSSL.ENGINE) void { + BoringSSL.ERR_clear_error(); + _ = BoringSSL.EVP_DigestInit_ex(&this.ctx, this.md, engine); +} + +pub fn hash(this: *EVP, engine: *BoringSSL.ENGINE, input: []const u8, output: []u8) ?u32 { + BoringSSL.ERR_clear_error(); + var outsize: c_uint = @min(@as(u16, @truncate(output.len)), this.size()); + if (BoringSSL.EVP_Digest(input.ptr, input.len, output.ptr, &outsize, this.md, engine) != 1) { + return null; + } + + return outsize; +} + +pub fn final(this: *EVP, engine: *BoringSSL.ENGINE, output: []u8) []u8 { + BoringSSL.ERR_clear_error(); + var outsize: u32 = @min(@as(u16, @truncate(output.len)), this.size()); + if (BoringSSL.EVP_DigestFinal_ex( + &this.ctx, + output.ptr, + &outsize, + ) != 1) { + return ""; + } + + this.reset(engine); + + return output[0..outsize]; +} + +pub fn update(this: *EVP, input: []const u8) void { + BoringSSL.ERR_clear_error(); + _ = BoringSSL.EVP_DigestUpdate(&this.ctx, input.ptr, input.len); +} + +pub fn size(this: *const EVP) u16 { + return @as(u16, @truncate(BoringSSL.EVP_MD_CTX_size(&this.ctx))); +} + +pub fn copy(this: *const EVP, engine: *BoringSSL.ENGINE) error{OutOfMemory}!EVP { + BoringSSL.ERR_clear_error(); + var new = init(this.algorithm, this.md, engine); + if (BoringSSL.EVP_MD_CTX_copy_ex(&new.ctx, &this.ctx) == 0) { + return error.OutOfMemory; + } + return new; +} + +pub fn byNameAndEngine(engine: *BoringSSL.ENGINE, name: []const u8) ?EVP { + if (Algorithm.map.getWithEql(name, strings.eqlCaseInsensitiveASCIIIgnoreLength)) |algorithm| { + if (algorithm.md()) |md| { + return EVP.init(algorithm, md, engine); + } + + if (BoringSSL.EVP_get_digestbyname(@tagName(algorithm))) |md| { + return EVP.init(algorithm, md, engine); + } + } + + return null; +} + +pub fn byName(name: ZigString, global: *JSC.JSGlobalObject) ?EVP { + var name_str = name.toSlice(global.allocator()); + defer name_str.deinit(); + return byNameAndEngine(global.bunVM().rareData().boringEngine(), name_str.slice()); +} + +pub fn deinit(this: *EVP) void { + // https://github.com/oven-sh/bun/issues/3250 + _ = BoringSSL.EVP_MD_CTX_cleanup(&this.ctx); +} + +pub const Digest = [BoringSSL.EVP_MAX_MD_SIZE]u8; +pub const PBKDF2 = @import("./PBKDF2.zig"); +pub const pbkdf2 = PBKDF2.pbkdf2; + +const std = @import("std"); +const bun = @import("root").bun; +const string = bun.string; +const strings = bun.strings; +const MutableString = bun.MutableString; +const stringZ = bun.stringZ; +const default_allocator = bun.default_allocator; +const JSC = bun.JSC; +const Async = bun.Async; +const ZigString = JSC.ZigString; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const CallFrame = JSC.CallFrame; +const assert = bun.assert; +const EVP = @This(); +const BoringSSL = bun.BoringSSL.c; diff --git a/src/bun.js/api/crypto/HMAC.zig b/src/bun.js/api/crypto/HMAC.zig new file mode 100644 index 0000000000..7fe2ebb0fc --- /dev/null +++ b/src/bun.js/api/crypto/HMAC.zig @@ -0,0 +1,56 @@ +ctx: BoringSSL.HMAC_CTX, +algorithm: EVP.Algorithm, + +pub usingnamespace bun.New(@This()); + +pub fn init(algorithm: EVP.Algorithm, key: []const u8) ?*HMAC { + const md = algorithm.md() orelse return null; + var ctx: BoringSSL.HMAC_CTX = undefined; + BoringSSL.HMAC_CTX_init(&ctx); + if (BoringSSL.HMAC_Init_ex(&ctx, key.ptr, @intCast(key.len), md, null) != 1) { + BoringSSL.HMAC_CTX_cleanup(&ctx); + return null; + } + return HMAC.new(.{ + .ctx = ctx, + .algorithm = algorithm, + }); +} + +pub fn update(this: *HMAC, data: []const u8) void { + _ = BoringSSL.HMAC_Update(&this.ctx, data.ptr, data.len); +} + +pub fn size(this: *const HMAC) usize { + return BoringSSL.HMAC_size(&this.ctx); +} + +pub fn copy(this: *HMAC) !*HMAC { + var ctx: BoringSSL.HMAC_CTX = undefined; + BoringSSL.HMAC_CTX_init(&ctx); + if (BoringSSL.HMAC_CTX_copy(&ctx, &this.ctx) != 1) { + BoringSSL.HMAC_CTX_cleanup(&ctx); + return error.BoringSSLError; + } + return HMAC.new(.{ + .ctx = ctx, + .algorithm = this.algorithm, + }); +} + +pub fn final(this: *HMAC, out: []u8) []u8 { + var outlen: c_uint = undefined; + _ = BoringSSL.HMAC_Final(&this.ctx, out.ptr, &outlen); + return out[0..outlen]; +} + +pub fn deinit(this: *HMAC) void { + BoringSSL.HMAC_CTX_cleanup(&this.ctx); + this.destroy(); +} + +const bun = @import("root").bun; +const BoringSSL = bun.BoringSSL.c; +const JSC = bun.JSC; +const EVP = JSC.API.Bun.Crypto.EVP; +const HMAC = @This(); diff --git a/src/bun.js/api/crypto/PBKDF2.zig b/src/bun.js/api/crypto/PBKDF2.zig new file mode 100644 index 0000000000..60eef0784c --- /dev/null +++ b/src/bun.js/api/crypto/PBKDF2.zig @@ -0,0 +1,265 @@ +password: JSC.Node.StringOrBuffer = JSC.Node.StringOrBuffer.empty, +salt: JSC.Node.StringOrBuffer = JSC.Node.StringOrBuffer.empty, +iteration_count: u32 = 1, +length: i32 = 0, +algorithm: EVP.Algorithm, + +pub fn run(this: *PBKDF2, output: []u8) bool { + const password = this.password.slice(); + const salt = this.salt.slice(); + const algorithm = this.algorithm; + const iteration_count = this.iteration_count; + const length = this.length; + + @memset(output, 0); + assert(this.length <= @as(i32, @intCast(output.len))); + BoringSSL.ERR_clear_error(); + const rc = BoringSSL.PKCS5_PBKDF2_HMAC( + if (password.len > 0) password.ptr else null, + @intCast(password.len), + salt.ptr, + @intCast(salt.len), + @intCast(iteration_count), + algorithm.md().?, + @intCast(length), + output.ptr, + ); + + if (rc <= 0) { + return false; + } + + return true; +} + +pub const Job = struct { + pbkdf2: PBKDF2, + output: []u8 = &[_]u8{}, + task: JSC.WorkPoolTask = .{ .callback = &runTask }, + promise: JSC.JSPromise.Strong = .{}, + vm: *JSC.VirtualMachine, + err: ?u32 = null, + any_task: JSC.AnyTask = undefined, + poll: Async.KeepAlive = .{}, + + pub usingnamespace bun.New(@This()); + + pub fn runTask(task: *JSC.WorkPoolTask) void { + const job: *PBKDF2.Job = @fieldParentPtr("task", task); + defer job.vm.enqueueTaskConcurrent(JSC.ConcurrentTask.create(job.any_task.task())); + job.output = bun.default_allocator.alloc(u8, @as(usize, @intCast(job.pbkdf2.length))) catch { + job.err = BoringSSL.EVP_R_MEMORY_LIMIT_EXCEEDED; + return; + }; + if (!job.pbkdf2.run(job.output)) { + job.err = BoringSSL.ERR_get_error(); + BoringSSL.ERR_clear_error(); + + bun.default_allocator.free(job.output); + job.output = &[_]u8{}; + } + } + + pub fn runFromJS(this: *Job) void { + defer this.deinit(); + if (this.vm.isShuttingDown()) { + return; + } + + const globalThis = this.vm.global; + const promise = this.promise.swap(); + if (this.err) |err| { + promise.reject(globalThis, createCryptoError(globalThis, err)); + return; + } + + const output_slice = this.output; + assert(output_slice.len == @as(usize, @intCast(this.pbkdf2.length))); + const buffer_value = JSC.JSValue.createBuffer(globalThis, output_slice, bun.default_allocator); + if (buffer_value == .zero) { + promise.reject(globalThis, ZigString.init("Failed to create buffer").toErrorInstance(globalThis)); + return; + } + + this.output = &[_]u8{}; + promise.resolve(globalThis, buffer_value); + } + + pub fn deinit(this: *Job) void { + this.poll.unref(this.vm); + this.pbkdf2.deinitAndUnprotect(); + this.promise.deinit(); + bun.default_allocator.free(this.output); + this.destroy(); + } + + pub fn create(vm: *JSC.VirtualMachine, globalThis: *JSC.JSGlobalObject, data: *const PBKDF2) *Job { + var job = Job.new(.{ + .pbkdf2 = data.*, + .vm = vm, + .any_task = undefined, + }); + + job.promise = JSC.JSPromise.Strong.init(globalThis); + job.any_task = JSC.AnyTask.New(@This(), &runFromJS).init(job); + job.poll.ref(vm); + JSC.WorkPool.schedule(&job.task); + + return job; + } +}; + +pub fn deinitAndUnprotect(this: *PBKDF2) void { + this.password.deinitAndUnprotect(); + this.salt.deinitAndUnprotect(); +} + +pub fn deinit(this: *PBKDF2) void { + this.password.deinit(); + this.salt.deinit(); +} + +pub fn fromJS(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame, is_async: bool) bun.JSError!PBKDF2 { + const arg0, const arg1, const arg2, const arg3, const arg4, const arg5 = callFrame.argumentsAsArray(6); + + if (!arg3.isNumber()) { + return globalThis.throwInvalidArgumentTypeValue("keylen", "number", arg3); + } + + const keylen_num = arg3.asNumber(); + + if (std.math.isInf(keylen_num) or std.math.isNan(keylen_num)) { + return globalThis.throwRangeError(keylen_num, .{ + .field_name = "keylen", + .msg = "an integer", + }); + } + + if (keylen_num < 0 or keylen_num > std.math.maxInt(i32)) { + return globalThis.throwRangeError(keylen_num, .{ .field_name = "keylen", .min = 0, .max = std.math.maxInt(i32) }); + } + + const keylen: i32 = @intFromFloat(keylen_num); + + if (globalThis.hasException()) { + return error.JSError; + } + + if (!arg2.isAnyInt()) { + return globalThis.throwInvalidArgumentTypeValue("iterations", "number", arg2); + } + + const iteration_count = arg2.coerce(i64, globalThis); + + if (!globalThis.hasException() and (iteration_count < 1 or iteration_count > std.math.maxInt(i32))) { + return globalThis.throwRangeError(iteration_count, .{ .field_name = "iterations", .min = 1, .max = std.math.maxInt(i32) + 1 }); + } + + if (globalThis.hasException()) { + return error.JSError; + } + + const algorithm = brk: { + if (!arg4.isString()) { + return globalThis.throwInvalidArgumentTypeValue("digest", "string", arg4); + } + + invalid: { + switch (try EVP.Algorithm.map.fromJSCaseInsensitive(globalThis, arg4) orelse break :invalid) { + .shake128, .shake256, .@"sha3-224", .@"sha3-256", .@"sha3-384", .@"sha3-512" => break :invalid, + else => |alg| break :brk alg, + } + } + + if (!globalThis.hasException()) { + const slice = try arg4.toSlice(globalThis, bun.default_allocator); + defer slice.deinit(); + const name = slice.slice(); + return globalThis.ERR_CRYPTO_INVALID_DIGEST("Invalid digest: {s}", .{name}).throw(); + } + return error.JSError; + }; + + var out = PBKDF2{ + .iteration_count = @intCast(iteration_count), + .length = keylen, + .algorithm = algorithm, + }; + defer { + if (globalThis.hasException()) { + if (is_async) + out.deinitAndUnprotect() + else + out.deinit(); + } + } + + const allow_string_object = true; + out.salt = try JSC.Node.StringOrBuffer.fromJSMaybeAsync(globalThis, bun.default_allocator, arg1, is_async, allow_string_object) orelse { + return globalThis.throwInvalidArgumentTypeValue("salt", "string or buffer", arg1); + }; + + if (out.salt.slice().len > std.math.maxInt(i32)) { + return globalThis.throwInvalidArguments("salt is too long", .{}); + } + + out.password = try JSC.Node.StringOrBuffer.fromJSMaybeAsync(globalThis, bun.default_allocator, arg0, is_async, allow_string_object) orelse { + return globalThis.throwInvalidArgumentTypeValue("password", "string or buffer", arg0); + }; + + if (out.password.slice().len > std.math.maxInt(i32)) { + return globalThis.throwInvalidArguments("password is too long", .{}); + } + + if (is_async) { + if (!arg5.isFunction()) { + return globalThis.throwInvalidArgumentTypeValue("callback", "function", arg5); + } + } + + return out; +} + +/// For usage in Zig +pub fn pbkdf2( + output: []u8, + password: []const u8, + salt: []const u8, + iteration_count: u32, + algorithm: Algorithm, +) ?[]const u8 { + var pbk = PBKDF2{ + .algorithm = algorithm, + .password = JSC.Node.StringOrBuffer{ .encoded_slice = JSC.ZigString.Slice.fromUTF8NeverFree(password) }, + .salt = JSC.Node.StringOrBuffer{ .encoded_slice = JSC.ZigString.Slice.fromUTF8NeverFree(salt) }, + .iteration_count = iteration_count, + .length = @intCast(output.len), + }; + + if (!pbk.run(output)) { + return null; + } + + return output; +} + +const std = @import("std"); +const bun = @import("root").bun; +const string = bun.string; +const strings = bun.strings; +const MutableString = bun.MutableString; +const stringZ = bun.stringZ; +const default_allocator = bun.default_allocator; +const JSC = bun.JSC; +const Async = bun.Async; +const ZigString = JSC.ZigString; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const CallFrame = JSC.CallFrame; +const assert = bun.assert; +const EVP = JSC.API.Bun.Crypto.EVP; +const Algorithm = EVP.Algorithm; +const BoringSSL = bun.BoringSSL.c; +const createCryptoError = JSC.API.Bun.Crypto.createCryptoError; +const VirtualMachine = JSC.VirtualMachine; +const PBKDF2 = @This(); diff --git a/src/bun.js/api/crypto/PasswordObject.zig b/src/bun.js/api/crypto/PasswordObject.zig new file mode 100644 index 0000000000..ac371f3793 --- /dev/null +++ b/src/bun.js/api/crypto/PasswordObject.zig @@ -0,0 +1,772 @@ +pub const PasswordObject = struct { + pub const pwhash = std.crypto.pwhash; + pub const Algorithm = enum { + argon2i, + argon2d, + argon2id, + bcrypt, + + pub const Value = union(Algorithm) { + argon2i: Argon2Params, + argon2d: Argon2Params, + argon2id: Argon2Params, + // bcrypt only accepts "cost" + bcrypt: u6, + + pub const bcrpyt_default = 10; + + pub const default = Algorithm.Value{ + .argon2id = .{}, + }; + + pub fn fromJS(globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) bun.JSError!Value { + if (value.isObject()) { + if (try value.getTruthy(globalObject, "algorithm")) |algorithm_value| { + if (!algorithm_value.isString()) { + return globalObject.throwInvalidArgumentType("hash", "algorithm", "string"); + } + + const algorithm_string = try algorithm_value.getZigString(globalObject); + + switch (PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse { + return globalObject.throwInvalidArgumentType("hash", "algorithm", unknown_password_algorithm_message); + }) { + .bcrypt => { + var algorithm = PasswordObject.Algorithm.Value{ + .bcrypt = PasswordObject.Algorithm.Value.bcrpyt_default, + }; + + if (try value.getTruthy(globalObject, "cost")) |rounds_value| { + if (!rounds_value.isNumber()) { + return globalObject.throwInvalidArgumentType("hash", "cost", "number"); + } + + const rounds = rounds_value.coerce(i32, globalObject); + + if (rounds < 4 or rounds > 31) { + return globalObject.throwInvalidArguments("Rounds must be between 4 and 31", .{}); + } + + algorithm.bcrypt = @as(u6, @intCast(rounds)); + } + + return algorithm; + }, + inline .argon2id, .argon2d, .argon2i => |tag| { + var argon = Algorithm.Argon2Params{}; + + if (try value.getTruthy(globalObject, "timeCost")) |time_value| { + if (!time_value.isNumber()) { + return globalObject.throwInvalidArgumentType("hash", "timeCost", "number"); + } + + const time_cost = time_value.coerce(i32, globalObject); + + if (time_cost < 1) { + return globalObject.throwInvalidArguments("Time cost must be greater than 0", .{}); + } + + argon.time_cost = @as(u32, @intCast(time_cost)); + } + + if (try value.getTruthy(globalObject, "memoryCost")) |memory_value| { + if (!memory_value.isNumber()) { + return globalObject.throwInvalidArgumentType("hash", "memoryCost", "number"); + } + + const memory_cost = memory_value.coerce(i32, globalObject); + + if (memory_cost < 1) { + return globalObject.throwInvalidArguments("Memory cost must be greater than 0", .{}); + } + + argon.memory_cost = @as(u32, @intCast(memory_cost)); + } + + return @unionInit(Algorithm.Value, @tagName(tag), argon); + }, + } + + unreachable; + } else { + return globalObject.throwInvalidArgumentType("hash", "options.algorithm", "string"); + } + } else if (value.isString()) { + const algorithm_string = try value.getZigString(globalObject); + + switch (PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse { + return globalObject.throwInvalidArgumentType("hash", "algorithm", unknown_password_algorithm_message); + }) { + .bcrypt => { + return PasswordObject.Algorithm.Value{ + .bcrypt = PasswordObject.Algorithm.Value.bcrpyt_default, + }; + }, + .argon2id => { + return PasswordObject.Algorithm.Value{ + .argon2id = .{}, + }; + }, + .argon2d => { + return PasswordObject.Algorithm.Value{ + .argon2d = .{}, + }; + }, + .argon2i => { + return PasswordObject.Algorithm.Value{ + .argon2i = .{}, + }; + }, + } + } else { + return globalObject.throwInvalidArgumentType("hash", "algorithm", "string"); + } + + unreachable; + } + }; + + pub const Argon2Params = struct { + // we don't support the other options right now, but can add them later if someone asks + memory_cost: u32 = pwhash.argon2.Params.interactive_2id.m, + time_cost: u32 = pwhash.argon2.Params.interactive_2id.t, + + pub fn toParams(this: Argon2Params) pwhash.argon2.Params { + return pwhash.argon2.Params{ + .t = this.time_cost, + .m = this.memory_cost, + .p = 1, + }; + } + }; + + pub const argon2 = Algorithm.argon2id; + + pub const label = bun.ComptimeStringMap( + Algorithm, + .{ + .{ "argon2i", .argon2i }, + .{ "argon2d", .argon2d }, + .{ "argon2id", .argon2id }, + .{ "bcrypt", .bcrypt }, + }, + ); + + pub const default = Algorithm.argon2; + + pub fn get(pw: []const u8) ?Algorithm { + if (pw[0] != '$') { + return null; + } + + // PHC format looks like $$$$ + if (strings.hasPrefixComptime(pw[1..], "argon2d$")) { + return .argon2d; + } + if (strings.hasPrefixComptime(pw[1..], "argon2i$")) { + return .argon2i; + } + if (strings.hasPrefixComptime(pw[1..], "argon2id$")) { + return .argon2id; + } + + if (strings.hasPrefixComptime(pw[1..], "bcrypt")) { + return .bcrypt; + } + + // https://en.wikipedia.org/wiki/Crypt_(C) + if (strings.hasPrefixComptime(pw[1..], "2")) { + return .bcrypt; + } + + return null; + } + }; + + pub const HashError = pwhash.Error || error{UnsupportedAlgorithm}; + + // This is purposely simple because nobody asked to make it more complicated + pub fn hash( + allocator: std.mem.Allocator, + password: []const u8, + algorithm: Algorithm.Value, + ) HashError![]const u8 { + switch (algorithm) { + inline .argon2i, .argon2d, .argon2id => |argon| { + var outbuf: [4096]u8 = undefined; + const hash_options = pwhash.argon2.HashOptions{ + .params = argon.toParams(), + .allocator = allocator, + .mode = switch (algorithm) { + .argon2i => .argon2i, + .argon2d => .argon2d, + .argon2id => .argon2id, + else => unreachable, + }, + .encoding = .phc, + }; + // warning: argon2's code may spin up threads if paralellism is set to > 0 + // we don't expose this option + // but since it parses from phc format, it's possible that it will be set + // eventually we should do something that about that. + const out_bytes = try pwhash.argon2.strHash(password, hash_options, &outbuf); + return try allocator.dupe(u8, out_bytes); + }, + .bcrypt => |cost| { + var outbuf: [4096]u8 = undefined; + var outbuf_slice: []u8 = outbuf[0..]; + var password_to_use = password; + // bcrypt silently truncates passwords longer than 72 bytes + // we use SHA512 to hash the password if it's longer than 72 bytes + if (password.len > 72) { + var sha_512 = bun.sha.SHA512.init(); + defer sha_512.deinit(); + sha_512.update(password); + sha_512.final(outbuf[0..bun.sha.SHA512.digest]); + password_to_use = outbuf[0..bun.sha.SHA512.digest]; + outbuf_slice = outbuf[bun.sha.SHA512.digest..]; + } + + const hash_options = pwhash.bcrypt.HashOptions{ + .params = pwhash.bcrypt.Params{ + .rounds_log = cost, + .silently_truncate_password = true, + }, + .allocator = allocator, + .encoding = .crypt, + }; + const out_bytes = try pwhash.bcrypt.strHash(password_to_use, hash_options, outbuf_slice); + return try allocator.dupe(u8, out_bytes); + }, + } + } + + pub fn verify( + allocator: std.mem.Allocator, + password: []const u8, + previous_hash: []const u8, + algorithm: ?Algorithm, + ) HashError!bool { + if (previous_hash.len == 0) { + return false; + } + + return verifyWithAlgorithm( + allocator, + password, + previous_hash, + algorithm orelse Algorithm.get(previous_hash) orelse return error.UnsupportedAlgorithm, + ); + } + + pub fn verifyWithAlgorithm( + allocator: std.mem.Allocator, + password: []const u8, + previous_hash: []const u8, + algorithm: Algorithm, + ) HashError!bool { + switch (algorithm) { + .argon2id, .argon2d, .argon2i => { + pwhash.argon2.strVerify(previous_hash, password, .{ .allocator = allocator }) catch |err| { + if (err == error.PasswordVerificationFailed) { + return false; + } + + return err; + }; + return true; + }, + .bcrypt => { + var password_to_use = password; + var outbuf: [bun.sha.SHA512.digest]u8 = undefined; + + // bcrypt silently truncates passwords longer than 72 bytes + // we use SHA512 to hash the password if it's longer than 72 bytes + if (password.len > 72) { + var sha_512 = bun.sha.SHA512.init(); + defer sha_512.deinit(); + sha_512.update(password); + sha_512.final(&outbuf); + password_to_use = &outbuf; + } + pwhash.bcrypt.strVerify(previous_hash, password_to_use, .{ + .allocator = allocator, + .silently_truncate_password = true, + }) catch |err| { + if (err == error.PasswordVerificationFailed) { + return false; + } + + return err; + }; + return true; + }, + } + } +}; + +pub const JSPasswordObject = struct { + const PascalToUpperUnderscoreCaseFormatter = struct { + input: []const u8, + pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + for (self.input) |c| { + if (std.ascii.isUpper(c)) { + try writer.writeByte('_'); + try writer.writeByte(c); + } else if (std.ascii.isLower(c)) { + try writer.writeByte(std.ascii.toUpper(c)); + } else { + try writer.writeByte(c); + } + } + } + }; + + pub export fn JSPasswordObject__create(globalObject: *JSC.JSGlobalObject) JSC.JSValue { + var object = JSValue.createEmptyObject(globalObject, 4); + object.put( + globalObject, + ZigString.static("hash"), + JSC.createCallback(globalObject, ZigString.static("hash"), 2, JSPasswordObject__hash), + ); + object.put( + globalObject, + ZigString.static("hashSync"), + JSC.createCallback(globalObject, ZigString.static("hashSync"), 2, JSPasswordObject__hashSync), + ); + object.put( + globalObject, + ZigString.static("verify"), + JSC.createCallback(globalObject, ZigString.static("verify"), 2, JSPasswordObject__verify), + ); + object.put( + globalObject, + ZigString.static("verifySync"), + JSC.createCallback(globalObject, ZigString.static("verifySync"), 2, JSPasswordObject__verifySync), + ); + return object; + } + + const HashJob = struct { + algorithm: PasswordObject.Algorithm.Value, + password: []const u8, + promise: JSC.JSPromise.Strong, + event_loop: *JSC.EventLoop, + global: *JSC.JSGlobalObject, + ref: Async.KeepAlive = .{}, + task: JSC.WorkPoolTask = .{ .callback = &run }, + + pub usingnamespace bun.New(@This()); + + pub const Result = struct { + value: Value, + ref: Async.KeepAlive = .{}, + + task: JSC.AnyTask = undefined, + promise: JSC.JSPromise.Strong, + global: *JSC.JSGlobalObject, + + pub usingnamespace bun.New(@This()); + + pub const Value = union(enum) { + err: PasswordObject.HashError, + hash: []const u8, + + pub fn toErrorInstance(this: Value, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + const error_code = std.fmt.allocPrint(bun.default_allocator, "PASSWORD{}", .{PascalToUpperUnderscoreCaseFormatter{ .input = @errorName(this.err) }}) catch bun.outOfMemory(); + defer bun.default_allocator.free(error_code); + const instance = globalObject.createErrorInstance("Password hashing failed with error \"{s}\"", .{@errorName(this.err)}); + instance.put(globalObject, ZigString.static("code"), JSC.ZigString.init(error_code).toJS(globalObject)); + return instance; + } + }; + + pub fn runFromJS(this: *Result) void { + var promise = this.promise; + defer promise.deinit(); + this.promise = .{}; + this.ref.unref(this.global.bunVM()); + const global = this.global; + switch (this.value) { + .err => { + const error_instance = this.value.toErrorInstance(global); + this.destroy(); + promise.reject(global, error_instance); + }, + .hash => |value| { + const js_string = JSC.ZigString.init(value).toJS(global); + this.destroy(); + promise.resolve(global, js_string); + }, + } + } + }; + + pub fn deinit(this: *HashJob) void { + this.promise.deinit(); + bun.freeSensitive(bun.default_allocator, this.password); + this.destroy(); + } + + pub fn getValue(password: []const u8, algorithm: PasswordObject.Algorithm.Value) Result.Value { + const value = PasswordObject.hash(bun.default_allocator, password, algorithm) catch |err| { + return Result.Value{ .err = err }; + }; + return Result.Value{ .hash = value }; + } + + pub fn run(task: *bun.ThreadPool.Task) void { + var this: *HashJob = @fieldParentPtr("task", task); + + var result = Result.new(.{ + .value = getValue(this.password, this.algorithm), + .task = undefined, + .promise = this.promise, + .global = this.global, + .ref = this.ref, + }); + this.promise = .empty; + + result.task = JSC.AnyTask.New(Result, Result.runFromJS).init(result); + this.ref = .{}; + this.event_loop.enqueueTaskConcurrent(JSC.ConcurrentTask.createFrom(&result.task)); + this.deinit(); + } + }; + + pub fn hash(globalObject: *JSC.JSGlobalObject, password: []const u8, algorithm: PasswordObject.Algorithm.Value, comptime sync: bool) bun.JSError!JSC.JSValue { + assert(password.len > 0); // caller must check + + if (comptime sync) { + const value = HashJob.getValue(password, algorithm); + switch (value) { + .err => { + const error_instance = value.toErrorInstance(globalObject); + return globalObject.throwValue(error_instance); + }, + .hash => |h| { + return JSC.ZigString.init(h).toJS(globalObject); + }, + } + + unreachable; + } + + const promise = JSC.JSPromise.Strong.init(globalObject); + + var job = HashJob.new(.{ + .algorithm = algorithm, + .password = password, + .promise = promise, + .event_loop = globalObject.bunVM().eventLoop(), + .global = globalObject, + }); + job.ref.ref(globalObject.bunVM()); + JSC.WorkPool.schedule(&job.task); + + return promise.value(); + } + + pub fn verify(globalObject: *JSC.JSGlobalObject, password: []const u8, prev_hash: []const u8, algorithm: ?PasswordObject.Algorithm, comptime sync: bool) bun.JSError!JSC.JSValue { + assert(password.len > 0); // caller must check + + if (comptime sync) { + const value = VerifyJob.getValue(password, prev_hash, algorithm); + switch (value) { + .err => { + const error_instance = value.toErrorInstance(globalObject); + return globalObject.throwValue(error_instance); + }, + .pass => |pass| { + return JSC.JSValue.jsBoolean(pass); + }, + } + + unreachable; + } + + var promise = JSC.JSPromise.Strong.init(globalObject); + + const job = VerifyJob.new(.{ + .algorithm = algorithm, + .password = password, + .prev_hash = prev_hash, + .promise = promise, + .event_loop = globalObject.bunVM().eventLoop(), + .global = globalObject, + }); + job.ref.ref(globalObject.bunVM()); + JSC.WorkPool.schedule(&job.task); + + return promise.value(); + } + + // Once we have bindings generator, this should be replaced with a generated function + pub fn JSPasswordObject__hash(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const arguments_ = callframe.arguments_old(2); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + return globalObject.throwNotEnoughArguments("hash", 1, 0); + } + + var algorithm = PasswordObject.Algorithm.Value.default; + + if (arguments.len > 1 and !arguments[1].isEmptyOrUndefinedOrNull()) { + algorithm = try PasswordObject.Algorithm.Value.fromJS(globalObject, arguments[1]); + } + + // TODO: this most likely should error like `hashSync` instead of stringifying. + // + // fromJS(...) orelse { + // return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray"); + // } + const password_to_hash = try JSC.Node.StringOrBuffer.fromJSToOwnedSlice(globalObject, arguments[0], bun.default_allocator); + errdefer bun.default_allocator.free(password_to_hash); + + if (password_to_hash.len == 0) { + return globalObject.throwInvalidArguments("password must not be empty", .{}); + } + + return hash(globalObject, password_to_hash, algorithm, false); + } + + // Once we have bindings generator, this should be replaced with a generated function + pub fn JSPasswordObject__hashSync(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const arguments_ = callframe.arguments_old(2); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 1) { + return globalObject.throwNotEnoughArguments("hash", 1, 0); + } + + var algorithm = PasswordObject.Algorithm.Value.default; + + if (arguments.len > 1 and !arguments[1].isEmptyOrUndefinedOrNull()) { + algorithm = try PasswordObject.Algorithm.Value.fromJS(globalObject, arguments[1]); + } + + var string_or_buffer = try JSC.Node.StringOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse { + return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray"); + }; + defer string_or_buffer.deinit(); + + if (string_or_buffer.slice().len == 0) { + return globalObject.throwInvalidArguments("password must not be empty", .{}); + } + + return hash(globalObject, string_or_buffer.slice(), algorithm, true); + } + + const VerifyJob = struct { + algorithm: ?PasswordObject.Algorithm = null, + password: []const u8, + prev_hash: []const u8, + promise: JSC.JSPromise.Strong, + event_loop: *JSC.EventLoop, + global: *JSC.JSGlobalObject, + ref: Async.KeepAlive = .{}, + task: JSC.WorkPoolTask = .{ .callback = &run }, + + pub usingnamespace bun.New(@This()); + + pub const Result = struct { + value: Value, + ref: Async.KeepAlive = .{}, + + task: JSC.AnyTask = undefined, + promise: JSC.JSPromise.Strong, + global: *JSC.JSGlobalObject, + + pub usingnamespace bun.New(@This()); + + pub const Value = union(enum) { + err: PasswordObject.HashError, + pass: bool, + + pub fn toErrorInstance(this: Value, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + const error_code = std.fmt.allocPrint(bun.default_allocator, "PASSWORD{}", .{PascalToUpperUnderscoreCaseFormatter{ .input = @errorName(this.err) }}) catch bun.outOfMemory(); + defer bun.default_allocator.free(error_code); + const instance = globalObject.createErrorInstance("Password verification failed with error \"{s}\"", .{@errorName(this.err)}); + instance.put(globalObject, ZigString.static("code"), JSC.ZigString.init(error_code).toJS(globalObject)); + return instance; + } + }; + + pub fn runFromJS(this: *Result) void { + var promise = this.promise; + defer promise.deinit(); + this.promise = .{}; + this.ref.unref(this.global.bunVM()); + const global = this.global; + switch (this.value) { + .err => { + const error_instance = this.value.toErrorInstance(global); + this.destroy(); + promise.reject(global, error_instance); + }, + .pass => |pass| { + this.destroy(); + promise.resolve(global, JSC.JSValue.jsBoolean(pass)); + }, + } + } + }; + + pub fn deinit(this: *VerifyJob) void { + this.promise.deinit(); + + bun.freeSensitive(bun.default_allocator, this.password); + bun.freeSensitive(bun.default_allocator, this.prev_hash); + + this.destroy(); + } + + pub fn getValue(password: []const u8, prev_hash: []const u8, algorithm: ?PasswordObject.Algorithm) Result.Value { + const pass = PasswordObject.verify(bun.default_allocator, password, prev_hash, algorithm) catch |err| { + return Result.Value{ .err = err }; + }; + return Result.Value{ .pass = pass }; + } + + pub fn run(task: *bun.ThreadPool.Task) void { + var this: *VerifyJob = @fieldParentPtr("task", task); + + var result = Result.new(.{ + .value = getValue(this.password, this.prev_hash, this.algorithm), + .task = undefined, + .promise = this.promise, + .global = this.global, + .ref = this.ref, + }); + this.promise = .empty; + + result.task = JSC.AnyTask.New(Result, Result.runFromJS).init(result); + this.ref = .{}; + this.event_loop.enqueueTaskConcurrent(JSC.ConcurrentTask.createFrom(&result.task)); + this.deinit(); + } + }; + + // Once we have bindings generator, this should be replaced with a generated function + pub fn JSPasswordObject__verify(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const arguments_ = callframe.arguments_old(3); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 2) { + return globalObject.throwNotEnoughArguments("verify", 2, 0); + } + + var algorithm: ?PasswordObject.Algorithm = null; + + if (arguments.len > 2 and !arguments[2].isEmptyOrUndefinedOrNull()) { + if (!arguments[2].isString()) { + return globalObject.throwInvalidArgumentType("verify", "algorithm", "string"); + } + + const algorithm_string = try arguments[2].getZigString(globalObject); + + algorithm = PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse { + if (!globalObject.hasException()) { + return globalObject.throwInvalidArgumentType("verify", "algorithm", unknown_password_algorithm_message); + } + return error.JSError; + }; + } + + // TODO: this most likely should error like `verifySync` instead of stringifying. + // + // fromJS(...) orelse { + // return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray"); + // } + const owned_password = try JSC.Node.StringOrBuffer.fromJSToOwnedSlice(globalObject, arguments[0], bun.default_allocator); + + // TODO: this most likely should error like `verifySync` instead of stringifying. + // + // fromJS(...) orelse { + // return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray"); + // } + const owned_hash = JSC.Node.StringOrBuffer.fromJSToOwnedSlice(globalObject, arguments[1], bun.default_allocator) catch |err| { + bun.default_allocator.free(owned_password); + return err; + }; + + if (owned_hash.len == 0) { + bun.default_allocator.free(owned_password); + return JSC.JSPromise.resolvedPromiseValue(globalObject, JSC.JSValue.jsBoolean(false)); + } + + if (owned_password.len == 0) { + bun.default_allocator.free(owned_hash); + return JSC.JSPromise.resolvedPromiseValue(globalObject, JSC.JSValue.jsBoolean(false)); + } + + return verify(globalObject, owned_password, owned_hash, algorithm, false); + } + + // Once we have bindings generator, this should be replaced with a generated function + pub fn JSPasswordObject__verifySync(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const arguments_ = callframe.arguments_old(3); + const arguments = arguments_.ptr[0..arguments_.len]; + + if (arguments.len < 2) { + return globalObject.throwNotEnoughArguments("verify", 2, 0); + } + + var algorithm: ?PasswordObject.Algorithm = null; + + if (arguments.len > 2 and !arguments[2].isEmptyOrUndefinedOrNull()) { + if (!arguments[2].isString()) { + return globalObject.throwInvalidArgumentType("verify", "algorithm", "string"); + } + + const algorithm_string = try arguments[2].getZigString(globalObject); + + algorithm = PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse { + if (!globalObject.hasException()) { + return globalObject.throwInvalidArgumentType("verify", "algorithm", unknown_password_algorithm_message); + } + return .zero; + }; + } + + var password = try JSC.Node.StringOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse { + return globalObject.throwInvalidArgumentType("verify", "password", "string or TypedArray"); + }; + + var hash_ = try JSC.Node.StringOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[1]) orelse { + password.deinit(); + return globalObject.throwInvalidArgumentType("verify", "hash", "string or TypedArray"); + }; + + defer password.deinit(); + defer hash_.deinit(); + + if (hash_.slice().len == 0) { + return JSC.JSValue.jsBoolean(false); + } + + if (password.slice().len == 0) { + return JSC.JSValue.jsBoolean(false); + } + + return verify(globalObject, password.slice(), hash_.slice(), algorithm, true); + } +}; + +const std = @import("std"); +const bun = @import("root").bun; +const string = bun.string; +const strings = bun.strings; +const MutableString = bun.MutableString; +const stringZ = bun.stringZ; +const default_allocator = bun.default_allocator; +const JSC = bun.JSC; +const Async = bun.Async; +const ZigString = JSC.ZigString; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const CallFrame = JSC.CallFrame; +const assert = bun.assert; + +const unknown_password_algorithm_message = "unknown algorithm, expected one of: \"bcrypt\", \"argon2id\", \"argon2d\", \"argon2i\" (default is \"argon2id\")"; diff --git a/test/internal/ban-words.test.ts b/test/internal/ban-words.test.ts index 7d388efcd2..fc383f6a7c 100644 --- a/test/internal/ban-words.test.ts +++ b/test/internal/ban-words.test.ts @@ -78,3 +78,17 @@ describe("banned words", () => { }); } }); + +describe("files that must have comments at the top", () => { + const files = ["src/bun.js/api/BunObject.zig"]; + + for (const file of files) { + test(file, async () => { + const joined = path.join(import.meta.dir, "..", "..", file); + const content = await Bun.file(joined).text(); + if (!content.startsWith("//")) { + throw new Error(`Please don't add imports to the top of ${file}. Put them at the bottom.`); + } + }); + } +});