diff --git a/Cargo.lock b/Cargo.lock index df6c2b7e0a02..4a45bb0c0948 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -915,7 +915,6 @@ dependencies = [ "bun_event_loop", "bun_exe_format", "bun_hash", - "bun_highway", "bun_http", "bun_http_types", "bun_install", diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index d0203fa489a0..8cbe4242e2e2 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -93,6 +93,10 @@ export const semver = b.namespace({ a `PropertyCallback` of a static property table, so the object is made when the property is first read. +A namespace that can itself be called, like `Bun.hash`, names the function that a call runs: +`b.namespace({ wyhash: { .. }, crc32: { .. } }, { call: "wyhash" })`. The object is then a +function named after the namespace. + ### What a function may do: `capability` What a function can do to the JS heap is a parameter it is given, so it is @@ -113,9 +117,23 @@ The compiler holds a body to its capability as far as this API goes. The older A other ways to a `&JSGlobalObject` (`VirtualMachine::get().global()`), and a body that takes one of those to run JavaScript has stepped around the check. +Much of the older API takes a `&JSGlobalObject` only to allocate or to throw. A `"heap"` +function calls it with `heap.global()`, which is `unsafe`: nothing the global object is passed +to may run JavaScript, and the compiler cannot see that. Read what is called all the way down +before writing the `SAFETY` comment. `ToString` of an object, a property that is looked up or +set and not defined, and a call to a constructor found on the global object all run JavaScript. +Throwing tells the debugger, which can run code, so after a throw the body returns without +reading what it borrowed. + +```rust +// SAFETY: parsing runs no JavaScript. Nor does making the result: its keys are primitives, and its properties are +// defined and not set. +let global = unsafe { heap.global() }; +``` + `throws: true` makes the return type a `Result`. The error is a `bindgen::Error<'a>`, made by `heap.error(..)`, `heap.type_error(..)`, -`heap.range_error(..)`, `heap.error_with_code(ErrorCode::INVALID_ARG_TYPE, ..)` or +`heap.range_error(..)`, `heap.error_with_code(ErrorCode::OUT_OF_RANGE, ..)`, `heap.invalid_arguments(..)` or `bindgen::Error::from_value(value)` (`?` makes one of a `JsError` or an `AllocError`), or any type that implements `bindgen::IntoError` (`bun_sys::Error`, `SystemError`, `JsError` and a `Box` of one do), which lets a `capability: "none"` function fail too. @@ -132,7 +150,7 @@ own Rust function, which gets exactly the types of that signature: ```ts export const read = b.overloads( { args: { path: b.string.utf8, options: ReadOptions.optional }, rustPath: "crate::read_path" }, - { args: { fd: b.i32, buffer: b.bufferSource.bytes, offset: b.u32.default(0) }, rustPath: "crate::read_fd" }, + { args: { fd: b.i32, buffer: b.bytes({ from: ["buffer"] }), offset: b.u32.default(0) }, rustPath: "crate::read_fd" }, ); ``` @@ -172,9 +190,8 @@ any other: nothing looks at how many there are. | `b.string.utf8` | `&'a [u8]` | the same, and a `String` object | | `b.string.utf8.owned` | `bun_core::Utf8Bytes<'static>` | as `b.string.utf8` | | `b.bufferSource` | `JsLocal<'a, BufferSource>` | a typed array, `DataView` or `ArrayBuffer` that is not shared | -| `b.bufferSource.bytes` | `&'a [u8]` | the same; see [Buffers](#buffers) | -| `b.bufferSource.bytes.allowShared` | `PossiblySharedBytes<'a>` | the same, and a shared one | | `b.bufferSource.arrayBuffer` | `ExternalShared` | the same; the bytes of a view are copied | +| `b.bytes(options)` | `&'a [u8]`, and others | see [Bytes](#bytes) | | `b.blob` | `ExternalShared` | a `Blob` | | `b.any` | `JsLocal<'a>` | anything | | `b.null` | `()` | `null` or `undefined`; only in a union | @@ -189,17 +206,19 @@ types: each is called what it is exported as, and has to be exported. It is used schemas of the crate it is generated into. A member or an alternative also names a C++ field, so one that is a C++ keyword is refused: a member takes a `rustName`. -These come last, in this order (`b.any` already takes `undefined` and `null`, so only `.withValue` applies to it): +These come last, in this order (`b.any` already takes `undefined` and `null`, so none applies to it): - `.optional`: `undefined` means absent; `Option`. - `.nullable`: `null` or `undefined` means absent; `Option`. - `.falsyAsNull`: every falsy value means absent; `Option`. - `.default(value)`: what to use when absent; `T`. `.nullable.default(value)` also applies to `null`. -- `.withValue`, for arguments: see [Keeping the original value](#keeping-the-original-value). Not with `.default()`. As results, `b.string.utf8` is anything that is `AsRef<[u8]>` and `b.array(T)` anything that is `IntoIterator`. The body of a `b.fn` or of a namespace's function whose bytes do not outlive -it returns the string itself, from `heap.create_utf8_string(bytes)`. +it returns the string itself, from `heap.create_utf8_string(bytes)`. `b.u64.asBigInt` is a +result only: a `u64` that is always returned as a BigInt. `b.bufferSource` as a result is what +`heap.create_uninitialized_uint8_array(length)`, `heap.create_uint8_array_from_box(bytes)` or +`heap.create_buffer_from_box(bytes)` made. The last two take over the allocation. ### Numbers @@ -288,7 +307,7 @@ in play apart: A strict type takes the kinds its row under [Types](#types) says. A type that coerces or reports its own error takes every kind: `.loose`, `.wrap`, `.enforceRange`, -`.validateInteger`, `b.any`, a dictionary. +`.validateInteger`, `b.any`, `b.bytes`, a dictionary. **A value goes to the first alternative, in written order, that takes its kind.** That alternative then converts it, and reports what is wrong with it: `1.5` given to @@ -305,11 +324,8 @@ place they end up in. ### Buffers -JavaScript can detach, resize or transfer a buffer, so `b.bufferSource.bytes` is only -for an argument (not a member or an alternative) of a function that cannot run -JavaScript. The generated code reads pointer and length after every other argument -is converted, since a conversion can run JavaScript too. A function that can run -JavaScript takes `b.bufferSource` itself and looks the bytes up when it needs them: +`b.bufferSource` is the buffer itself. JavaScript can detach, resize or transfer it, so the +body looks the bytes up when it needs them: ```rust let before = buffer.with_bytes(&cx.heap, sum); @@ -318,32 +334,85 @@ let after = buffer.with_bytes(&cx.heap, sum); ``` Calling `callback.call(cx, ..)` inside the closure does not compile. A detached or -out-of-bounds view is empty. - -Another thread can write the bytes of a `SharedArrayBuffer` at any time, which a `&[u8]` says -cannot happen, so these types refuse one. `.allowShared`, WebIDL's `[AllowShared]`, takes it, -and the body gets a `PossiblySharedBytes<'a>` whether the buffer is shared or not. There is no -way from it to a slice. What reads the bytes is a method of it, written for a byte that can -differ each time it is read: `index_of_char` hands the pointer to a kernel that uses nothing -it reads as an index or a length. Such an argument is always there: it is not `.optional`. +out-of-bounds view is empty. A shared buffer is refused: [`b.bytes`](#bytes) takes one. -### Keeping the original value +### Bytes -A converted argument is for use now. To hand the value to something that holds -it longer (one of a class's `internalFields`, a `Strong`, another JS call, the result), ask -for both: +Many functions take "a string or a buffer" and want its bytes. They differ in which values they +take, in what becomes of anything else, and in how long they need the bytes. `b.bytes` takes +those as named options. The combinations that are build errors are listed at the end. ```ts -args: { name: b.string.utf8.withValue }, +export const gzipSync = b.fn({ + args: { data: b.bytes({ from: ["string", "buffer"] }), options: ZlibCompressionOptions.default({}) }, + returns: b.bufferSource, + capability: "heap", + throws: true, +}); ``` ```rust -fn set_name<'a>(&mut self, this: JsWrapper<'a, Self>, heap: &JsHeap<'a>, name: WithValue<'a, &'a [u8]>) { - this.set_name_field(heap, name.value); // the JS string itself, not a copy - self.name_length = name.converted.len(); -} +fn gzip_sync<'a>(heap: &JsHeap<'a>, data: &[u8], options: ZlibCompressionOptions<'a>) -> bindgen::Result<'a, JsLocal<'a, BufferSource>> ``` +| Option | Values | Meaning | +| ----------- | ---------------------------------------------------- | ---------------------------------------------------------------------- | +| `from` | `"string"`, `"buffer"`, `"blob"` | what is taken without `ToString` | +| `otherwise` | **`"throw"`**, `"toString"` | anything else | +| `refuse` | `"undefined"`, `"null"` | with `"toString"`: these throw, and are not `"undefined"` and `"null"` | +| `string` | **`"utf8"`**, `"asIs"` | a string is its UTF-8, or stays a `bun_core::String` | +| `shared` | **`"borrowed"`**, `"copied"` | the bytes of a shared buffer | +| `held` | **`"borrowed"`**, `"handle"`, `"copied"`, `"pinned"` | what the body gets | + +`"string"` is a string or a `String` object. `"buffer"` is a typed array, `DataView`, +`ArrayBuffer` or `SharedArrayBuffer`. `"blob"` is a `Blob` whose bytes are in memory: a file, +here or in S3, throws. + +| `held` | `string` | Rust | +| ------------ | -------- | --------------------- | +| `"borrowed"` | `"utf8"` | `&'a [u8]` | +| `"handle"` | `"utf8"` | `JsBytes<'a>` | +| `"handle"` | `"asIs"` | `StringOrJsBytes<'a>` | +| `"copied"` | `"utf8"` | `Vec` | +| `"pinned"` | `"utf8"` | `PinnedBytes` | + +**Borrowed** bytes are where they are. A string is converted into the frame of the host function, +unless it is stored as ASCII already. JavaScript can detach, resize or free a buffer, so this is +for a function that cannot run it. + +**A handle** is for a function that can. `JsBytes::get` borrows the bytes from the `JsHeap`, and the +compiler refuses whatever takes the `&mut JsScope` until the slice is gone: + +```rust +fn parse<'a>(cx: &mut JsScope<'a>, input: JsBytes<'a>) -> JsResult { + let tree = parse_tree(input.get(&cx.heap))?; // no JavaScript here + let global = cx.global(); // `tree` owns what it kept, or this does not compile +``` + +Each `get` reads the pointer and the length of a buffer again. With `string: "asIs"` the body +gets either `String`, in Latin-1 or UTF-16, or `Bytes`, the handle. That also tells a string +from a buffer. + +**Copied** bytes are the body's. **Pinned** bytes are for a job on another thread to read after +the call returns: a string is thread-isolated, a buffer is a GC-rooted `PinnedArrayBuffer`, and a +resizable one is copied. A `PinnedBytes` is `Send`, derefs to `[u8]`, and is dropped on the JS +thread. + +**A shared buffer** is borrowed like any other, as JSC's own `toBase64` and `indexOf` read one, and +another thread that writes it during the call changes what the body reads. `shared: "copied"` is +for a body that reads a byte again and counts on what it was, such as a parser that scans ahead +and comes back. JSC's `sort` copies for the same reason. + +The kind of the value is checked, and a string converted, in the order of the arguments. The +bytes of a buffer are read, copied or pinned after every argument is converted. The argument is +always there: it is not `.optional`, and not a member of a dictionary, a union or an array. + +An option that cannot apply is a build error: `refuse` without `"toString"`, `string` where no +string is taken, `shared` where no buffer is taken or `held: "copied"` copies it anyway. So is +what no function needs yet, which is not written: `"asIs"` without `"handle"` or that copies a +shared buffer, `"pinned"` that takes a `Blob` or copies a shared buffer, and a `"pinned"` argument +next to another or next to a `"borrowed"` one. + ## Values and lifetimes A JS value in Rust is a `JsLocal<'a>`: a `JSValue` tied to the host call it belongs diff --git a/src/bun_alloc/c_thunks.rs b/src/bun_alloc/c_thunks.rs index 1108adef976d..e01d1aab3314 100644 --- a/src/bun_alloc/c_thunks.rs +++ b/src/bun_alloc/c_thunks.rs @@ -29,9 +29,6 @@ pub unsafe extern "C" fn mi_free_opaque(_: *mut c_void, ptr: *mut c_void) { unsafe { raw::free(ptr) }; } -/// JSC `JSTypedArrayBytesDeallocator` → default allocator `free(ctx)`; `bytes` ignored. -pub use mi_free_opaque as mi_free_ctx; - /// JSC `JSTypedArrayBytesDeallocator` → default allocator `free(bytes)`; `ctx` ignored. pub unsafe extern "C" fn mi_free_bytes(bytes: *mut c_void, _ctx: *mut c_void) { // SAFETY: bytes was allocated by the default allocator (or is null). diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 20657ebe82a9..b9ffb9657ecd 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -27,7 +27,6 @@ import { UnionType, UnitType, type ValueKind, - WithValueType, } from "./bindgen-lib-internal.ts"; import { cap, low } from "./helpers.ts"; @@ -136,8 +135,8 @@ export function analyze(files: readonly SchemaFile[]): Tables { const implementationType = (type: Type) => `${type.idlType}::ImplementationType`; const externType = (type: Type): string => - type instanceof WithValueType - ? `Bun::Bindgen::ExternWithValue<${externType(type.inner)}>` + type.passesValue + ? `Bun::Bindgen::ExternWithValue>` : `Bun::Bindgen::ExternOf<${type.idlType}>`; const toExtern = (variable: string) => `Bun::Bindgen::toExtern(${variable})`; @@ -408,7 +407,11 @@ namespace Bun::Bindgen::Generated::${origin.namespace} { ${cppNamespaceSignature(namespace)} { JSC::JSGlobalObject* globalObject = owner->globalObject(); - JSC::JSObject* object = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), std::min(${namespace.functions.length}, JSC::JSFinalObject::maxInlineCapacity)); + JSC::JSObject* object = ${ + namespace.call + ? `JSC::JSFunction::create(vm, globalObject, ${namespace.call.length}, ${asciiLiteral(namespace.name)}, ${hostFunctionName(namespace.call)}, JSC::ImplementationVisibility::Public)` + : `JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), std::min(${namespace.functions.length}, JSC::JSFinalObject::maxInlineCapacity))` + }; ${namespace.functions .map( fn => @@ -543,22 +546,18 @@ function cppDeclareConverted(type: Type, variable: string, value: string, ctx: s /** Converts the arguments, then returns what `symbol(globalObject, ...leading, ...converted)` returns. */ function cppCallBody(signature: Signature, symbol: string, leading: readonly string[] = []): string { const lines: string[] = []; - const rereads: string[] = []; + // Converting a later argument can run JavaScript that detaches or resizes a buffer. + const readLast: string[] = []; signature.args.forEach(({ name, type }, i) => { lines.push(`JSC::EnsureStillAliveScope argument${i} = callFrame->argument(${i});`); lines.push(`[[maybe_unused]] auto ctx${i} = Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(name)} };`); - // Converting a later argument can run JavaScript that detaches or resizes the buffer. - if (type.borrowsHeap && i < signature.args.length - 1) - rereads.push( - `converted${i} = ${type.cppConvert(`argument${i}.value()`, `ctx${i}`, name)};`, - `scope.assertNoException();`, - ); + readLast.push(...type.cppReadLast(`converted${i}`, `argument${i}.value()`)); lines.push(...cppDeclareConverted(type, `converted${i}`, `argument${i}.value()`, `ctx${i}`, name)); if (type.conversionThrows) lines.push(`RETURN_IF_EXCEPTION(scope, {});`); }); - lines.push(...rereads); + lines.push(...readLast); const args = signature.args.map(({ type }, i) => { - return type instanceof WithValueType + return type.passesValue ? `${externType(type)} { ${toExtern(`converted${i}`)}, JSC::JSValue::encode(argument${i}.value()) }` : toExtern(`converted${i}`); }); @@ -952,11 +951,18 @@ const rustExternParameters = (signature: Signature, scope: RustScope) => const rustArguments = (signature: Signature) => signature.args.map(rustArgumentName); /** Each argument gets the call's lifetime here: left to inference in the call, a body could ask for `'static`. */ function rustTakeArguments(args: readonly { name: string; type: Type }[], scope: RustScope): string { - const taken = args.flatMap(arg => { - const name = rustArgumentName(arg); - const converted = arg.type.rustFromExtern(name, scope); - return converted === name ? [] : [`let ${name} = ${converted};\n`]; - }); + // What can return early comes last: by then Rust owns whatever the other arguments hand over. + const taken = args + .toSorted((a, b) => +a.type.rustFromExternThrows - +b.type.rustFromExternThrows) + .flatMap(arg => { + const name = rustArgumentName(arg); + const converted = arg.type.rustFromExtern(name, scope); + if (arg.type.rustFromExternThrows) + return [ + `let ${name} = match ${converted} {\n Ok(value) => value,\n Err(error) => return error.into_host_fn_return(cx),\n};\n`, + ]; + return converted === name ? [] : [`let ${name} = ${converted};\n`]; + }); if (taken.some(line => /\bheap\b/.test(line))) taken.unshift("let heap = &cx.heap;\n"); return taken.join(""); } diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index b6117e258c02..b5c0229f741b 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -160,10 +160,6 @@ export abstract class Type { get falsyAsNull(): OptionalType { return new OptionalType(this, "falsyAsNull"); } - /** Converted for use now, and the value itself for whatever holds it longer. Only for arguments. */ - get withValue(): WithValueType { - return new WithValueType(this); - } /** Value to use when absent. Only for arguments and dictionary members. */ default(value: unknown): DefaultType { return new DefaultType(this, value); @@ -194,6 +190,14 @@ export abstract class Type { get isOptional(): boolean { return false; } + /** The host function passes the argument itself along: `ExternWithValue`. Only for an argument. */ + get passesValue(): boolean { + return false; + } + /** `rustFromExtern` is a `bindgen::Result`. Only for an argument. */ + get rustFromExternThrows(): boolean { + return false; + } /** Converting can throw, so an exception check follows. */ get conversionThrows(): boolean { return true; @@ -202,7 +206,7 @@ export abstract class Type { get holdsJSValue(): boolean { return this.dependencies.some(type => type.holdsJSValue); } - /** Points into memory JavaScript can detach, resize or free: not for a function that can run it. */ + /** Points into memory JavaScript can detach, resize or free: not for a function that can run it, and only for an argument, read once all are converted. */ get borrowsHeap(): boolean { return this.dependencies.some(type => type.borrowsHeap); } @@ -240,6 +244,10 @@ export abstract class Type { cppConvert(value: string, ctx: string, name: string): string { return `Bun::convertIDL<${this.idlType}>(*globalObject, ${value}, ${ctx})`; } + /** C++ lines for once every argument is converted. They read into `target`, converted from `value`, what converting a later argument could have changed. */ + cppReadLast(target: string, value: string): string[] { + return []; + } /** Large, or holds something large: built where it ends up, not returned and moved there. */ get convertsInPlace(): boolean { return false; @@ -407,11 +415,33 @@ export class IntegerType extends PrimitiveType { this.#refusesBigInt(".validateInteger()"); return new RangeType(this.info, min, max, "validateInteger"); } + /** As a result: a BigInt, which holds every `u64`. */ + get asBigInt(): BigIntResultType { + assert(this.info.rust === "u64" && this.#conversion === "Strict" && !this.#allowBigInt, ".asBigInt follows b.u64"); + return new BigIntResultType(); + } #refusesBigInt(modifier: string): void { assert(!this.#allowBigInt, `${modifier} refuses a BigInt, which .allowBigInt asked for`); } } +export class BigIntResultType extends Type { + readonly idlType = ""; + readonly tsType = "bigint"; + layout(): Layout { + return { size: 8, align: 8 }; + } + rustExternType(): string { + return "u64"; + } + rustType(): string { + return "u64"; + } + rustToJS(value: string): string { + return `bindgen::to_js::u64_as_bigint(heap, ${value})`; + } +} + export class RangeType extends IntegerType { readonly modifier: "enforceRange" | "validateInteger"; readonly #range: readonly [number, number]; @@ -614,7 +644,7 @@ export class Utf8Type extends Type { return true; } cppConvertInto(target: string, value: string, ctx: string): string { - return `WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target});`; + return `WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target}, "a string"_s);`; } cppAssign(target: string, value: unknown): string { assert(typeof value === "string", `${util.inspect(value)} is not a string`); @@ -649,46 +679,121 @@ export class Utf8Type extends Type { const bufferSourceKinds: ValueKinds = ["arrayBuffer", "arrayBufferView"]; const sharedBuffers = "shared buffers"; -/** The bytes of a typed array, `DataView` or `ArrayBuffer`, as `&[u8]`. */ +/** What each option means: docs/project/bindgen.mdx, "Bytes". */ +export interface BytesOptions { + from: readonly ("string" | "buffer" | "blob")[]; + otherwise?: "throw" | "toString"; + refuse?: readonly ("undefined" | "null")[]; + string?: "utf8" | "asIs"; + shared?: "borrowed" | "copied"; + held?: "borrowed" | "handle" | "copied" | "pinned"; +} + +/** The bytes of an argument. Those of a buffer are read, copied or pinned once every argument is converted. */ export class BytesType extends Type { - readonly idlType: string = "Bun::Bindgen::IDLBufferSourceBytes"; - readonly tsType: string = "ArrayBuffer | ArrayBufferView"; - /** WebIDL's `[AllowShared]`: a `SharedArrayBuffer` and a view of one too, as a `PossiblySharedBytes<'a>`. */ - get allowShared(): PossiblySharedBytesType { - return new PossiblySharedBytesType(); + readonly #options: Required; + constructor(options: BytesOptions) { + super(); + const { from, otherwise = "throw", refuse = [], string = "utf8", shared = "borrowed", held = "borrowed" } = options; + this.#options = { from, otherwise, refuse, string, shared, held }; + assert(from.length > 0, "from: it takes nothing"); + assertDistinct(from, "kinds"); + assertDistinct(refuse, "refused values"); + assert(refuse.length === 0 || otherwise === "toString", 'refuse: without otherwise: "toString" they throw already'); + assert(string === "utf8" || from.includes("string") || otherwise === "toString", "string: it takes no string"); + assert(shared === "borrowed" || from.includes("buffer"), "shared: it takes no buffer"); + assert(shared === "borrowed" || held !== "copied", 'held: "copied" copies every buffer'); + // What no function needs yet is not written. + assert(string === "utf8" || held === "handle", 'string: "asIs" is only with held: "handle"'); + assert(string === "utf8" || shared === "borrowed", 'string: "asIs" does not copy a shared buffer'); + assert(shared === "borrowed" || held !== "pinned", 'held: "pinned" pins a shared buffer; it does not copy one'); + assert(held !== "pinned" || !from.includes("blob"), 'held: "pinned" does not take a Blob'); + } + /** C++ converts a string to UTF-8 in the frame. Otherwise Rust gets the string. */ + get #isView(): boolean { + const { held, string } = this.#options; + return string === "utf8" && (held === "borrowed" || held === "handle"); } - get valueKinds(): ValueKinds { - return bufferSourceKinds; + get idlType(): string { + const { from, otherwise, refuse } = this.#options; + const options = { + fromString: from.includes("string"), + fromBuffer: from.includes("buffer"), + fromBlob: from.includes("blob"), + toStringOtherwise: otherwise === "toString", + refuseUndefined: refuse.includes("undefined"), + refuseNull: refuse.includes("null"), + }; + const members = Object.entries(options).flatMap(([name, value]) => (value ? [`.${name} = true`] : [])); + const sink = this.#isView ? "JSBytesView" : "JSBlobOrStringOrBuffer"; + return `Bun::Bindgen::IDLBytes`; + } + get tsType(): string { + if (this.#options.otherwise === "toString") return "any"; + const spelling = { string: "string", buffer: "ArrayBuffer | SharedArrayBuffer | ArrayBufferView", blob: "Blob" }; + return this.#options.from.map(kind => spelling[kind]).join(" | "); + } + get passesValue(): boolean { + return this.#options.held !== "borrowed"; } get borrowsHeap(): boolean { + return this.#options.held === "borrowed"; + } + get convertsInPlace(): boolean { return true; } - get nullRepresentation() { - return "pointer" as const; + cppConvertInto(target: string, value: string, ctx: string): string { + const spelling = { string: ["a string"], buffer: ["ArrayBuffer", "ArrayBufferView"], blob: ["Blob"] }; + const expected = (["string", "buffer", "blob"] as const) + .filter(kind => this.#options.from.includes(kind)) + .flatMap(kind => spelling[kind]); + if (!this.#options.from.includes("string")) expected[0] = `${/^A/.test(expected[0]) ? "an" : "a"} ${expected[0]}`; + const last = expected.pop(); + const list = expected.length > 0 ? `${expected.join(", ")} or ${last}` : last!; + return `WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target}, ${asciiLiteral(list)});`; + } + cppReadLast(target: string, value: string): string[] { + if (!this.#isView) return []; + return [ + ...(this.#options.held === "borrowed" ? [`${target}.read(${value});`] : []), + ...(this.#options.shared === "copied" + ? [ + `WTF::MallocSpan ${target}Copy;`, + `${target}.copyIfShared(*globalObject, ${value}, ${target}Copy);`, + `RETURN_IF_EXCEPTION(scope, {});`, + ] + : []), + ]; } layout(): Layout { - return { size: 16, align: 8 }; + const converted = this.#isView ? { size: 16, align: 8 } : { size: 8, align: 8 }; + return this.passesValue ? structLayout([converted, { size: 8, align: 8 }]) : converted; } rustExternType(): string { - return "bindgen::extern_types::ExternBytes"; + const converted = `bindgen::extern_types::Extern${this.#isView ? "Bytes" : "String"}`; + return this.passesValue ? `bindgen::extern_types::ExternWithValue<${converted}>` : converted; + } + /** What the body gets, and the expression that makes it of `argument`. */ + #rust(argument: string): readonly [type: string, fromExtern: string] { + const stringOrBytes = `bindgen::StringOrJsBytes::from_extern(${argument}, heap)`; + if (this.#options.string === "asIs") return ["bindgen::StringOrJsBytes<'a>", stringOrBytes]; + return ( + { + borrowed: ["&'a [u8]", `${argument}.get(heap)`], + handle: ["bindgen::JsBytes<'a>", `bindgen::JsBytes::from_extern(${argument}, heap)`], + copied: ["Vec", `${stringOrBytes}.into_vec(heap)`], + pinned: ["bindgen::PinnedBytes", `bindgen::PinnedBytes::from_extern(${argument}, heap)`], + } as const + )[this.#options.held]; } rustType(): string { - return "&'a [u8]"; - } - rustFromExtern(expr: string): string { - return `${expr}.get(heap)`; + return this.#rust("")[0]; } -} - -/** Bytes that another thread may be writing, so not as `&[u8]`. */ -export class PossiblySharedBytesType extends BytesType { - readonly idlType = "WebCore::IDLAllowSharedAdaptor"; - readonly tsType = "ArrayBuffer | SharedArrayBuffer | ArrayBufferView"; - rustType(): string { - return "bindgen::PossiblySharedBytes<'a>"; + get rustFromExternThrows(): boolean { + return this.#options.held === "pinned"; } rustFromExtern(expr: string): string { - return `${expr}.get_possibly_shared(heap)`; + return this.#rust(expr)[1]; } } @@ -702,10 +807,6 @@ export class BufferSourceType extends Type { get refuses() { return sharedBuffers; } - /** `&[u8]` for the duration of the call. Not for `capability: "js"`, where JavaScript could detach it. */ - get bytes(): BytesType { - return new BytesType(); - } /** A reference on a `JSC::ArrayBuffer`, which can be stored: the buffer itself, or a copy of what a view shows. */ get arrayBuffer(): RefType { return new RefType( @@ -737,62 +838,6 @@ export class BufferSourceType extends Type { } } -/** Converts as `inner` does; the host function passes the argument itself along. */ -export class WithValueType extends Type { - readonly inner: Type; - constructor(inner: Type) { - super(); - assert(!(inner instanceof DefaultType), ".withValue cannot be combined with .default()"); - assert(!(inner instanceof WithValueType), "already .withValue"); - this.inner = inner; - } - get idlType(): string { - return this.inner.idlType; - } - get tsType(): string { - return this.inner.tsType; - } - get takesUndefined(): boolean { - return this.inner.takesUndefined; - } - get isOptional(): boolean { - return this.inner.isOptional; - } - get conversionThrows(): boolean { - return this.inner.conversionThrows; - } - get valueKinds(): ValueKinds { - return this.inner.valueKinds; - } - get holdsJSValue(): boolean { - return true; - } - get dependencies(): readonly Type[] { - return [this.inner]; - } - get convertsInPlace(): boolean { - return this.inner.convertsInPlace; - } - cppConvertInto(target: string, value: string, ctx: string, name: string): string { - return this.inner.cppConvertInto(target, value, ctx, name); - } - cppConvert(value: string, ctx: string, name: string): string { - return this.inner.cppConvert(value, ctx, name); - } - layout(): Layout { - return structLayout([this.inner.layout(), { size: 8, align: 8 }]); - } - rustExternType(scope: RustScope): string { - return `bindgen::extern_types::ExternWithValue<${this.inner.rustExternType(scope)}>`; - } - rustType(scope: RustScope): string { - return `bindgen::WithValue<'a, ${this.inner.rustType(scope)}>`; - } - rustFromExtern(expr: string, scope: RustScope): string { - return `{ let with_value = ${expr}; bindgen::WithValue { converted: ${this.inner.rustFromExtern("with_value.converted", scope)}, value: bindgen::JsLocal::from_extern(with_value.value, heap) } }`; - } -} - export class AnyType extends Type { readonly idlType = "Bun::Bindgen::IDLRawAny"; readonly tsType = "any"; @@ -893,9 +938,8 @@ export class OptionalType extends Type { assert(!(inner instanceof OptionalType), "already optional"); assert(!(inner instanceof DefaultType), "put .default() last"); assert(!(inner instanceof AnyType), "b.any already accepts undefined and null"); - assert(!(inner instanceof WithValueType), "put .withValue last"); assert(!inner.takesUndefined, `${inner.tsType} already takes undefined`); - assert(!(inner instanceof PossiblySharedBytesType), ".allowShared is for an argument that is always there"); + assert(!(inner instanceof BytesType), "b.bytes is for an argument that is always there"); this.inner = inner; this.modifier = modifier; } @@ -1076,10 +1120,10 @@ export class ArrayType extends Type { super(); assert(!(element instanceof DefaultType), "an element of an array is never absent, so it has no default"); assert(!(element instanceof RangeType), "a range reports the name of its argument, which an element does not have"); - assert(!(element instanceof WithValueType), ".withValue is only for an argument"); + assert(!element.passesValue, "b.bytes is only for an argument"); assert( !element.borrowsHeap, - "b.bufferSource.bytes is only for an argument: it is read after everything else is converted", + "the bytes of a buffer are only for an argument: they are read after everything else is converted", ); assert(!(element instanceof UnitType), "an array of nothing but null"); assert( @@ -1284,15 +1328,14 @@ export class DictionaryType extends NamedType { this.members.map(member => member.fieldName), "fields", ); - for (const { name, type } of this.members) - assert(!(type instanceof WithValueType), `${name}: .withValue is only for an argument`); + for (const { name, type } of this.members) assert(!type.passesValue, `${name}: b.bytes is only for an argument`); assert( !this.nonObjectAsEmpty || this.hasDefault, "nonObjectAsEmpty: every member must be optional or have a default", ); assert( !this.borrowsHeap, - "b.bufferSource.bytes is only for an argument: it is read after everything else is converted", + "the bytes of a buffer are only for an argument: they are read after everything else is converted", ); assert( !this.fromJS || !this.borrowsFrame, @@ -1387,13 +1430,13 @@ export class UnionType extends NamedType { ); assert( !this.borrowsHeap, - "b.bufferSource.bytes is only for an argument: it is read after everything else is converted", + "the bytes of a buffer are only for an argument: they are read after everything else is converted", ); for (const { name, type } of this.alternatives) { assert(!(type instanceof UnionType), `${name}: list the alternatives of the other union here`); assert(!(type instanceof OptionalType), `${name}: add b.null as an alternative`); assert(!(type instanceof DefaultType), `${name}: put .default() on the union`); - assert(!(type instanceof WithValueType), `${name}: .withValue is only for an argument`); + assert(!type.passesValue, `${name}: b.bytes is only for an argument`); assert(/^[a-z][A-Za-z0-9]*$/.test(name), `${name}: an alternative is named in camelCase`); assert(name !== "tag", "tag: the generated struct has a member of that name"); assert(!cppKeywords.has(name), `${name} is a C++ keyword, and JavaScript never sees the name: pick another`); @@ -1506,13 +1549,18 @@ export class Signature { `length: ${this.length} is not a count`, ); for (const { name, type } of this.args) { + assert(!isMadeOf(type, BigIntResultType), `${name}: .asBigInt is a result; an argument is b.u64.allowBigInt`); assert( !type.borrowsHeap || this.capability !== "js", - type instanceof PossiblySharedBytesType - ? `${name}: JavaScript can detach a buffer; .allowShared is for a function that cannot run it` - : `${name}: JavaScript can detach a buffer; take b.bufferSource without .bytes and use with_bytes`, + `${name}: JavaScript can detach a buffer; held: "handle" borrows the bytes each time they are read`, ); } + const pinned = this.args.filter(arg => arg.type.rustFromExternThrows); + assert(pinned.length <= 1, 'held: "pinned": if one fails, nothing releases the string of the next'); + assert( + pinned.length === 0 || !this.args.some(arg => arg.type.borrowsHeap), + 'held: "pinned" moves the bytes of a small typed array, which another argument that is held: "borrowed" can be', + ); assert(!(this.returns instanceof DefaultType), "a result has no default"); assert( !this.returns.holdsJSValue || this.capability !== "none", @@ -1525,6 +1573,11 @@ export class Signature { } } +/** `type` is a `kind`, or something it is made of is. */ +function isMadeOf(type: Type, kind: new (...args: never[]) => Type): boolean { + return type.dependencies.some(dependency => isMadeOf(dependency, kind)) || type instanceof kind; +} + /** Call a signature, or look at the kind of one more argument. */ export type OverloadDispatch = Signature | { argument: number; byKind: Map }; @@ -1802,11 +1855,17 @@ export class ClassType extends NamedType { } } -/** A plain object whose properties are native functions, as WebIDL's `namespace`. */ +export interface NamespaceOptions { + /** The object is itself a function, named as the namespace is, that does what this one of its functions does. */ + call?: string; +} + +/** A plain object whose properties are native functions, as WebIDL's `namespace`. With `call`, a function that has them. */ export class NamespaceDefinition { readonly functions: readonly FunctionDefinition[]; + readonly call: FunctionDefinition | undefined; name = ""; - constructor(functions: Record) { + constructor(functions: Record, options: NamespaceOptions = {}) { this.functions = Object.entries(functions).map(([name, options]) => { assert(/^[A-Za-z_$][\w$]*$/.test(name), `${name}: a function is named by an identifier`); assert(!(options instanceof FunctionDefinition), `${name}: write what b.fn takes, without b.fn`); @@ -1815,6 +1874,8 @@ export class NamespaceDefinition { return fn; }); assert(this.functions.length > 0, "a namespace with no functions"); + this.call = this.functions.find(fn => fn.name === options.call); + assert(options.call === undefined || this.call, `call: ${options.call} is not one of its functions`); } } diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index 71ca86e31822..409b74efdc4a 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -5,6 +5,8 @@ import { ArrayType, BoolType, BufferSourceType, + type BytesOptions, + BytesType, type ClassOptions, ClassType, type DictionaryOptions, @@ -16,6 +18,7 @@ import { type MemberOptions, ModuleDefinition, NamespaceDefinition, + type NamespaceOptions, RefType, type SignatureOptions, StringType, @@ -30,7 +33,7 @@ export { null_ as null }; /** Strict: only a boolean. `.loose` for `ToBoolean`. */ export const bool = new BoolType(); -/** Strict: a number that is an integer in range. `.allowBigInt`, `.loose`, `.wrap`, `.enforceRange()`, `.validateInteger()`. */ +/** Strict: a number that is an integer in range. `.allowBigInt`, `.loose`, `.wrap`, `.enforceRange()`, `.validateInteger()`. As a result, `b.u64.asBigInt`. */ export const { u8, u16, i32, u32, u64, usize } = integers; /** Strict: only a number. `.loose` for `ToNumber`, then `.allowBigInt`. */ @@ -42,9 +45,12 @@ export const string = new StringType(); /** The JS value itself, as a `JsLocal<'a>`. */ export const any = new AnyType(); -/** A typed array, `DataView` or `ArrayBuffer` that is not shared, as a `JsLocal<'a, BufferSource>`. `.bytes`, `.arrayBuffer`. */ +/** A typed array, `DataView` or `ArrayBuffer` that is not shared, as a `JsLocal<'a, BufferSource>`. `.arrayBuffer`. */ export const bufferSource = new BufferSourceType(); +/** The bytes of a string, a buffer or a `Blob`: which of them, what becomes of anything else, and how the body holds them. */ +export const bytes = (options: BytesOptions) => new BytesType(options); + /** The native `Blob` behind a JS `Blob`. */ export const blob = new RefType( "Bun::Bindgen::IDLBlobRef", @@ -82,8 +88,9 @@ export const fn = (signature: SignatureOptions) => new FunctionDefinition([signa /** One JS function, several signatures, told apart by the kinds of their arguments. */ export const overloads = (...signatures: SignatureOptions[]) => new FunctionDefinition(signatures); -/** `export const semver = b.namespace({ satisfies: { args, returns }, .. })`: an object that holds those functions. */ -export const namespace = (functions: Record) => new NamespaceDefinition(functions); +/** `export const semver = b.namespace({ satisfies: { args, returns }, .. })`: an object that holds those functions, or with `{ call: "satisfies" }` a function that does what that one does. */ +export const namespace = (functions: Record, options?: NamespaceOptions) => + new NamespaceDefinition(functions, options); /** `export default b.module({ rustPath: "crate::node::os" })`: where the file's Rust functions are. */ export const module = (options: { rustPath: string }) => new ModuleDefinition(options); diff --git a/src/hash/adler32.rs b/src/hash/adler32.rs index 3c060344d3e8..88e1287e6cff 100644 --- a/src/hash/adler32.rs +++ b/src/hash/adler32.rs @@ -2,9 +2,7 @@ //! //! Follows https://tools.ietf.org/html/rfc1950#section-9 and zlib's `adler32.c`. //! -//! `HashObject.rs` exposes this via `hash_wrap::`, which calls -//! the single-argument `hash(input)` (no seed) — the JS-side seed argument is -//! ignored for Adler32. +//! `HashObject.rs` exposes the single-argument `hash(input)`: Adler32 takes no seed. pub struct Adler32; diff --git a/src/hash/murmur.rs b/src/hash/murmur.rs index b7271ca8fb07..4cb6641aec78 100644 --- a/src/hash/murmur.rs +++ b/src/hash/murmur.rs @@ -1,7 +1,7 @@ //! MurmurHash family. //! //! `HashObject.rs` exposes -//! these via `hash_wrap`, which dispatches to `hash_with_seed(str, seed)` (the +//! these through `hash_with_seed(str, seed)` (the //! `(bytes, seed)` argument order — note the seed comes *second*, unlike //! XxHash). The default seed is `0xc70f6907`. //! diff --git a/src/hash/rapidhash.rs b/src/hash/rapidhash.rs index fee48d36c3a1..1e22ecea4a71 100644 --- a/src/hash/rapidhash.rs +++ b/src/hash/rapidhash.rs @@ -1,7 +1,6 @@ //! RapidHash. //! -//! `HashObject.rs` exposes this via `hash_wrap::`, -//! which calls `hash(seed: u64, input: &[u8]) -> u64`. +//! `HashObject.rs` exposes `hash(seed: u64, input: &[u8]) -> u64`. pub struct RapidHash; diff --git a/src/hash/xxhash.rs b/src/hash/xxhash.rs index 19b24740dd05..9d6e00a8a76d 100644 --- a/src/hash/xxhash.rs +++ b/src/hash/xxhash.rs @@ -8,7 +8,7 @@ //! reference test vectors — verified against the reference (and across every //! dispatch target) by `test/js/bun/util/hash.test.js`, which runs in CI. //! -//! `HashObject.rs` exposes these via `hash_wrap` with a `(seed, bytes)` +//! `HashObject.rs` exposes these, which have a `(seed, bytes)` //! signature (seed first, unlike Murmur/CityHash). pub struct XxHash32; diff --git a/src/highway/lib.rs b/src/highway/lib.rs index 0ad57b358812..176285eb13dd 100644 --- a/src/highway/lib.rs +++ b/src/highway/lib.rs @@ -241,8 +241,7 @@ pub fn index_of_char(haystack: &[u8], needle: u8) -> Option { found } -/// [`index_of_char`] of bytes that another thread may be writing, which is why they are not a `&[u8]`. The byte found -/// can have changed by the time this returns. +/// [`index_of_char`] of bytes that another thread may be writing. The byte found can have changed by the time this returns. /// /// # Safety /// `len` bytes at `haystack` are readable. diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 2ed9d72dae8c..65fe2ea38e2d 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -262,14 +262,30 @@ export const bindgen = $bindgen("bindgen_test.bind.ts", "add") && { range: $bindgen("bindgen_test.bind.ts", "range"), words: $bindgen("bindgen_test.bind.ts", "words"), sumBytes: $bindgen("bindgen_test.bind.ts", "sumBytes"), - sumBytesOptional: $bindgen("bindgen_test.bind.ts", "sumBytesOptional"), sumBytesThenText: $bindgen("bindgen_test.bind.ts", "sumBytesThenText"), sumBeforeAndAfterCallback: $bindgen("bindgen_test.bind.ts", "sumBeforeAndAfterCallback"), - withValueEcho: $bindgen("bindgen_test.bind.ts", "withValueEcho"), fail: $bindgen("bindgen_test.bind.ts", "fail"), TestCounter: $bindgen("bindgen_test.bind.ts", "TestCounter"), TestHolder: $bindgen("bindgen_test.bind.ts", "TestHolder"), makeToken: $bindgen("bindgen_test.bind.ts", "makeToken"), + bytesOf: $bindgen("bindgen_test.bind.ts", "bytesOf"), + bytesOfOrToString: $bindgen("bindgen_test.bind.ts", "bytesOfOrToString"), + bytesOfString: $bindgen("bindgen_test.bind.ts", "bytesOfString"), + bytesOfBufferOrBlob: $bindgen("bindgen_test.bind.ts", "bytesOfBufferOrBlob"), + bytesOfRefusingUndefined: $bindgen("bindgen_test.bind.ts", "bytesOfRefusingUndefined"), + bytesOfThen: $bindgen("bindgen_test.bind.ts", "bytesOfThen"), + bytesOfThenCopyingShared: $bindgen("bindgen_test.bind.ts", "bytesOfThenCopyingShared"), + bytesOfThenCopied: $bindgen("bindgen_test.bind.ts", "bytesOfThenCopied"), + bytesOfAreSameMemory: $bindgen("bindgen_test.bind.ts", "bytesOfAreSameMemory"), + bytesOfAreSameMemoryCopyingShared: $bindgen("bindgen_test.bind.ts", "bytesOfAreSameMemoryCopyingShared"), + bytesBeforeAndAfterCallback: $bindgen("bindgen_test.bind.ts", "bytesBeforeAndAfterCallback"), + bytesBeforeAndAfterCallbackCopyingShared: $bindgen( + "bindgen_test.bind.ts", + "bytesBeforeAndAfterCallbackCopyingShared", + ), + pinnedBytesAfterCallback: $bindgen("bindgen_test.bind.ts", "pinnedBytesAfterCallback"), + stringAsIsOrBytes: $bindgen("bindgen_test.bind.ts", "stringAsIsOrBytes"), + asBigInt: $bindgen("bindgen_test.bind.ts", "asBigInt"), integerOrBigInt: $bindgen("bindgen_test.bind.ts", "integerOrBigInt"), looseInteger: $bindgen("bindgen_test.bind.ts", "looseInteger"), looseIntegerOrBigInt: $bindgen("bindgen_test.bind.ts", "looseIntegerOrBigInt"), diff --git a/src/jsc/Cargo.toml b/src/jsc/Cargo.toml index 048bce7fe77d..38ebcaff9485 100644 --- a/src/jsc/Cargo.toml +++ b/src/jsc/Cargo.toml @@ -25,7 +25,6 @@ bun_boringssl.workspace = true bun_alloc.workspace = true bun_errno.workspace = true bun_core.workspace = true -bun_highway.workspace = true bun_bundler.workspace = true bun_collections.workspace = true bun_crash_handler.workspace = true diff --git a/src/jsc/JSGlobalObject.rs b/src/jsc/JSGlobalObject.rs index 29b358b59b09..6e948798215a 100644 --- a/src/jsc/JSGlobalObject.rs +++ b/src/jsc/JSGlobalObject.rs @@ -146,7 +146,7 @@ impl JSGlobalObject { #[cold] #[inline(never)] pub fn throw_out_of_memory_value(&self) -> JSValue { - JSGlobalObject__throwOutOfMemoryError(self); + let _ = self.throw_out_of_memory(); JSValue::ZERO } diff --git a/src/jsc/bindgen.rs b/src/jsc/bindgen.rs index b499e987c29d..e570e133dba6 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -7,7 +7,7 @@ use core::ptr::NonNull; use crate::{ ArrayBuffer, ErrorCode, JSGlobalObject, JSObject, JSString, JSValue, JsError, JsThread, - MarkedArgumentBuffer, Strong, + MarkedArgumentBuffer, PinnedArrayBuffer, Strong, ThreadIsolated, ThreadIsolatedArg, }; #[path = "bindgen/extern_types.rs"] @@ -37,22 +37,33 @@ pub struct JsScope<'a> { } impl<'a> JsHeap<'a> { + /// For this module, which passes it to nothing that runs JavaScript except on behalf of a [`JsScope`]. #[inline(always)] - fn global(&self) -> &JSGlobalObject { + fn global_object(&self) -> &JSGlobalObject { // SAFETY: a `JsHeap` only exists at the address of a live global (see `JsScope::with`). unsafe { JSGlobalObject::opaque_ref_nn(core::ptr::from_ref(self).cast()) } } + /// For what takes the global object only to allocate or to throw. + /// + /// # Safety + /// Nothing it is passed to runs JavaScript. Throwing tells the debugger, which can, so after a throw the body + /// returns without reading what it borrowed. + #[inline(always)] + pub unsafe fn global(&self) -> &JSGlobalObject { + self.global_object() + } + #[doc(hidden)] #[inline(always)] pub fn global_ptr(&self) -> *mut JSGlobalObject { - self.global().as_mut_ptr() + self.global_object().as_mut_ptr() } #[inline] pub fn create_utf8_string(&self, utf8: &[u8]) -> Result<'a, JsLocal<'a, JSString>> { JsLocal::from_js_result(crate::bun_string_jsc::create_utf8_for_js( - self.global(), + self.global_object(), utf8, )) } @@ -64,33 +75,57 @@ impl<'a> JsHeap<'a> { length: usize, ) -> Result<'a, JsLocal<'a, BufferSource>> { JsLocal::from_js_result(JSValue::create_uninitialized_uint8_array( - self.global(), + self.global_object(), length, )) } + /// A `Uint8Array` that takes over the allocation of `bytes`. + #[inline] + pub fn create_uint8_array_from_box( + &self, + bytes: Box<[u8]>, + ) -> Result<'a, JsLocal<'a, BufferSource>> { + JsLocal::from_js_result(crate::JSUint8Array::from_bytes(self.global_object(), bytes)) + } + + /// A `Buffer` that takes over the allocation of `bytes`. + #[inline] + pub fn create_buffer_from_box( + &self, + bytes: Box<[u8]>, + ) -> Result<'a, JsLocal<'a, BufferSource>> { + JsLocal::from_js_result(JSValue::create_buffer_from_box(self.global_object(), bytes)) + } + /// An `Error` for the body to return. It is thrown after that. #[cold] pub fn error(&self, args: core::fmt::Arguments<'_>) -> Error<'a> { - Error::unthrown(self.global().create_error_instance(args)) + Error::unthrown(self.global_object().create_error_instance(args)) } /// As [`error`](Self::error), a `TypeError`. #[cold] pub fn type_error(&self, args: core::fmt::Arguments<'_>) -> Error<'a> { - Error::unthrown(self.global().create_type_error_instance(args)) + Error::unthrown(self.global_object().create_type_error_instance(args)) } /// As [`error`](Self::error), a `RangeError`. #[cold] pub fn range_error(&self, args: core::fmt::Arguments<'_>) -> Error<'a> { - Error::unthrown(self.global().create_range_error_instance(args)) + Error::unthrown(self.global_object().create_range_error_instance(args)) } /// As [`error`](Self::error), with a `code`, which also picks the class. #[cold] pub fn error_with_code(&self, code: ErrorCode, args: core::fmt::Arguments<'_>) -> Error<'a> { - Error::unthrown(code.fmt(self.global(), args)) + Error::unthrown(code.fmt(self.global_object(), args)) + } + + /// What [`JSGlobalObject::throw_invalid_arguments`] throws. + #[cold] + pub fn invalid_arguments(&self, args: core::fmt::Arguments<'_>) -> Error<'a> { + self.error_with_code(ErrorCode::INVALID_ARG_TYPE, args) } } @@ -110,13 +145,13 @@ impl<'a> JsScope<'a> { /// Anything can be done with a `&JSGlobalObject`, so it borrows the whole `JsScope`. #[inline(always)] pub fn global(&mut self) -> &JSGlobalObject { - self.heap.global() + self.heap.global_object() } /// For the APIs that take the tree's own proof of being on the JS thread. #[inline] pub fn js_thread(&mut self) -> JsThread<'_> { - self.heap.global().js_thread_of_caller_no_frame() + self.heap.global_object().js_thread_of_caller_no_frame() } } @@ -208,13 +243,13 @@ impl<'a> IntoError<'a> for JsError { impl<'a> IntoError<'a> for bun_sys::Error { #[cold] fn into_error(self, heap: &JsHeap<'a>) -> Error<'a> { - Error::unthrown(crate::SysErrorJsc::to_js(&self, heap.global())) + Error::unthrown(crate::SysErrorJsc::to_js(&self, heap.global_object())) } } impl<'a> IntoError<'a> for crate::SystemError { #[cold] fn into_error(self, heap: &JsHeap<'a>) -> Error<'a> { - Error::unthrown(self.to_error_instance(heap.global())) + Error::unthrown(self.to_error_instance(heap.global_object())) } } impl<'a, E: IntoError<'a>> IntoError<'a> for Box { @@ -274,7 +309,7 @@ impl<'a, T> JsLocal<'a, T> { /// Keeps the value past the call. #[inline] pub fn to_strong(self, heap: &JsHeap<'a>) -> Strong { - Strong::create(self.0, heap.global()) + Strong::create(self.0, heap.global_object()) } } @@ -313,7 +348,7 @@ impl<'a> JsLocal<'a, Callable> { ) -> Result<'a, JsLocal<'a>> { // SAFETY: `JsLocal` is `repr(transparent)` over `JSValue`. let args = unsafe { &*(core::ptr::from_ref(args) as *const [JSValue]) }; - JsLocal::from_js_result(self.0.call(cx.heap.global(), this.0, args)) + JsLocal::from_js_result(self.0.call(cx.heap.global_object(), this.0, args)) } } @@ -324,49 +359,142 @@ impl<'a> JsLocal<'a, BufferSource> { /// The bytes as they are now. A detached or out-of-bounds view is empty. #[inline] pub fn with_bytes(self, heap: &JsHeap<'a>, f: impl FnOnce(&[u8]) -> R) -> R { - let array_buffer = self.0.as_array_buffer(heap.global()); + let array_buffer = self.0.as_array_buffer(heap.global_object()); let result = f(array_buffer.as_ref().map_or(&[], ArrayBuffer::byte_slice)); self.0.ensure_still_alive(); result } } -/// The bytes of a buffer that may be a `SharedArrayBuffer`'s, which another thread can write at any time. There is no -/// `&[u8]` of them: a byte can differ each time it is read, and only what is written for that reads them. +/// `b.bytes` with `held: "handle"`, for a function that can run JavaScript, which can detach, resize or free a buffer. #[derive(Clone, Copy)] -pub struct PossiblySharedBytes<'a> { - data: *const u8, - length: usize, - lifetime: PhantomData<&'a ()>, +pub struct JsBytes<'a> { + /// What JavaScript does not change: the UTF-8 of a string, or a copy. `None` for a `Blob` or a buffer. + stable: Option<&'a [u8]>, + value: JsLocal<'a>, } -impl PossiblySharedBytes<'_> { - /// `None` if `range` starts beyond the end. +impl<'a> JsBytes<'a> { + /// # Safety + /// As [`ExternBytes::get`](extern_types::ExternBytes::get). + #[doc(hidden)] + #[inline(always)] + pub unsafe fn from_extern( + argument: extern_types::ExternWithValue, + heap: &JsHeap<'a>, + ) -> Self { + // SAFETY: forwarded. + unsafe { + Self { + stable: argument.converted.get_optional(heap), + value: JsLocal::from_extern(argument.value, heap), + } + } + } + + /// Those of a buffer are as they are now. A detached or out-of-bounds view is empty. #[inline] - pub fn get(self, range: core::ops::RangeFrom) -> Option { - let length = self.length.checked_sub(range.start)?; - // SAFETY: at most one past the end. - let data = unsafe { self.data.add(range.start) }; - Some(Self { - data, - length, - ..self - }) + pub fn get<'heap>(self, heap: &'heap JsHeap<'a>) -> &'heap [u8] { + match self.stable { + Some(bytes) => bytes, + // SAFETY: the frame keeps `value` alive, and only JavaScript moves its bytes, which takes the `&mut JsScope` that `'heap` borrows from. + None => unsafe { + extern_types::Bun__Bindgen__bytesOfBlobOrBuffer(self.value.0).get(heap) + }, + } } +} - /// The byte there can have changed by the time this returns. +/// `b.bytes` with `string: "asIs"`. +pub enum StringOrJsBytes<'a> { + /// Latin-1 or UTF-16. + String(bun_core::String), + Bytes(JsBytes<'a>), +} + +impl<'a> StringOrJsBytes<'a> { + /// # Safety + /// As [`ExternBytes::get`](extern_types::ExternBytes::get). The string's ref becomes the result's. + #[doc(hidden)] #[inline] - pub fn index_of_char(self, needle: u8) -> Option { - // SAFETY: readable for `'a`. - unsafe { bun_highway::index_of_char_raw(self.data, self.length, needle) } + pub unsafe fn from_extern( + argument: extern_types::ExternWithValue, + heap: &JsHeap<'a>, + ) -> Self { + if !argument.converted.is_null() { + return Self::String(extern_types::adopt_string(argument.converted)); + } + Self::Bytes(JsBytes { + stable: None, + // SAFETY: forwarded. + value: unsafe { JsLocal::from_extern(argument.value, heap) }, + }) + } + + /// `held: "copied"`. + #[doc(hidden)] + pub fn into_vec(self, heap: &JsHeap<'a>) -> Vec { + let copy = match self { + Self::String(string) => string.to_owned_slice(), + Self::Bytes(bytes) => bytes.get(heap).to_vec(), + }; + heap.global_object().vm().report_extra_memory(copy.len()); + copy } } -/// An argument converted for use now, and the value itself for whatever holds it longer. -#[derive(Clone, Copy)] -pub struct WithValue<'a, T> { - pub converted: T, - pub value: JsLocal<'a>, +/// `b.bytes` with `held: "pinned"`, for a job on another thread to read. It is dropped on the JS thread. +pub struct PinnedBytes(ThreadIsolated); + +enum Pinned { + /// Thread-isolated. + String(bun_core::Utf8WithString), + /// GC-rooted too. A resizable one is copied. + Buffer(PinnedArrayBuffer), +} + +// SAFETY: `PinnedBytes::from_extern` builds it so. +unsafe impl ThreadIsolatedArg for Pinned {} + +impl PinnedBytes { + /// # Safety + /// As [`StringOrJsBytes::from_extern`]. What is not a string is a buffer. + #[doc(hidden)] + pub unsafe fn from_extern<'a>( + argument: extern_types::ExternWithValue, + heap: &JsHeap<'a>, + ) -> Result<'a, Self> { + let global = heap.global_object(); + // SAFETY: forwarded. + let pinned = match unsafe { StringOrJsBytes::from_extern(argument, heap) } { + StringOrJsBytes::Bytes(bytes) => { + match PinnedArrayBuffer::root_read_only(global, bytes.value.0) { + Some(buffer) => Pinned::Buffer(buffer), + None => return Err(global.throw_out_of_memory().into()), + } + } + StringOrJsBytes::String(string) => { + let utf8 = string.into_utf8_with_string_thread_isolated(); + if !utf8.is_shared() { + global.vm().report_extra_memory(utf8.slice().len()); + } + Pinned::String(utf8) + } + }; + // SAFETY: the string is thread-isolated, and the buffer pinned and GC-rooted. + Ok(Self(unsafe { ThreadIsolated::new(pinned) })) + } +} + +impl core::ops::Deref for PinnedBytes { + type Target = [u8]; + #[inline] + fn deref(&self) -> &[u8] { + match &*self.0 { + Pinned::String(utf8) => utf8.slice(), + Pinned::Buffer(buffer) => buffer.slice(), + } + } } // ───────────────────────────── classes ───────────────────────────── @@ -565,6 +693,14 @@ pub mod to_js { f64: f64 => |n| JSValue::js_number(JSValue::purify_nan(n)); } + #[inline] + pub fn u64_as_bigint<'a>(heap: &JsHeap<'a>, value: u64) -> Result<'a, JsLocal<'a>> { + JsLocal::from_js_result(JSValue::from_uint64_no_truncate( + heap.global_object(), + value, + )) + } + #[inline(always)] pub fn undefined<'a>(_: &JsHeap<'a>, (): ()) -> Result<'a, JsLocal<'a>> { Ok(JsLocal::UNDEFINED) @@ -616,7 +752,7 @@ pub mod to_js { #[inline] pub fn string<'a>(heap: &JsHeap<'a>, value: bun_core::String) -> Result<'a, JsLocal<'a>> { - JsLocal::from_js_result(crate::StringJsc::into_js(value, heap.global())) + JsLocal::from_js_result(crate::StringJsc::into_js(value, heap.global_object())) } #[inline] @@ -632,7 +768,7 @@ pub mod to_js { let Some(item) = items.next() else { // SAFETY: `count` initialized values. return JsLocal::from_js_result(crate::call_zero_is_throw( - heap.global(), + heap.global_object(), || unsafe { extern_types::Bun__Bindgen__constructArrayFromValues( heap.global_ptr(), @@ -652,7 +788,7 @@ pub mod to_js { values.append(convert(heap, item)?.0); } // SAFETY: `values` is live for the call. - JsLocal::from_js_result(crate::call_zero_is_throw(heap.global(), || unsafe { + JsLocal::from_js_result(crate::call_zero_is_throw(heap.global_object(), || unsafe { extern_types::Bun__Bindgen__constructArray(heap.global_ptr(), values) })) }) @@ -711,7 +847,7 @@ pub fn host_fn_return<'a, T, E: IntoError<'a>>( }; debug_assert_eq!( value.is_empty(), - cx.heap.global().has_exception(), + cx.heap.global_object().has_exception(), "a body fails exactly when an exception is pending", ); value diff --git a/src/jsc/bindgen/extern_types.rs b/src/jsc/bindgen/extern_types.rs index 10e16afac138..cc35fcf650f2 100644 --- a/src/jsc/bindgen/extern_types.rs +++ b/src/jsc/bindgen/extern_types.rs @@ -1,9 +1,8 @@ //! Rust side of `src/jsc/bindings/Bindgen/Extern*.h`. The generated conversion that reads a value takes ownership. use core::ffi::{c_uint, c_void}; -use core::marker::PhantomData; -use super::{JsHeap, PossiblySharedBytes}; +use super::JsHeap; use crate::{JSGlobalObject, JSValue, MarkedArgumentBuffer}; /// `WTF::StringImpl*` carrying one ref, or null. @@ -84,7 +83,7 @@ impl ExternVector { } } -/// Bytes in the C++ host function's frame, or in a JS value that frame keeps alive, until it returns. +/// Bytes in the C++ host function's frame, or in a JS value that frame keeps alive, until it returns. Another thread can write those of a shared buffer. #[repr(C)] #[derive(Clone, Copy)] pub struct ExternBytes { @@ -102,16 +101,6 @@ impl ExternBytes { /// # Safety /// As [`get`](Self::get). #[inline(always)] - pub unsafe fn get_possibly_shared<'a>(self, _: &JsHeap<'a>) -> PossiblySharedBytes<'a> { - PossiblySharedBytes { - data: self.data, - length: self.length, - lifetime: PhantomData, - } - } - /// # Safety - /// As [`get`](Self::get). - #[inline(always)] pub unsafe fn get_optional<'a>(self, heap: &JsHeap<'a>) -> Option<&'a [u8]> { if self.data.is_null() { return None; @@ -129,6 +118,8 @@ pub struct ExternWithValue { } unsafe extern "C" { + /// `value` is a `BufferSource` or a `Blob` whose bytes are in memory. + pub(super) safe fn Bun__Bindgen__bytesOfBlobOrBuffer(value: JSValue) -> ExternBytes; pub(super) fn Bun__Bindgen__constructArray( global: *mut JSGlobalObject, values: *mut MarkedArgumentBuffer, diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index 2bfae182a4d1..7e7fc66eb5ed 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -71,11 +71,10 @@ export const sumArray = b.fn({ args: { numbers: b.array(b.i32) }, returns: b.f64 export const range = b.fn({ args: { length: b.u32 }, returns: b.array(b.u32) }); export const words = b.fn({ args: { text: b.string.utf8 }, returns: b.array(b.string.utf8) }); -export const sumBytes = b.fn({ args: { bytes: b.bufferSource.bytes }, returns: b.u32 }); -export const sumBytesOptional = b.fn({ args: { bytes: b.bufferSource.bytes.nullable }, returns: b.u32.nullable }); +export const sumBytes = b.fn({ args: { bytes: b.bytes({ from: ["buffer"] }) }, returns: b.u32 }); /** Converting `text` can run JavaScript. */ export const sumBytesThenText = b.fn({ - args: { bytes: b.bufferSource.bytes, text: b.string.loose.utf8 }, + args: { bytes: b.bytes({ from: ["buffer"] }), text: b.string.loose.utf8 }, returns: b.u32, }); /** The sum before `callback` runs, then after it. */ @@ -86,14 +85,71 @@ export const sumBeforeAndAfterCallback = b.fn({ throws: true, }); -/** The original value if the converted text is not empty. */ -export const withValueEcho = b.fn({ - args: { text: b.string.loose.utf8.withValue }, - returns: b.any, +type BytesOptions = Parameters[0]; +const bytesOfWith = (options: BytesOptions) => + b.fn({ args: { data: b.bytes(options) }, returns: b.array(b.u8), rustPath: "crate::bindgen_test::bytes_of" }); +export const bytesOf = bytesOfWith({ from: ["string", "buffer"] }); +export const bytesOfString = bytesOfWith({ from: ["string"] }); +export const bytesOfBufferOrBlob = bytesOfWith({ from: ["buffer", "blob"] }); +export const bytesOfOrToString = bytesOfWith({ from: ["string", "buffer", "blob"], otherwise: "toString" }); +export const bytesOfRefusingUndefined = bytesOfWith({ + from: ["string", "buffer"], + otherwise: "toString", + refuse: ["undefined"], +}); +/** `after` is converted after `data`, and can run JavaScript. */ +const bytesOfThenWith = (options: Omit, rustName: string) => + b.fn({ + args: { data: b.bytes({ from: ["string", "buffer", "blob"], ...options }), after: b.f64.loose }, + returns: b.array(b.u8), + rustPath: `crate::bindgen_test::${rustName}`, + }); +export const bytesOfThen = bytesOfThenWith({}, "bytes_of_then"); +export const bytesOfThenCopyingShared = bytesOfThenWith({ shared: "copied" }, "bytes_of_then"); +export const bytesOfThenCopied = bytesOfThenWith({ held: "copied" }, "bytes_of_then_copied"); +/** Given one value twice: whether both are its own memory, and not a copy each. */ +const bytesOfAreSameMemoryWith = (shared: BytesOptions["shared"]) => { + const data = b.bytes({ from: ["string", "buffer", "blob"], shared }); + return b.fn({ + args: { data, again: data }, + returns: b.bool, + rustPath: "crate::bindgen_test::bytes_of_are_same_memory", + }); +}; +export const bytesOfAreSameMemory = bytesOfAreSameMemoryWith("borrowed"); +export const bytesOfAreSameMemoryCopyingShared = bytesOfAreSameMemoryWith("copied"); +/** The bytes before `callback` runs, then after it. */ +const bytesBeforeAndAfterCallbackWith = (shared: BytesOptions["shared"]) => + b.fn({ + args: { + data: b.bytes({ from: ["string", "buffer", "blob"], otherwise: "toString", shared, held: "handle" }), + callback: b.any, + }, + returns: b.array(b.array(b.u8)), + capability: "js", + throws: true, + rustPath: "crate::bindgen_test::bytes_before_and_after_callback", + }); +export const bytesBeforeAndAfterCallback = bytesBeforeAndAfterCallbackWith("borrowed"); +export const bytesBeforeAndAfterCallbackCopyingShared = bytesBeforeAndAfterCallbackWith("copied"); +/** The bytes another thread reads after `callback` has run. */ +export const pinnedBytesAfterCallback = b.fn({ + args: { data: b.bytes({ from: ["string", "buffer"], held: "pinned" }), callback: b.any }, + returns: b.array(b.u8), + capability: "js", + throws: true, +}); +/** The size of a code unit of a string, 0 for anything else, then the bytes. */ +export const stringAsIsOrBytes = b.fn({ + args: { + data: b.bytes({ from: ["string", "buffer", "blob"], otherwise: "toString", string: "asIs", held: "handle" }), + }, + returns: b.array(b.u8), capability: "heap", }); +export const asBigInt = b.fn({ args: { value: b.u64.allowBigInt }, returns: b.u64.asBigInt }); -export const TestFailure = b.enumeration(["type", "range", "plain", "syscall", "value"]); +export const TestFailure = b.enumeration(["type", "range", "plain", "code", "syscall", "outOfMemory", "value"]); export const fail = b.fn({ args: { how: TestFailure, value: b.any }, capability: "heap", throws: true }); export const TestCounter = b.class({ diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index ec04047fc175..984bdcce039a 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -3,8 +3,8 @@ use core::sync::atomic::{AtomicUsize, Ordering}; use crate::bindgen::{ - self, BufferSource, IntoError as _, JsHeap, JsLocal, JsPayload as _, JsScope, JsWrapper, - JsWrapperStrong, WithValue, + self, BufferSource, IntoError as _, JsBytes, JsHeap, JsLocal, JsPayload as _, JsScope, + JsWrapper, JsWrapperStrong, PinnedBytes, StringOrJsBytes, }; use crate::generated_bindgen::bindgen_test::{ TestAnything, TestChoice, TestColor, TestColorOrRest, TestCounterClass, @@ -202,10 +202,6 @@ pub(crate) fn sum_bytes(bytes: &[u8]) -> u32 { sum(bytes) } -pub(crate) fn sum_bytes_optional(bytes: Option<&[u8]>) -> Option { - bytes.map(sum) -} - pub(crate) fn sum_bytes_then_text(bytes: &[u8], text: &[u8]) -> u32 { sum(bytes) + sum(text) } @@ -216,20 +212,72 @@ pub(crate) fn sum_before_and_after_callback<'a>( callback: JsLocal<'a>, ) -> bindgen::Result<'a, [u32; 2]> { let before = buffer.with_bytes(&cx.heap, sum); + call(cx, callback)?; + Ok([before, buffer.with_bytes(&cx.heap, sum)]) +} + +pub(crate) fn bytes_of(data: &[u8]) -> Vec { + data.to_vec() +} + +pub(crate) fn bytes_of_then(data: &[u8], _: f64) -> Vec { + data.to_vec() +} + +pub(crate) fn bytes_of_then_copied(data: Vec, _: f64) -> Vec { + data +} + +fn call<'a>(cx: &mut JsScope<'a>, callback: JsLocal<'a>) -> bindgen::Result<'a, ()> { let Some(callback) = callback.as_callable() else { return Err(cx .heap .type_error(format_args!("callback must be a function"))); }; callback.call(cx, JsLocal::UNDEFINED, &[])?; - Ok([before, buffer.with_bytes(&cx.heap, sum)]) + Ok(()) } -pub(crate) fn with_value_echo<'a>(_: &JsHeap<'a>, text: WithValue<'a, &'a [u8]>) -> JsLocal<'a> { - if text.converted.is_empty() { - return JsLocal::UNDEFINED; +pub(crate) fn bytes_before_and_after_callback<'a>( + cx: &mut JsScope<'a>, + data: JsBytes<'a>, + callback: JsLocal<'a>, +) -> bindgen::Result<'a, [Vec; 2]> { + let before = data.get(&cx.heap).to_vec(); + call(cx, callback)?; + Ok([before, data.get(&cx.heap).to_vec()]) +} + +pub(crate) fn pinned_bytes_after_callback<'a>( + cx: &mut JsScope<'a>, + data: PinnedBytes, + callback: JsLocal<'a>, +) -> bindgen::Result<'a, Vec> { + call(cx, callback)?; + let (bytes, data) = std::thread::scope(|scope| { + scope + .spawn(move || (data.to_vec(), data)) + .join() + .expect("the thread does not panic") + }); + drop(data); + Ok(bytes) +} + +pub(crate) fn string_as_is_or_bytes<'a>(heap: &JsHeap<'a>, data: StringOrJsBytes<'a>) -> Vec { + match data { + StringOrJsBytes::Bytes(bytes) => [&[0], bytes.get(heap)].concat(), + StringOrJsBytes::String(text) if text.is_8bit() => [&[1], text.latin1()].concat(), + StringOrJsBytes::String(text) => [&[2], bytemuck::cast_slice(text.utf16())].concat(), } - text.value +} + +pub(crate) fn bytes_of_are_same_memory(data: &[u8], again: &[u8]) -> bool { + core::ptr::eq(data, again) +} + +pub(crate) fn as_big_int(value: u64) -> u64 { + value } pub(crate) fn fail<'a>( @@ -241,6 +289,11 @@ pub(crate) fn fail<'a>( TestFailure::Type => heap.type_error(format_args!("a type error")), TestFailure::Range => heap.range_error(format_args!("a range error")), TestFailure::Plain => heap.error(format_args!("an error")), + TestFailure::Code => heap.error_with_code( + crate::ErrorCode::INVALID_ARG_VALUE, + format_args!("an error with a code"), + ), + TestFailure::OutOfMemory => bun_alloc::AllocError.into(), TestFailure::Syscall => { bun_sys::Error::from_code(bun_sys::E::ENOENT, bun_sys::Tag::open).into_error(heap) } diff --git a/src/jsc/bindings/Bindgen/ExternTraits.h b/src/jsc/bindings/Bindgen/ExternTraits.h index f1d52b4c3914..7fa664359dee 100644 --- a/src/jsc/bindings/Bindgen/ExternTraits.h +++ b/src/jsc/bindings/Bindgen/ExternTraits.h @@ -7,6 +7,7 @@ #include #include #include +#include "Bindgen/JSBytesView.h" #include "Bindgen/JSStringUTF8View.h" namespace Bun::Bindgen { @@ -120,6 +121,25 @@ template<> struct ExternTraits { } }; +template<> struct ExternTraits { + using ExternType = ExternBytes; + + // As for JSStringUTF8View. + static ExternType convertToExtern(JSBytesView&& cppValue) + { + return ExternTraits>::convertToExtern(cppValue.bytes()); + } +}; + +template<> struct ExternTraits { + using ExternType = WTF::StringImpl*; + + static ExternType convertToExtern(JSBlobOrStringOrBuffer&& cppValue) + { + return ExternTraits::convertToExtern(cppValue.takeString()); + } +}; + // A converted argument and the argument itself. template struct ExternWithValue { diff --git a/src/jsc/bindings/Bindgen/IDLConvert.h b/src/jsc/bindings/Bindgen/IDLConvert.h index b70d71a58727..dadf0313b3aa 100644 --- a/src/jsc/bindings/Bindgen/IDLConvert.h +++ b/src/jsc/bindings/Bindgen/IDLConvert.h @@ -42,10 +42,9 @@ template<> struct IDLHumanReadableName : BaseIDLHu static constexpr auto humanReadableName = std::to_array("string"); }; template<> struct IDLHumanReadableName : IDLHumanReadableName {}; -template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { static constexpr auto humanReadableName = std::to_array("ArrayBuffer or ArrayBufferView"); }; -template<> struct IDLHumanReadableName : IDLHumanReadableName {}; namespace Bindgen { @@ -72,49 +71,49 @@ inline std::optional> unsharedBufferSourceSpan(JSC::VM& return std::nullopt; } -// As unsharedBufferSourceSpan, and the bytes of a shared buffer, which another thread can write. -inline std::optional> possiblySharedBufferSourceSpan(JSC::VM& vm, JSC::JSValue value) -{ - if (auto* view = dynamicDowncast(value)) - return view->span(); - if (auto* buffer = JSC::toPossiblySharedArrayBuffer(vm, value)) - return buffer->span(); - return std::nullopt; -} - -// Null data is the null value of IDLBufferSourceBytes. -inline std::optional> withNonNullData(std::optional> bytes) -{ - if (bytes && !bytes->data()) - return byteCast(WTF::nonNullEmptyUTF8Span()); - return bytes; -} - } } namespace Bun::Bindgen::Detail { -template -struct UTF8StringConverter : DefaultContextConverter { +template +struct BytesConverter : DefaultContextConverter { + using Result = typename IDL::ImplementationType; + + // `expected` is what `options` takes without `ToString`, for the message. template - ALWAYS_INLINE static void convertInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, JSStringUTF8View& result) + ALWAYS_INLINE static void convertInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, Result& result, WTF::ASCIILiteral expected) { - if (value.isString()) [[likely]] - return result.initialize(globalObject, JSC::asString(value)); - convertIntoSlow(globalObject, value, ctx, result); + if constexpr (options.fromString) { + if (value.isString()) + return result.initialize(globalObject, JSC::asString(value)); + } + if constexpr (options.fromBuffer) { + if (value.inherits() || value.inherits()) + return result.initializeAsBlobOrBuffer(); + } + convertIntoSlow(globalObject, value, ctx, result, expected); } private: template - NEVER_INLINE static void convertIntoSlow(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, JSStringUTF8View& result) + NEVER_INLINE static void convertIntoSlow(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, Result& result, WTF::ASCIILiteral expected) { auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); - if constexpr (std::same_as) { - if (!value.inherits()) { - ctx.throwNotString(globalObject, scope); - return; - } + if constexpr (options.fromBlob) { + bool isBlob = checkInMemoryBlob(globalObject, value); + RETURN_IF_EXCEPTION(scope, void()); + if (isBlob) + return result.initializeAsBlobOrBuffer(); + } + bool isRefused; + if constexpr (options.toStringOtherwise) + isRefused = (options.refuseUndefined && value.isUndefined()) || (options.refuseNull && value.isNull()); + else + isRefused = !options.fromString || !value.inherits(); + if (isRefused) { + ctx.throwTypeMustBe(globalObject, scope, expected); + return; } JSC::JSString* string = value.toString(&globalObject); RETURN_IF_EXCEPTION(scope, void()); @@ -123,8 +122,13 @@ struct UTF8StringConverter : DefaultContextConverter { }; } -template<> struct WebCore::Converter : Bun::Bindgen::Detail::UTF8StringConverter {}; -template<> struct WebCore::Converter : Bun::Bindgen::Detail::UTF8StringConverter {}; +template +struct WebCore::Converter> + : Bun::Bindgen::Detail::BytesConverter, options> {}; +template<> struct WebCore::Converter + : Bun::Bindgen::Detail::BytesConverter {}; +template<> struct WebCore::Converter + : Bun::Bindgen::Detail::BytesConverter {}; template<> struct WebCore::Converter : Bun::DefaultTryConverter { template @@ -142,34 +146,6 @@ template<> struct WebCore::Converter struct WebCore::Converter : Bun::DefaultTryConverter { - template - static std::optional> tryConvert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx&) - { - return Bun::Bindgen::withNonNullData(Bun::Bindgen::unsharedBufferSourceSpan(JSC::getVM(&globalObject), value)); - } - - template - static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) - { - Bun::Bindgen::throwNotUnsharedBufferSource(globalObject, scope, ctx); - } -}; - -template<> struct WebCore::Converter> : Bun::DefaultTryConverter> { - template - static std::optional> tryConvert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx&) - { - return Bun::Bindgen::withNonNullData(Bun::Bindgen::possiblySharedBufferSourceSpan(JSC::getVM(&globalObject), value)); - } - - template - static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) - { - ctx.throwTypeMustBe(globalObject, scope, "an ArrayBuffer or ArrayBufferView"_s); - } -}; - template<> struct WebCore::Converter : Bun::DefaultTryConverter { template static std::optional tryConvert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx&) diff --git a/src/jsc/bindings/Bindgen/IDLTypes.h b/src/jsc/bindings/Bindgen/IDLTypes.h index 023bf4240c6b..9ca524a2c611 100644 --- a/src/jsc/bindings/Bindgen/IDLTypes.h +++ b/src/jsc/bindings/Bindgen/IDLTypes.h @@ -1,4 +1,5 @@ #pragma once +#include "Bindgen/JSBytesView.h" #include "Bindgen/JSStringUTF8View.h" // WebCore's. In quotes it would be this file. #include @@ -99,15 +100,25 @@ struct IDLLooseUTF8String : IDLStrictUTF8String {}; // The values IDLStrictUTF8String takes, as the WTF::String. struct IDLStrictStringOrStringObject : WebCore::IDLDOMString {}; -// The bytes of a BufferSource that is not shared. Valid while the argument is alive and no JavaScript runs. -struct IDLBufferSourceBytes : WebCore::IDLType> { +// `b.bytes`: which values it takes. +struct BytesOptions { + // Without `ToString`. A string: what IDLStrictUTF8String takes. A buffer: a BufferSource, shared or not. + bool fromString { false }; + bool fromBuffer { false }; + bool fromBlob { false }; + // `ToString` of anything else, except the two below. Without it, anything else is refused. + bool toStringOtherwise { false }; + bool refuseUndefined { false }; + bool refuseNull { false }; +}; + +// `Sink` is JSBytesView, where a string is its UTF-8, or JSBlobOrStringOrBuffer, where it is the WTF::String. +template +struct IDLBytes : WebCore::IDLType { using SequenceStorageType = void; - using NullableType = std::span; - using NullableInnerParameterType = std::span; - static NullableType nullValue() { return {}; } }; -// The same values, as the `JSValue` itself. See IDLRawAny. +// A BufferSource that is not shared, as the `JSValue` itself. See IDLRawAny. struct IDLRawBufferSource : WebCore::IDLType { using SequenceStorageType = void; }; diff --git a/src/jsc/bindings/Bindgen/JSBytesView.cpp b/src/jsc/bindings/Bindgen/JSBytesView.cpp new file mode 100644 index 000000000000..f200e3df368d --- /dev/null +++ b/src/jsc/bindings/Bindgen/JSBytesView.cpp @@ -0,0 +1,79 @@ +#include "root.h" +#include "Bindgen/JSBytesView.h" +#include "Bindgen/ExternTraits.h" +#include "ErrorCode.h" +#include "ZigGeneratedClasses.h" +#include "blob.h" +#include +#include + +namespace Bun::Bindgen { +using namespace JSC; + +bool checkInMemoryBlob(JSGlobalObject& globalObject, JSValue value) +{ + if (!value.inherits()) + return false; + const uint8_t* data = nullptr; + size_t length = 0; + if (!WebCore::Blob__sharedView(JSValue::encode(value), &data, &length)) { + auto scope = DECLARE_THROW_SCOPE(getVM(&globalObject)); + Bun::throwError(&globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "File blob cannot be used here"_s); + return false; + } + return true; +} + +std::span bytesOfBlobOrBuffer(JSValue value) +{ + std::span bytes; + if (auto* view = dynamicDowncast(value)) { + bytes = view->span(); + } else if (auto* buffer = dynamicDowncast(value)) { + bytes = buffer->impl()->span(); + } else { + const uint8_t* data = nullptr; + size_t length = 0; + WebCore::Blob__sharedView(JSValue::encode(value), &data, &length); + bytes = { data, length }; + } + // A Rust slice has no null data. + if (!bytes.data()) + return byteCast(WTF::nonNullEmptyUTF8Span()); + return bytes; +} + +static bool isSharedBuffer(JSValue value) +{ + if (auto* view = dynamicDowncast(value)) + return view->isShared(); + if (auto* buffer = dynamicDowncast(value)) + return buffer->isShared(); + return false; +} + +void JSBytesView::copyIfShared(JSGlobalObject& globalObject, JSValue argument, MallocSpan& copy) +{ + if (!m_isBlobOrBuffer || !isSharedBuffer(argument)) + return; + m_bytes = bytesOfBlobOrBuffer(argument); + if (m_bytes.empty()) + return; + // Not a Vector, which holds less than a buffer can. + copy = MallocSpan::tryMalloc(m_bytes.size()); + if (!copy) [[unlikely]] { + auto scope = DECLARE_THROW_SCOPE(getVM(&globalObject)); + throwOutOfMemoryError(&globalObject, scope); + return; + } + memcpySpan(copy.mutableSpan(), m_bytes); + m_bytes = copy.span(); +} + +} + +extern "C" Bun::Bindgen::ExternBytes Bun__Bindgen__bytesOfBlobOrBuffer(JSC::EncodedJSValue value) +{ + auto bytes = Bun::Bindgen::bytesOfBlobOrBuffer(JSC::JSValue::decode(value)); + return { bytes.data(), bytes.size() }; +} diff --git a/src/jsc/bindings/Bindgen/JSBytesView.h b/src/jsc/bindings/Bindgen/JSBytesView.h new file mode 100644 index 000000000000..6df4d5a3903f --- /dev/null +++ b/src/jsc/bindings/Bindgen/JSBytesView.h @@ -0,0 +1,77 @@ +#pragma once +#include "root.h" +#include "Bindgen/JSStringUTF8View.h" +#include +#include + +namespace Bun::Bindgen { + +// The bytes of a BufferSource or of a Blob whose bytes are in memory, where they are now. The data is not null. +std::span bytesOfBlobOrBuffer(JSC::JSValue); + +// Whether it is a Blob. Throws for one whose bytes are not in memory: a file. +bool checkInMemoryBlob(JSC::JSGlobalObject&, JSC::JSValue); + +// The UTF-8 of a string, or the bytes of a Blob or a buffer where they are, which only JavaScript and a thread that writes a shared buffer change. +class JSBytesView { + WTF_FORBID_HEAP_ALLOCATION; + WTF_MAKE_NONCOPYABLE(JSBytesView); + WTF_MAKE_NONMOVABLE(JSBytesView); + +public: + // Not `= default`: see JSStringUTF8View. + JSBytesView() {} + + void initialize(JSC::JSGlobalObject& globalObject, JSC::JSString* string) + { + m_string.initialize(globalObject, string); + } + + void initializeAsBlobOrBuffer() + { + ASSERT(!m_isBlobOrBuffer); + m_isBlobOrBuffer = true; + } + + // Converting a later argument can run JavaScript, so these two come after all of them. + ALWAYS_INLINE void read(JSC::JSValue argument) + { + if (m_isBlobOrBuffer) + m_bytes = bytesOfBlobOrBuffer(argument); + } + + // The bytes of a shared buffer become those of `copy`. Allocating it can throw. + void copyIfShared(JSC::JSGlobalObject&, JSC::JSValue argument, WTF::MallocSpan& copy); + + // Null data for a Blob or a buffer that was neither read nor copied: Rust reads it, each time it borrows the bytes. + std::span bytes() const LIFETIME_BOUND + { + return m_isBlobOrBuffer ? m_bytes : m_string.bytes(); + } + +private: + JSStringUTF8View m_string; + std::span m_bytes; + bool m_isBlobOrBuffer { false }; +}; + +// The string, or null for a Blob or a buffer, which Rust reads, copies or pins from the argument itself. +class JSBlobOrStringOrBuffer { + WTF_FORBID_HEAP_ALLOCATION; + +public: + // Resolving a rope can throw. + void initialize(JSC::JSGlobalObject& globalObject, JSC::JSString* string) + { + m_string = string->value(&globalObject); + } + + void initializeAsBlobOrBuffer() {} + + WTF::String takeString() { return std::exchange(m_string, {}); } + +private: + WTF::String m_string; +}; + +} diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp b/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp index 0994e14509cf..6cee6b2c1c4a 100644 --- a/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp @@ -6,27 +6,35 @@ namespace Bun::Bindgen { using namespace JSC; +std::span JSStringUTF8View::tryAllocate(size_t size) +{ + if (size <= m_inlineBuffer.size()) + return m_inlineBuffer; + m_heapBuffer = MallocSpan::tryMalloc(size); + return m_heapBuffer->mutableSpan(); +} + bool JSStringUTF8View::convert() { StringView view = m_view; m_isConverted = true; if (view.is8Bit()) { auto span = view.span8(); - if (!m_converted.tryGrow(span.size() * 2)) + auto buffer = tryAllocate(span.size() * 2); + if (!buffer.data()) return false; - m_converted.shrink(simdutf::convert_latin1_to_utf8(byteCast(span), m_converted.mutableSpan())); + m_converted = buffer.first(simdutf::convert_latin1_to_utf8(byteCast(span), buffer)); return true; } auto span = view.span16(); - if (!m_converted.tryGrow(span.size() * 3)) + auto buffer = tryAllocate(span.size() * 3); + if (!buffer.data()) return false; - size_t length = StringImpl::tryConvertUTF16ToUTF8(span, m_converted.mutableSpan()); - if (length != notFound) [[likely]] { - m_converted.shrink(length); - return true; - } + size_t length = StringImpl::tryConvertUTF16ToUTF8(span, buffer); // simdutf refuses unpaired surrogates; each becomes U+FFFD. - m_converted.shrink(WTF::Unicode::convertReplacingInvalidSequences(span, m_converted.mutableSpan()).buffer.size()); + if (length == notFound) [[unlikely]] + length = WTF::Unicode::convertReplacingInvalidSequences(span, buffer).buffer.size(); + m_converted = buffer.first(length); return true; } diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.h b/src/jsc/bindings/Bindgen/JSStringUTF8View.h index 03d4a6a9462b..bd34cee959f5 100644 --- a/src/jsc/bindings/Bindgen/JSStringUTF8View.h +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.h @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace Bun::Bindgen { @@ -44,7 +45,7 @@ class JSStringUTF8View { { if (m_isNull) return {}; - auto span = byteCast(m_isConverted ? m_converted.span() : byteCast(m_view->span8())); + auto span = byteCast(m_isConverted ? m_converted : byteCast(m_view->span8())); if (!span.data()) return byteCast(WTF::nonNullEmptyUTF8Span()); return span; @@ -52,10 +53,15 @@ class JSStringUTF8View { private: bool convert(); + std::span tryAllocate(size_t); // The owner is not always the string: the characters of a substring are its base's. JSC::GCOwnedDataScope m_view; - WTF::Vector m_converted; + // Not a Vector with inline capacity, which holds less than the UTF-8 of a string can be. + std::array m_inlineBuffer; + // Optional: ~MallocSpan calls free, of null too. + std::optional> m_heapBuffer; + std::span m_converted; bool m_isConverted { false }; bool m_isNull { true }; }; diff --git a/src/jsc/bindings/BunObject+exports.h b/src/jsc/bindings/BunObject+exports.h index 100303d35947..22bb11410969 100644 --- a/src/jsc/bindings/BunObject+exports.h +++ b/src/jsc/bindings/BunObject+exports.h @@ -10,8 +10,6 @@ macro(FileSystemRouter) \ macro(Glob) \ macro(Image) \ - macro(JSON5) \ - macro(JSONC) \ macro(MD4) \ macro(markdown) \ macro(MD5) \ @@ -22,9 +20,6 @@ macro(SHA384) \ macro(SHA512) \ macro(SHA512_256) \ - macro(TOML) \ - macro(XML) \ - macro(YAML) \ macro(Terminal) \ macro(Transpiler) \ macro(ValkeyClient) \ @@ -33,7 +28,6 @@ macro(cwd) \ macro(embeddedFiles) \ macro(enableANSIColors) \ - macro(hash) \ macro(inspect) \ macro(isStandaloneExecutable) \ macro(origin) \ @@ -47,11 +41,7 @@ macro(connect) \ macro(createParsedShellScript) \ macro(createShellInterpreter) \ - macro(deflateSync) \ macro(file) \ - macro(gunzipSync) \ - macro(gzipSync) \ - macro(inflateSync) \ macro(jest) \ macro(listen) \ macro(registerMacro) \ @@ -63,10 +53,6 @@ macro(spawnSync) \ macro(udpSocket) \ macro(write) \ - macro(zstdCompressSync) \ - macro(zstdDecompressSync) \ - macro(zstdCompress) \ - macro(zstdDecompress) \ #define DECLARE_ZIG_BUN_OBJECT_CALLBACK(name) BUN_DECLARE_HOST_FUNCTION(BunObject_callback_##name); FOR_EACH_CALLBACK(DECLARE_ZIG_BUN_OBJECT_CALLBACK); diff --git a/src/jsc/bindings/BunObject.cpp b/src/jsc/bindings/BunObject.cpp index 15fec3f40ea8..f94f3ccc567c 100644 --- a/src/jsc/bindings/BunObject.cpp +++ b/src/jsc/bindings/BunObject.cpp @@ -37,10 +37,18 @@ #include "BunObject+exports.h" #include "ErrorCode.h" #include "GeneratedBunObject.h" +#include "GeneratedCrypto.h" #include "GeneratedCsrfJsc.h" #include "GeneratedDns.h" +#include "GeneratedHashObject.h" +#include "GeneratedJSON5Object.h" +#include "GeneratedJSONCObject.h" +#include "GeneratedPasswordObject.h" #include "GeneratedSemverObject.h" +#include "GeneratedTOMLObject.h" #include "GeneratedUnsafeObject.h" +#include "GeneratedXMLObject.h" +#include "GeneratedYAMLObject.h" #include "JavaScriptCore/BunV8HeapSnapshotBuilder.h" #include "BunObjectModule.h" #include "_NativeModule.h" @@ -75,7 +83,6 @@ BUN_DECLARE_HOST_FUNCTION(Bun__DNS__lookupService); BUN_DECLARE_HOST_FUNCTION(Bun__fetch); BUN_DECLARE_HOST_FUNCTION(Bun__fetchPreconnect); BUN_DECLARE_HOST_FUNCTION(Bun__randomUUIDv7); -BUN_DECLARE_HOST_FUNCTION(Bun__randomUUIDv5); #include "sliceAnsi.h" #include "escapeHTML.h" @@ -332,13 +339,6 @@ static JSValue constructBunSQLObject(VM& vm, JSObject* bunObject) RELEASE_AND_RETURN(scope, sqlValue.getObject()->get(globalObject, clientData->builtinNames().SQLPublicName())); } -extern "C" JSC::EncodedJSValue JSPasswordObject__create(JSGlobalObject*); - -static JSValue constructPasswordObject(VM& vm, JSObject* bunObject) -{ - return JSValue::decode(JSPasswordObject__create(bunObject->globalObject())); -} - JSValue constructBunFetchObject(VM& vm, JSObject* bunObject) { JSFunction* fetchFn = JSFunction::create(vm, bunObject->globalObject(), 1, "fetch"_s, Bun__fetch, ImplementationVisibility::Public, NoIntrinsic); @@ -937,13 +937,13 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj SHA384 BunObject_lazyPropCb_wrap_SHA384 DontDelete|PropertyCallback SHA512 BunObject_lazyPropCb_wrap_SHA512 DontDelete|PropertyCallback SHA512_256 BunObject_lazyPropCb_wrap_SHA512_256 DontDelete|PropertyCallback - JSONC BunObject_lazyPropCb_wrap_JSONC DontDelete|PropertyCallback - JSON5 BunObject_lazyPropCb_wrap_JSON5 DontDelete|PropertyCallback + JSONC Bun::Bindgen::Generated::JSONCObject::createJSONC DontDelete|PropertyCallback + JSON5 Bun::Bindgen::Generated::JSON5Object::createJSON5 DontDelete|PropertyCallback JSONL constructJSONLObject ReadOnly|DontDelete|PropertyCallback markdown BunObject_lazyPropCb_wrap_markdown DontDelete|PropertyCallback - TOML BunObject_lazyPropCb_wrap_TOML DontDelete|PropertyCallback - XML BunObject_lazyPropCb_wrap_XML DontDelete|PropertyCallback - YAML BunObject_lazyPropCb_wrap_YAML DontDelete|PropertyCallback + TOML Bun::Bindgen::Generated::TOMLObject::createTOML DontDelete|PropertyCallback + XML Bun::Bindgen::Generated::XMLObject::createXML DontDelete|PropertyCallback + YAML Bun::Bindgen::Generated::YAMLObject::createYAML DontDelete|PropertyCallback Transpiler BunObject_lazyPropCb_wrap_Transpiler DontDelete|PropertyCallback embeddedFiles BunObject_lazyPropCb_wrap_embeddedFiles DontDelete|PropertyCallback S3Client BunObject_lazyPropCb_wrap_S3Client DontDelete|PropertyCallback @@ -959,7 +959,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj color BunObject_callback_color DontDelete|Function 2 deepEquals functionBunDeepEquals DontDelete|Function 2 deepMatch functionBunDeepMatch DontDelete|Function 2 - deflateSync BunObject_callback_deflateSync DontDelete|Function 1 + deflateSync Bun::Bindgen::Generated::BunObject::jsDeflateSync DontDelete|Function 1 dns constructDNSObject ReadOnly|DontDelete|PropertyCallback enableANSIColors BunObject_lazyPropCb_wrap_enableANSIColors DontDelete|PropertyCallback env constructEnvObject ReadOnly|DontDelete|PropertyCallback @@ -969,11 +969,11 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj fileURLToPath functionFileURLToPath DontDelete|Function 1 gc Bun::Bindgen::Generated::BunObject::jsGc DontDelete|Function 1 generateHeapSnapshot functionGenerateHeapSnapshot DontDelete|Function 2 - gunzipSync BunObject_callback_gunzipSync DontDelete|Function 1 - gzipSync BunObject_callback_gzipSync DontDelete|Function 1 - hash BunObject_lazyPropCb_wrap_hash DontDelete|PropertyCallback + gunzipSync Bun::Bindgen::Generated::BunObject::jsGunzipSync DontDelete|Function 1 + gzipSync Bun::Bindgen::Generated::BunObject::jsGzipSync DontDelete|Function 1 + hash Bun::Bindgen::Generated::HashObject::createHash DontDelete|PropertyCallback indexOfLine Bun::Bindgen::Generated::BunObject::jsIndexOfLine DontDelete|Function 1 - inflateSync BunObject_callback_inflateSync DontDelete|Function 1 + inflateSync Bun::Bindgen::Generated::BunObject::jsInflateSync DontDelete|Function 1 inspect BunObject_lazyPropCb_wrap_inspect DontDelete|PropertyCallback isMainThread constructIsMainThread ReadOnly|DontDelete|PropertyCallback isStandaloneExecutable BunObject_lazyPropCb_wrap_isStandaloneExecutable ReadOnly|DontDelete|PropertyCallback @@ -986,12 +986,12 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj openInEditor Bun::Bindgen::Generated::BunObject::jsOpenInEditor DontDelete|Function 1 origin BunObject_lazyPropCb_wrap_origin DontEnum|ReadOnly|DontDelete|PropertyCallback version_with_sha constructBunVersionWithSha DontEnum|ReadOnly|DontDelete|PropertyCallback - password constructPasswordObject DontDelete|PropertyCallback + password Bun::Bindgen::Generated::PasswordObject::createPassword DontDelete|PropertyCallback pathToFileURL functionPathToFileURL DontDelete|Function 1 peek constructBunPeekObject DontDelete|PropertyCallback plugin constructPluginObject ReadOnly|DontDelete|PropertyCallback randomUUIDv7 Bun__randomUUIDv7 DontDelete|Function 2 - randomUUIDv5 Bun__randomUUIDv5 DontDelete|Function 3 + randomUUIDv5 Bun::Bindgen::Generated::Crypto::jsRandomUUIDv5 DontDelete|Function 3 readableStreamToArray WebCore::jsFunctionReadableStreamToArray DontDelete|Function 1 readableStreamToArrayBuffer WebCore::jsFunctionReadableStreamToArrayBuffer DontDelete|Function 1 readableStreamToBytes WebCore::jsFunctionReadableStreamToBytes DontDelete|Function 1 @@ -1030,10 +1030,10 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj redis BunObject_lazyPropCb_wrap_valkey DontDelete|PropertyCallback secrets constructSecretsObject DontDelete|PropertyCallback write BunObject_callback_write DontDelete|Function 1 - zstdCompressSync BunObject_callback_zstdCompressSync DontDelete|Function 1 - zstdDecompressSync BunObject_callback_zstdDecompressSync DontDelete|Function 1 - zstdCompress BunObject_callback_zstdCompress DontDelete|Function 1 - zstdDecompress BunObject_callback_zstdDecompress DontDelete|Function 1 + zstdCompressSync Bun::Bindgen::Generated::BunObject::jsZstdCompressSync DontDelete|Function 1 + zstdDecompressSync Bun::Bindgen::Generated::BunObject::jsZstdDecompressSync DontDelete|Function 1 + zstdCompress Bun::Bindgen::Generated::BunObject::jsZstdCompress DontDelete|Function 1 + zstdDecompress Bun::Bindgen::Generated::BunObject::jsZstdDecompress DontDelete|Function 1 @end */ diff --git a/src/jsc/host_object.rs b/src/jsc/host_object.rs index c225cc55ddeb..eaaab1c41e69 100644 --- a/src/jsc/host_object.rs +++ b/src/jsc/host_object.rs @@ -21,30 +21,15 @@ use crate::{JSFunction, JSGlobalObject, JSHostFn, JSValue}; /// a [`JSHostFn`]); `arity` is the JS `function.length` reported to userland. pub type HostFnEntry = (&'static str, JSHostFn, u32); -/// `{ name₀: fn₀, name₁: fn₁, … }` — fresh object pre-sized to `fns.len()` -/// inline-capacity slots, then [`JSValue::put_host_functions`]. -/// -/// Use when the *whole* object is a fn table (TOML/YAML/JSONC/…). When -/// the receiver is something else — a callable, a null-prototype binding bag -/// with non-fn props mixed in — build it yourself and call -/// [`JSValue::put_host_functions`] directly. +/// `{ name₀: fn₀, name₁: fn₁, … }` — fresh object pre-sized to `fns.len()` inline-capacity slots. pub fn create_host_function_object(global: &JSGlobalObject, fns: &[HostFnEntry]) -> JSValue { - JSValue::create_empty_object(global, fns.len()).put_host_functions(global, fns) -} - -impl JSValue { - /// Install each `(name, shim, arity)` as a `JSFunction` property on `self` - /// and return `self` for chaining. The JS-visible `function.name` is the - /// table key; options are `Default` (public visibility, no intrinsic, no - /// constructor) — matching every hand-rolled ladder this replaces. - pub fn put_host_functions(self, global: &JSGlobalObject, fns: &[HostFnEntry]) -> JSValue { - for &(name, host_fn, arity) in fns { - self.put( - global, - name.as_bytes(), - JSFunction::create(global, name, host_fn, arity, Default::default()), - ); - } - self + let object = JSValue::create_empty_object(global, fns.len()); + for &(name, host_fn, arity) in fns { + object.put( + global, + name.as_bytes(), + JSFunction::create(global, name, host_fn, arity, Default::default()), + ); } + object } diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index 994f7ca7cf40..1baea7609235 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -165,6 +165,7 @@ pub mod string_builder; pub mod strong; #[path = "Task.rs"] pub mod task; +mod thread_isolated; #[path = "TopExceptionScope.rs"] pub mod top_exception_scope; #[path = "uuid.rs"] @@ -203,6 +204,7 @@ pub use self::js_promise::JSPromise as JSInternalPromise; pub use self::rare_data as RareData; pub use self::system_error::SystemError; pub use self::task::Taskable; +pub use self::thread_isolated::{ThreadIsolated, ThreadIsolatedArg}; /// Trait surface for `write_format`-style hooks on runtime types /// (`Response::write_format`, `Request::write_format`, `S3File::write_format`, diff --git a/src/jsc/thread_isolated.rs b/src/jsc/thread_isolated.rs new file mode 100644 index 000000000000..3b17de1ee998 --- /dev/null +++ b/src/jsc/thread_isolated.rs @@ -0,0 +1,33 @@ +/// # Safety +/// Its JS buffers are pinned and GC-rooted and its strings thread-isolated: a work-pool job may read it and the JS thread drops it. +pub unsafe trait ThreadIsolatedArg {} + +/// A `T` built as [`ThreadIsolatedArg`] describes. +pub struct ThreadIsolated(T); + +impl ThreadIsolated { + /// # Safety + /// `value` was built as [`ThreadIsolatedArg`] describes. + #[inline] + pub unsafe fn new(value: T) -> Self { + Self(value) + } +} + +// SAFETY: `new`'s contract is exactly what makes the value sendable. +unsafe impl Send for ThreadIsolated {} + +impl core::ops::Deref for ThreadIsolated { + type Target = T; + #[inline] + fn deref(&self) -> &T { + &self.0 + } +} + +impl core::ops::DerefMut for ThreadIsolated { + #[inline] + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} diff --git a/src/runtime/api.rs b/src/runtime/api.rs index 833cd61d7632..25a7332bb2f5 100644 --- a/src/runtime/api.rs +++ b/src/runtime/api.rs @@ -140,99 +140,21 @@ pub(crate) use crate::api::bun::h2_frame_parser::H2FrameParser; pub(crate) use crate::api::bun::ssl_context_cache as SSLContextCache; pub(crate) use crate::api::filesystem_router::FileSystemRouter; pub(crate) use crate::api::filesystem_router::MatchedRoute; -pub(crate) use crate::api::hash_object as HashObject; pub(crate) use crate::api::js_bundler::BuildArtifact; pub(crate) use crate::api::js_bundler::JSBundler; -pub(crate) use crate::api::json5_object as JSON5Object; -pub(crate) use crate::api::toml_object as TOMLObject; -pub(crate) use crate::api::xml_object as XMLObject; -pub(crate) use crate::api::yaml_object as YAMLObject; -// ─── shared scaffold for Bun.{TOML,JSONC,JSON5,YAML}.parse ─────────────────── +// ─── shared scaffold for Bun.{TOML,JSONC,JSON5,YAML,XML}.parse ─────────────── // -// All four host fns repeat: Arena + ASTMemoryAllocator scope + Log + -// frame.argument(0) → bytes → Source::init_path_string. They diverge on -// (a) whether nullish input throws, (b) whether Blob/Buffer is accepted, and -// (c) parse-error class + Expr→JS tail — so this helper owns ONLY the scaffold -// and hands `(&arena, &mut log, &source)` to a per-format closure that does the -// format-specific parse, error match (StackOverflow / OOM / SyntaxError vs +// All five repeat: Arena + ASTMemoryAllocator scope + Log + Source::init_path_string. They diverge on the parse-error +// class and the Expr→JS tail, so this helper owns ONLY the scaffold and hands `(&arena, &mut log, &source)` to a +// per-format closure that does the format-specific parse, error match (StackOverflow / OOM / SyntaxError vs // log.to_js), and tail conversion. fn with_text_format_source( global: &bun_jsc::JSGlobalObject, - frame: &bun_jsc::CallFrame, + bytes: &[u8], path: &'static [u8], - blob_or_buffer_input: BlobOrBufferInput, - nullish_input: NullishInput, f: impl FnOnce(&bun_alloc::Arena, &mut bun_ast::Log, &bun_ast::Source) -> bun_jsc::JsResult, ) -> bun_jsc::JsResult { - with_text_format_source_encoded( - global, - frame, - path, - blob_or_buffer_input, - nullish_input, - StringInput::Utf8, - |arena, log, source, _| f(arena, log, source), - ) -} - -/// What `parse` does with a `Blob`, `ArrayBuffer`, typed array or `DataView`. -#[derive(Clone, Copy, PartialEq, Eq)] -enum BlobOrBufferInput { - /// Parses its bytes. - Bytes, - /// Stringifies it like any other argument, as `JSON.parse` would. - ToString, -} - -/// What `parse` does with an `undefined` or `null` argument. -#[derive(Clone, Copy, PartialEq, Eq)] -enum NullishInput { - Throw, - /// Parses the text `"undefined"` / `"null"`. - ToString, -} - -/// What `parse` hands the closure for a string argument. -#[derive(Clone, Copy, PartialEq, Eq)] -enum StringInput { - /// The string re-encoded as UTF-8 ([`SourceEncoding::Utf8Text`]). - Utf8, - /// The string's own storage, Latin-1 or UTF-16, as is. - AsIs, -} - -/// How the bytes handed to the closure of -/// [`with_text_format_source_encoded`] are encoded. -#[derive(Clone, Copy, PartialEq, Eq)] -enum SourceEncoding { - /// A `Buffer` / typed array / `Blob`: bytes as given (UTF-8 expected). - Bytes, - /// A JS string, re-encoded as UTF-8. - Utf8Text, - /// A Latin-1 JS string, borrowed as is (only under [`StringInput::AsIs`]). - Latin1Text, - /// A UTF-16 JS string, borrowed as is: the bytes are its code units - /// (only under [`StringInput::AsIs`]). - Utf16Text, -} - -fn with_text_format_source_encoded( - global: &bun_jsc::JSGlobalObject, - frame: &bun_jsc::CallFrame, - path: &'static [u8], - blob_or_buffer_input: BlobOrBufferInput, - nullish_input: NullishInput, - string_input: StringInput, - f: impl FnOnce( - &bun_alloc::Arena, - &mut bun_ast::Log, - &bun_ast::Source, - SourceEncoding, - ) -> bun_jsc::JsResult, -) -> bun_jsc::JsResult { - use crate::node::{BlobOrStringOrBuffer, StringOrBuffer}; - // A private mi_heap costs microseconds to create, more than parsing a // small document: keep one per thread and recycle it between calls. // `#[thread_local]` rather than `thread_local!` so there is no @@ -254,39 +176,6 @@ fn with_text_format_source_encoded( let mut ast_memory_allocator = bun_ast::ASTMemoryAllocator::borrowing(arena); let _ast_scope = ast_memory_allocator.enter(); - let input_value = frame.argument(0); - if nullish_input == NullishInput::Throw && input_value.is_empty_or_undefined_or_null() { - return Err(global.throw_invalid_arguments(format_args!("Expected a string to parse"))); - } - - // Hold whichever input storage applies; all expose the bytes. - // Conditional-init + drop-flag — only the taken branch's holder is live. - let _blob_hold: BlobOrStringOrBuffer; - let _str_hold: StringOrBuffer; - let _latin1_hold: bun_core::String; - let mut encoding = SourceEncoding::Utf8Text; - let bytes: &[u8] = 'bytes: { - if blob_or_buffer_input == BlobOrBufferInput::Bytes && !input_value.is_string() { - if let Some(v) = BlobOrStringOrBuffer::from_js(global, input_value)? { - _blob_hold = v; - encoding = SourceEncoding::Bytes; - break 'bytes _blob_hold.slice(); - } - } - let s = input_value.to_bun_string(global)?; - if string_input == StringInput::AsIs { - _latin1_hold = s; - if _latin1_hold.is_8bit() { - encoding = SourceEncoding::Latin1Text; - break 'bytes _latin1_hold.latin1(); - } - encoding = SourceEncoding::Utf16Text; - break 'bytes bytemuck::cast_slice(_latin1_hold.utf16()); - } - _str_hold = StringOrBuffer::String(s.into_utf8_with_string()); - _str_hold.slice() - }; - // Every parser reached from here records source positions as an `i32` // (`ast::Loc` via `usize2loc` for JSONC/TOML, JSON5's token locs, YAML's // `Pos`), so an input those offsets cannot represent panics inside the @@ -305,7 +194,7 @@ fn with_text_format_source_encoded( let mut log = bun_ast::Log::init(); let source = bun_ast::Source::init_path_string(path, bytes); - f(arena, &mut log, &source, encoding) + f(arena, &mut log, &source) } // ─── shared Expr → JS conversion for the text-format parsers ───────────────── diff --git a/src/runtime/api/BunObject.bind.ts b/src/runtime/api/BunObject.bind.ts index 2887fd8864b8..0dbf28ce34b4 100644 --- a/src/runtime/api/BunObject.bind.ts +++ b/src/runtime/api/BunObject.bind.ts @@ -2,6 +2,8 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::api::bun_object" }); +const stringOrBuffer = b.bytes({ from: ["string", "buffer"] }); + export const allocUnsafe = b.fn({ args: { size: b.u32.validateInteger() }, returns: b.bufferSource, @@ -27,6 +29,39 @@ export const braces = b.fn({ throws: true, }); +/** `windowBits` is converted and not used. */ +export const ZlibCompressionOptions = b.dictionary( + { + windowBits: b.f64.loose.allowBigInt.optional, + library: b.string.utf8.nullable, + level: b.f64.loose.allowBigInt.optional, + }, + { allowArray: true }, +); + +/** `level`, `memLevel` and `strategy` are converted and not used. */ +export const ZlibDecompressionOptions = b.dictionary( + { + windowBits: b.f64.loose.allowBigInt.optional, + level: b.f64.loose.allowBigInt.optional, + memLevel: b.f64.loose.allowBigInt.optional, + strategy: b.f64.loose.allowBigInt.optional, + library: b.string.utf8.nullable, + }, + { allowArray: true }, +); + +const zlib = (rustName: string, options: typeof ZlibCompressionOptions | typeof ZlibDecompressionOptions) => + b.fn({ + args: { data: stringOrBuffer, options: options.default({}) }, + returns: b.bufferSource, + capability: "heap", + throws: true, + rustPath: `crate::api::bun_object::JSZlib::${rustName}`, + }); + +export const deflateSync = zlib("deflate_sync", ZlibCompressionOptions); + export const gc = b.fn({ args: { force: b.bool.loose.default(false), @@ -36,14 +71,19 @@ export const gc = b.fn({ rustPath: "crate::api::bun_object::gc_from_js", }); +export const gunzipSync = zlib("gunzip_sync", ZlibDecompressionOptions); +export const gzipSync = zlib("gzip_sync", ZlibCompressionOptions); + export const indexOfLine = b.fn({ args: { - buffer: b.bufferSource.bytes.allowShared, + buffer: b.bytes({ from: ["buffer"] }), offset: b.f64.loose.allowBigInt.default(0), }, returns: b.f64, }); +export const inflateSync = zlib("inflate_sync", ZlibDecompressionOptions); + export const MMapOptions = b.dictionary( { shared: b.bool.loose.default(true), @@ -116,3 +156,38 @@ export const which = b.fn({ capability: "heap", throws: true, }); + +export const ZstdCompressionOptions = b.dictionary({ level: b.f64.loose.allowBigInt.optional }, { allowArray: true }); + +const pinned = b.bytes({ from: ["string", "buffer"], held: "pinned" }); + +export const zstdCompress = b.fn({ + args: { data: pinned, options: ZstdCompressionOptions.default({}) }, + returns: b.any, + capability: "js", + throws: true, + rustPath: "crate::api::bun_object::JSZstd::compress", +}); + +export const zstdCompressSync = b.fn({ + args: { data: stringOrBuffer, options: ZstdCompressionOptions.default({}) }, + returns: b.bufferSource, + capability: "heap", + throws: true, + rustPath: "crate::api::bun_object::JSZstd::compress_sync", +}); + +export const zstdDecompress = b.fn({ + args: { data: pinned }, + returns: b.any, + capability: "js", + rustPath: "crate::api::bun_object::JSZstd::decompress", +}); + +export const zstdDecompressSync = b.fn({ + args: { data: stringOrBuffer }, + returns: b.bufferSource, + capability: "heap", + throws: true, + rustPath: "crate::api::bun_object::JSZstd::decompress_sync", +}); diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index 41a480230077..cce6946b001b 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -63,7 +63,6 @@ pub(crate) fn get_public_path_with_asset_prefix( } use bun_jsc::HostReturn as _; -use core::ffi::c_void; use std::io::Write as _; use bun_core::Output; @@ -80,7 +79,6 @@ use bun_shell_parser::braces as Braces; use bun_sys::{self as sys, Fd, FdExt as _}; use bun_zlib as zlib; -use crate::api::{HashObject, JSON5Object, TOMLObject, XMLObject, YAMLObject}; use crate::crypto as Crypto; use crate::node; use crate::test_runner::jest::Jest; @@ -91,10 +89,11 @@ use bun_jsc::call_frame::ArgumentsSlice; use bun_jsc::{StringJsc as _, bun_string_jsc}; use crate::generated_bindgen::bun_object::{ - BracesOptions, EditorOptions, MMapOptions, WhichOptions, + BracesOptions, EditorOptions, MMapOptions, WhichOptions, ZlibCompressionOptions, + ZlibDecompressionOptions, ZstdCompressionOptions, }; use crate::generated_bindgen::glob::GlobClass as _; -use bun_jsc::bindgen::{self, BufferSource, JsHeap, JsLocal, JsScope, PossiblySharedBytes}; +use bun_jsc::bindgen::{self, BufferSource, JsHeap, JsLocal, JsScope, PinnedBytes}; // ─── wrap_static_method adapters ─────────────────────────────────────────── // `#[bun_jsc::host_fn(static)]` is not yet emitted, @@ -250,11 +249,7 @@ pub(crate) mod bun_object { BunObject_callback_build => super::static_adapters::js_bundler_build, BunObject_callback_color => bun_css_jsc::js_function_color, BunObject_callback_connect => super::static_adapters::listener_connect, - BunObject_callback_deflateSync => JSZlib::deflate_sync, BunObject_callback_file => crate::webcore::blob::construct_bun_file, - BunObject_callback_gunzipSync => JSZlib::gunzip_sync, - BunObject_callback_gzipSync => JSZlib::gzip_sync, - BunObject_callback_inflateSync => JSZlib::inflate_sync, BunObject_callback_jest => Jest::call, BunObject_callback_listen => super::static_adapters::listener_listen, BunObject_callback_registerMacro => super::register_macro, @@ -266,10 +261,6 @@ pub(crate) mod bun_object { BunObject_callback_spawnSync => super::static_adapters::subprocess_spawn_sync, BunObject_callback_udpSocket => super::static_adapters::udp_socket, BunObject_callback_write => crate::webcore::blob::write_file, - BunObject_callback_zstdCompressSync => JSZstd::compress_sync, - BunObject_callback_zstdDecompressSync => JSZstd::decompress_sync, - BunObject_callback_zstdCompress => JSZstd::compress, - BunObject_callback_zstdDecompress => JSZstd::decompress, } // `createParsedShellScript` / `createShellInterpreter` go through the same // `to_js_host_call` thunk as the macro-generated callbacks (their bodies @@ -297,12 +288,7 @@ pub(crate) mod bun_object { BunObject_lazyPropCb_SHA384 => Crypto::SHA384::getter, BunObject_lazyPropCb_SHA512 => Crypto::SHA512::getter, BunObject_lazyPropCb_SHA512_256 => Crypto::SHA512_256::getter, - BunObject_lazyPropCb_JSONC => super::get_jsonc_object, BunObject_lazyPropCb_markdown => super::get_markdown_object, - BunObject_lazyPropCb_TOML => super::get_toml_object, - BunObject_lazyPropCb_JSON5 => super::get_json5_object, - BunObject_lazyPropCb_XML => super::get_xml_object, - BunObject_lazyPropCb_YAML => super::get_yaml_object, BunObject_lazyPropCb_Transpiler => super::get_transpiler_constructor, BunObject_lazyPropCb_argv => super::get_argv, BunObject_lazyPropCb_cron => super::get_cron_object, @@ -310,7 +296,6 @@ pub(crate) mod bun_object { BunObject_lazyPropCb_embeddedFiles => super::get_embedded_files, BunObject_lazyPropCb_enableANSIColors => super::enable_ansi_colors, BunObject_lazyPropCb_isStandaloneExecutable => super::get_is_standalone_executable, - BunObject_lazyPropCb_hash => super::get_hash_object, BunObject_lazyPropCb_inspect => super::get_inspect, BunObject_lazyPropCb_origin => super::get_origin, BunObject_lazyPropCb_S3Client => super::get_s3_client_constructor, @@ -1225,14 +1210,14 @@ pub(crate) fn bun_resolve_sync_with_source_if_exists( }) } -pub(crate) fn index_of_line(buffer: PossiblySharedBytes<'_>, offset: f64) -> f64 { +pub(crate) fn index_of_line(buffer: &[u8], offset: f64) -> f64 { // The cast saturates: NaN and a negative offset are 0. let offset = offset as usize; // 0x0A never appears inside a multi-byte UTF-8 sequence. - match buffer - .get(offset..) - .and_then(|rest| rest.index_of_char(b'\n')) - { + // SAFETY: readable. `index_of_char` asserts that the byte it found is still there, and another thread can write a shared buffer. + match buffer.get(offset..).and_then(|rest| unsafe { + bun_highway::index_of_char_raw(rest.as_ptr(), rest.len(), b'\n') + }) { Some(i) => (offset + i) as f64, None => -1.0, } @@ -1451,6 +1436,8 @@ pub(crate) fn mmap(cx: &mut JsScope<'_>, path: &[u8], options: MMapOptions) -> J #[cfg(not(windows))] { + use core::ffi::c_void; + if path.len() > MAX_PATH_BYTES { return Err(global_this.throw_invalid_arguments(format_args!("Path too long"))); } @@ -1551,31 +1538,9 @@ fn get_file_system_router(global_this: &JSGlobalObject, _: &JSObject) -> JSValue ) } -fn get_hash_object(global_this: &JSGlobalObject, _: &JSObject) -> JSValue { - HashObject::create(global_this) -} - -fn get_jsonc_object(global_this: &JSGlobalObject, _: &JSObject) -> JSValue { - crate::api::jsonc_object::create(global_this) -} fn get_markdown_object(global_this: &JSGlobalObject, _: &JSObject) -> JSValue { crate::api::markdown_object::create(global_this) } -fn get_toml_object(global_this: &JSGlobalObject, _: &JSObject) -> JSValue { - TOMLObject::create(global_this) -} - -fn get_json5_object(global_this: &JSGlobalObject, _: &JSObject) -> JSValue { - JSON5Object::create(global_this) -} - -fn get_xml_object(global_this: &JSGlobalObject, _: &JSObject) -> JSValue { - XMLObject::create(global_this) -} - -fn get_yaml_object(global_this: &JSGlobalObject, _: &JSObject) -> JSValue { - YAMLObject::create(global_this) -} fn get_archive_constructor(global_this: &JSGlobalObject, _: &JSObject) -> JSValue { jsc::codegen::js::get_constructor::(global_this) @@ -1876,68 +1841,13 @@ extern "C" fn Bun__reportError(global_object: &JSGlobalObject, err: JSValue) { let _ = vm.uncaught_exception(global_object, err, false); } -/// Shared argument prefix for `Bun.{gzip,gunzip,deflate,inflate}Sync` and -/// `Bun.zstd{Compress,Decompress}{,Sync}`: returns `(arguments[0] ?? undefined, -/// arguments[1] if object)`. Throws if `arguments[1]` is present but neither an -/// object nor `undefined`. -/// -/// Kept separate from [`parse_compress_buffer_and_options`] so async callers -/// (e.g. `JSZstd::get_options_async`) can read `options` *before* pinning and -/// rooting the buffer, preserving error precedence. -#[inline] -pub(crate) fn parse_compress_args( - global: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult<(JSValue, Option)> { - let arguments = callframe.arguments(); - let buffer_value: JSValue = if arguments.len() > 0 { - arguments[0] - } else { - JSValue::UNDEFINED - }; - let options_val: Option = if arguments.len() > 1 && arguments[1].is_object() { - Some(arguments[1]) - } else if arguments.len() > 1 && !arguments[1].is_undefined() { - return Err( - global.throw_invalid_arguments(format_args!("Expected options to be an object")) - ); - } else { - None - }; - Ok((buffer_value, options_val)) -} - -/// Sync `StringOrBuffer` coercion of the buffer argument. Callers that read -/// option properties (which can run arbitrary JS) must do so *before* calling -/// this, so nothing runs between the coercion and the use of the slice. -#[inline] -pub(crate) fn coerce_compress_buffer( - global: &JSGlobalObject, - buffer_value: JSValue, -) -> JsResult> { - if let Some(buffer) = node::StringOrBuffer::from_js(global, buffer_value)? { - return Ok(buffer); - } - Err(global.throw_invalid_arguments(format_args!("Expected buffer to be a string or buffer"))) -} - -/// [`parse_compress_args`] + [`coerce_compress_buffer`], for callers that read -/// no further option properties. -#[inline] -pub(crate) fn parse_compress_buffer_and_options( - global: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult<(node::StringOrBuffer<'static>, Option)> { - let (buffer_value, options_val) = parse_compress_args(global, callframe)?; - Ok((coerce_compress_buffer(global, buffer_value)?, options_val)) -} - #[allow(non_snake_case)] pub(crate) mod JSZlib { use super::*; - use bun_jsc::ComptimeStringMapExt as _; use bun_libdeflate_sys::libdeflate as bun_libdeflate; + type BufferResult<'a> = bindgen::Result<'a, JsLocal<'a, BufferSource>>; + /// Local shim: libdeflate's `Status` has no `Into<&str>` upstream. #[inline] fn libdeflate_status_str(s: bun_libdeflate::Status) -> &'static str { @@ -1949,155 +1859,86 @@ pub(crate) mod JSZlib { } } - // NOTE: a full `[0..capacity)` window - // was previously shimmed here as `&mut [u8]`, but materializing `&mut [u8]` - // over uninitialized bytes is UB regardless of later `set_len`. - // Callers now use `Vec::spare_capacity_mut()` (-> `&mut [MaybeUninit]`) - // with `compress_into` / `decompress_into`, which is the sound equivalent. - - // NOTE: no `reader_deallocator` / `compressor_deallocator` exports are - // needed to free a heap-allocated reader/compressor from the ArrayBuffer - // finalizer. The reader stays on-stack - // borrowing a local `Vec`, then leaks only the Vec's allocation into - // the ArrayBuffer — so both zlib paths converge on `global_deallocator` - // and the per-type callbacks are gone. (`no_mangle` dropped: 0 C++ refs.) - use bun_alloc::c_thunks::mi_free_ctx as global_deallocator; - - #[derive(Copy, Clone, PartialEq, Eq, strum::IntoStaticStr, strum::EnumString)] - #[strum(serialize_all = "lowercase")] - pub(crate) enum Library { + enum Library { Zlib, Libdeflate, } - // bun.ComptimeEnumMap(Library) - bun_core::comptime_string_map! { - pub(crate) static LIBRARY_MAP: Library = { - b"zlib" => Library::Zlib, - b"libdeflate" => Library::Libdeflate, - }; + impl Library { + fn from_option<'a>(heap: &JsHeap<'a>, name: Option<&[u8]>) -> bindgen::Result<'a, Self> { + match name { + None | Some(b"" | b"zlib") => Ok(Library::Zlib), + Some(b"libdeflate") => Ok(Library::Libdeflate), + Some(_) => Err(heap.invalid_arguments(format_args!( + "Expected library to be one of 'zlib' or 'libdeflate'" + ))), + } + } } - /// Move `list`'s allocation into a `Uint8Array` backing store without - /// copying. After `shrink_to_fit`, an empty `Vec` owns no allocation (its - /// pointer is dangling), so no deallocator is registered for it. - fn leak_list_into_uint8array( - global_this: &JSGlobalObject, - mut list: Vec, - ) -> JsResult { - list.shrink_to_fit(); - let is_empty = list.is_empty(); - let leaked: &'static mut [u8] = list.leak(); - let ptr = leaked.as_mut_ptr(); - let array_buffer = ArrayBuffer::from_bytes(leaked, jsc::JSType::Uint8Array); - // SAFETY: non-empty: `ptr` is the just-leaked `Vec` allocation, freed - // exactly once at GC by `global_deallocator` (`mi_free_ctx`) via the ctx - // pointer. Empty: no callback, and the dangling `ptr` is never read. - unsafe { - array_buffer.to_js_with_context( - global_this, - if is_empty { - core::ptr::null_mut() - } else { - ptr.cast::() - }, - if is_empty { - None - } else { - Some(global_deallocator) - }, - ) + fn init_error<'a>(heap: &JsHeap<'a>, err: zlib::ZlibError) -> bindgen::Error<'a> { + match err { + zlib::ZlibError::OutOfMemory => bun_alloc::AllocError.into(), + zlib::ZlibError::InvalidArgument => { + heap.error(format_args!("Zlib error: Invalid argument")) + } + err => heap.error(format_args!("{} Zlib error", <&'static str>::from(err))), } } - #[bun_jsc::host_fn] - pub(crate) fn gzip_sync( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let (buffer_value, options_val) = parse_compress_args(global_this, callframe)?; - gzip_or_deflate_sync(global_this, buffer_value, options_val, true) + fn read_error<'a>( + heap: &JsHeap<'a>, + err: zlib::ZlibError, + message: Option<&[u8]>, + ) -> bindgen::Error<'a> { + if err == zlib::ZlibError::OutOfMemory { + return bun_alloc::AllocError.into(); + } + let message = message.unwrap_or(b"Zlib returned an error"); + heap.error(format_args!("{}", bstr::BStr::new(message))) } - #[bun_jsc::host_fn] - pub(crate) fn inflate_sync( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let (buffer_value, options_val) = parse_compress_args(global_this, callframe)?; - gunzip_or_inflate_sync(global_this, buffer_value, options_val, false) + pub(crate) fn gzip_sync<'a>( + heap: &JsHeap<'a>, + data: &[u8], + options: ZlibCompressionOptions<'a>, + ) -> BufferResult<'a> { + gzip_or_deflate_sync(heap, data, options, true) } - #[bun_jsc::host_fn] - pub(crate) fn deflate_sync( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let (buffer_value, options_val) = parse_compress_args(global_this, callframe)?; - gzip_or_deflate_sync(global_this, buffer_value, options_val, false) + #[allow(clippy::large_types_passed_by_value)] // the generated thunk hands over the struct it made + pub(crate) fn inflate_sync<'a>( + heap: &JsHeap<'a>, + data: &[u8], + options: ZlibDecompressionOptions<'a>, + ) -> BufferResult<'a> { + gunzip_or_inflate_sync(heap, data, &options, false) } - #[bun_jsc::host_fn] - pub(crate) fn gunzip_sync( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let (buffer_value, options_val) = parse_compress_args(global_this, callframe)?; - gunzip_or_inflate_sync(global_this, buffer_value, options_val, true) + pub(crate) fn deflate_sync<'a>( + heap: &JsHeap<'a>, + data: &[u8], + options: ZlibCompressionOptions<'a>, + ) -> BufferResult<'a> { + gzip_or_deflate_sync(heap, data, options, false) } - fn gunzip_or_inflate_sync( - global_this: &JSGlobalObject, - buffer_value: JSValue, - options_val_: Option, - is_gzip: bool, - ) -> JsResult { - let mut opts = zlib::Options { - gzip: is_gzip, - window_bits: if is_gzip { 31 } else { -15 }, - ..Default::default() - }; - - let mut library = Library::Zlib; - if let Some(options_val) = options_val_ { - if let Some(window) = options_val.get(global_this, "windowBits")? { - opts.window_bits = window.coerce::(global_this)?; - library = Library::Zlib; - } - - if let Some(level) = options_val.get(global_this, "level")? { - opts.level = level.coerce::(global_this)?; - } - - if let Some(mem_level) = options_val.get(global_this, "memLevel")? { - opts.mem_level = mem_level.coerce::(global_this)?; - library = Library::Zlib; - } - - if let Some(strategy) = options_val.get(global_this, "strategy")? { - opts.strategy = strategy.coerce::(global_this)?; - library = Library::Zlib; - } - - if let Some(library_value) = options_val.get_truthy(global_this, "library")? { - if !library_value.is_string() { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected library to be a string"))); - } - - library = match LIBRARY_MAP.from_js(global_this, library_value)? { - Some(v) => v, - None => { - return Err(global_this.throw_invalid_arguments(format_args!( - "Expected library to be one of 'zlib' or 'libdeflate'", - ))); - } - }; - } - } + #[allow(clippy::large_types_passed_by_value)] // the generated thunk hands over the struct it made + pub(crate) fn gunzip_sync<'a>( + heap: &JsHeap<'a>, + data: &[u8], + options: ZlibDecompressionOptions<'a>, + ) -> BufferResult<'a> { + gunzip_or_inflate_sync(heap, data, &options, true) + } - let buffer = coerce_compress_buffer(global_this, buffer_value)?; - let compressed = buffer.slice(); + fn gunzip_or_inflate_sync<'a>( + heap: &JsHeap<'a>, + compressed: &[u8], + options: &ZlibDecompressionOptions<'a>, + is_gzip: bool, + ) -> BufferResult<'a> { + let library = Library::from_option(heap, options.library)?; let mut list: Vec = Vec::new(); let mut reserved = false; @@ -2122,54 +1963,32 @@ pub(crate) mod JSZlib { } else { 32 }) - .map_err(|_| global_this.throw_out_of_memory())?; + .map_err(|_| bun_alloc::AllocError)?; } match library { Library::Zlib => { - let mut reader = match zlib::ZlibReaderArrayList::init_with_options( + let mut reader = zlib::ZlibReaderArrayList::init_with_options( compressed, &mut list, zlib::Options { - window_bits: opts.window_bits, - level: opts.level, + window_bits: options + .window_bits + .map_or(if is_gzip { 31 } else { -15 }, |bits| bits as i32), ..Default::default() }, - ) { - Ok(r) => r, - Err(err) => { - // `list` is still mutably borrowed by the match - // scrutinee's temporary; it drops on `return` anyway. - if err == zlib::ZlibError::InvalidArgument { - return Err( - global_this.throw(format_args!("Zlib error: Invalid argument")) - ); - } - return Err(global_this.throw_error(crate::Error::from(err), "Zlib error")); - } - }; + ) + .map_err(|err| init_error(heap, err))?; - match reader.read_all(true) { - Ok(()) => {} - Err(zlib::ZlibError::OutOfMemory) => { - return Err(global_this.throw_out_of_memory()); - } - Err(_) => { - let msg = reader.error_message().unwrap_or(b"Zlib returned an error"); - return Err(global_this.throw(format_args!("{}", bstr::BStr::new(msg)))); - } + if let Err(err) = reader.read_all(true) { + return Err(read_error(heap, err, reader.error_message())); } - // NOTE: the reader *borrows* `list_ptr`, - // so drop the reader to release the borrow, then leak the owned - // `list` directly into the ArrayBuffer (freed by - // `global_deallocator`). drop(reader); - leak_list_into_uint8array(global_this, list) + heap.create_uint8_array_from_box(list.into_boxed_slice()) } Library::Libdeflate => { let Some(mut decompressor) = bun_libdeflate::OwnedDecompressor::new() else { - drop(list); - return Err(global_this.throw_out_of_memory()); + return Err(bun_alloc::AllocError.into()); }; let encoding = if is_gzip { bun_libdeflate::Encoding::Gzip @@ -2179,145 +1998,70 @@ pub(crate) mod JSZlib { let max_output = ArrayBuffer::MAX_SIZE as usize; let result = decompressor .decompress_to_vec_grow(compressed, &mut list, encoding, max_output) - .map_err(|_| global_this.throw_out_of_memory())?; + .map_err(|_| bun_alloc::AllocError)?; match result.status { bun_libdeflate::Status::Success if list.len() <= max_output => {} bun_libdeflate::Status::Success | bun_libdeflate::Status::InsufficientSpace => { - drop(list); - return Err(global_this - .err( - jsc::ErrCode::BUFFER_TOO_LARGE, - format_args!( - "Cannot create a Buffer larger than {max_output} bytes", - ), - ) - .throw()); + return Err(heap.error_with_code( + jsc::ErrorCode::BUFFER_TOO_LARGE, + format_args!("Cannot create a Buffer larger than {max_output} bytes"), + )); } _ => { - drop(list); - return Err(global_this.throw(format_args!( + return Err(heap.error(format_args!( "libdeflate returned an error: {}", libdeflate_status_str(result.status), ))); } } - - // Ownership of the allocation transfers to JSC; freed via - // `global_deallocator` once the ArrayBuffer is finalized. - let leaked: &'static mut [u8] = list.leak(); - let ptr = leaked.as_mut_ptr(); - let array_buffer = ArrayBuffer::from_bytes(leaked, jsc::JSType::Uint8Array); - // SAFETY: `ptr` is the just-leaked `Vec` allocation, live until - // `global_deallocator` (`mi_free_ctx`) frees it exactly once at - // GC via the ctx pointer (the data pointer itself). - unsafe { - array_buffer.to_js_with_context( - global_this, - ptr.cast::(), - Some(global_deallocator), - ) - } + heap.create_uint8_array_from_box(list.into_boxed_slice()) } } } - fn gzip_or_deflate_sync( - global_this: &JSGlobalObject, - buffer_value: JSValue, - options_val_: Option, + fn gzip_or_deflate_sync<'a>( + heap: &JsHeap<'a>, + data: &[u8], + options: ZlibCompressionOptions<'a>, is_gzip: bool, - ) -> JsResult { - let mut level: Option = None; - let mut library = Library::Zlib; - let mut window_bits: i32 = 0; - - if let Some(options_val) = options_val_ { - if let Some(window) = options_val.get(global_this, "windowBits")? { - window_bits = window.coerce::(global_this)?; - library = Library::Zlib; - } + ) -> BufferResult<'a> { + let level = options.level.map_or(6, |level| level as i32); - if let Some(library_value) = options_val.get_truthy(global_this, "library")? { - if !library_value.is_string() { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected library to be a string"))); - } - - library = match LIBRARY_MAP.from_js(global_this, library_value)? { - Some(v) => v, - None => { - return Err(global_this.throw_invalid_arguments(format_args!( - "Expected library to be one of 'zlib' or 'libdeflate'", - ))); - } - }; - } - - if let Some(level_value) = options_val.get(global_this, "level")? { - level = Some(level_value.coerce::(global_this)?); - } - } - - let buffer = coerce_compress_buffer(global_this, buffer_value)?; - let compressed = buffer.slice(); - let _ = window_bits; // unused - - match library { + match Library::from_option(heap, options.library)? { Library::Zlib => { // `init` reserves `deflateBound` of the input. let mut list: Vec = Vec::new(); - let mut reader = match zlib::ZlibCompressorArrayList::init( - compressed, + let mut reader = zlib::ZlibCompressorArrayList::init( + data, &mut list, zlib::Options { window_bits: 15, gzip: is_gzip, - level: level.unwrap_or(6), + level, ..Default::default() }, - ) { - Ok(r) => r, - Err(err) => { - // `list` is still mutably borrowed by the match - // scrutinee's temporary; it drops on `return` anyway. - if err == zlib::ZlibError::InvalidArgument { - return Err( - global_this.throw(format_args!("Zlib error: Invalid argument")) - ); - } - return Err(global_this.throw_error(crate::Error::from(err), "Zlib error")); - } - }; + ) + .map_err(|err| init_error(heap, err))?; - match reader.read_all() { - Ok(()) => {} - Err(zlib::ZlibError::OutOfMemory) => { - return Err(global_this.throw_out_of_memory()); - } - Err(_) => { - let msg = reader.error_message().unwrap_or(b"Zlib returned an error"); - return Err(global_this.throw(format_args!("{}", bstr::BStr::new(msg)))); - } + if let Err(err) = reader.read_all() { + return Err(read_error(heap, err, reader.error_message())); } - // NOTE: see gunzip path — reader borrows `list`, so drop - // it before leaking `list` into the ArrayBuffer. drop(reader); - leak_list_into_uint8array(global_this, list) + heap.create_uint8_array_from_box(list.into_boxed_slice()) } Library::Libdeflate => { - let level = level.unwrap_or(6); if !(bun_libdeflate::MIN_COMPRESSION_LEVEL..=bun_libdeflate::MAX_COMPRESSION_LEVEL) .contains(&level) { - return Err(global_this.throw_invalid_arguments(format_args!( + return Err(heap.invalid_arguments(format_args!( "Compression level must be between {} and {} for libdeflate", bun_libdeflate::MIN_COMPRESSION_LEVEL, bun_libdeflate::MAX_COMPRESSION_LEVEL, ))); } let Some(mut compressor) = bun_libdeflate::OwnedCompressor::new(level) else { - return Err(global_this.throw_out_of_memory()); + return Err(bun_alloc::AllocError.into()); }; let encoding = if is_gzip { bun_libdeflate::Encoding::Gzip @@ -2327,31 +2071,15 @@ pub(crate) mod JSZlib { let mut list: Vec = Vec::new(); let result = compressor - .compress_to_vec(compressed, &mut list, encoding) - .map_err(|_| global_this.throw_out_of_memory())?; + .compress_to_vec(data, &mut list, encoding) + .map_err(|_| bun_alloc::AllocError)?; if result.status != bun_libdeflate::Status::Success { - drop(list); - return Err(global_this.throw(format_args!( + return Err(heap.error(format_args!( "libdeflate error: {}", libdeflate_status_str(result.status), ))); } - - // Ownership of the allocation transfers to JSC; freed via - // `global_deallocator` once the ArrayBuffer is finalized. - let leaked: &'static mut [u8] = list.leak(); - let ptr = leaked.as_mut_ptr(); - let array_buffer = ArrayBuffer::from_bytes(leaked, jsc::JSType::Uint8Array); - // SAFETY: `ptr` is the just-leaked `Vec` allocation, live until - // `global_deallocator` (`mi_free_ctx`) frees it exactly once at - // GC via the ctx pointer (the data pointer itself). - unsafe { - array_buffer.to_js_with_context( - global_this, - ptr.cast::(), - Some(global_deallocator), - ) - } + heap.create_uint8_array_from_box(list.into_boxed_slice()) } } } @@ -2360,44 +2088,19 @@ pub(crate) mod JSZlib { #[allow(non_snake_case)] pub(crate) mod JSZstd { use super::*; - - fn get_level(global_this: &JSGlobalObject, options_val: Option) -> JsResult { - if let Some(option_obj) = options_val { - if let Some(level_val) = option_obj.get(global_this, "level")? { - let value = level_val.coerce::(global_this)?; - - if value < 1 || value > 22 { - return Err(global_this.throw_invalid_arguments(format_args!( - "Compression level must be between 1 and 22", - ))); - } - - return Ok(value); - } - } - - Ok(3) - } - - #[inline] - fn get_options_async( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult<( - node::ThreadIsolated>, - Option, - i32, - )> { - let (buffer_value, options_val) = parse_compress_args(global_this, callframe)?; - - let level = get_level(global_this, options_val)?; - - if let Some(buffer) = node::StringOrBuffer::from_js_async(global_this, buffer_value)? { - return Ok((buffer, options_val, level)); + use bun_jsc::bindgen::IntoError as _; + + fn level_of<'a>( + heap: &JsHeap<'a>, + options: ZstdCompressionOptions, + ) -> bindgen::Result<'a, i32> { + let level = options.level.map_or(3, |level| level as i32); + if !(1..=22).contains(&level) { + return Err( + heap.invalid_arguments(format_args!("Compression level must be between 1 and 22")) + ); } - - Err(global_this - .throw_invalid_arguments(format_args!("Expected buffer to be a string or buffer"))) + Ok(level) } /// Error of a `Bun.zstd*` call: thrown by the sync functions, rejected by [`ZstdJob`]. @@ -2410,28 +2113,33 @@ pub(crate) mod JSZstd { Decompression(bun_zstd::ZstdError), } - impl Failure { - fn throw(self, global_this: &JSGlobalObject) -> jsc::JsError { + /// The message of the `ERR_ZSTD`. + impl core::fmt::Display for Failure { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Failure::OutOfMemory => f.write_str("Out of memory"), + Failure::Compression(message) => write!(f, "{}", bstr::BStr::new(message)), + Failure::Decompression(err) => write!(f, "Decompression failed: {err}"), + } + } + } + + impl<'a> bindgen::IntoError<'a> for Failure { + #[cold] + fn into_error(self, heap: &JsHeap<'a>) -> bindgen::Error<'a> { match self { - Failure::OutOfMemory => global_this.throw_out_of_memory(), - failure => global_this.throw_value(failure.to_js(global_this)), + Failure::OutOfMemory => bun_alloc::AllocError.into(), + failure => heap.error_with_code(jsc::ErrorCode::ZSTD, format_args!("{failure}")), } } + } + impl Failure { fn to_js(self, global_this: &JSGlobalObject) -> JSValue { match self { Failure::OutOfMemory => global_this.create_out_of_memory_error(), - Failure::Compression(message) => global_this - .err( - jsc::ErrCode::ZSTD, - format_args!("{}", bstr::BStr::new(message)), - ) - .to_js(), - Failure::Decompression(err) => global_this - .err( - jsc::ErrCode::ZSTD, - format_args!("Decompression failed: {}", err), - ) + failure => global_this + .err(jsc::ErrorCode::ZSTD, format_args!("{failure}")) .to_js(), } } @@ -2475,41 +2183,29 @@ pub(crate) mod JSZstd { .map_err(Failure::from) } - #[bun_jsc::host_fn] - pub(crate) fn compress_sync( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let (buffer_value, options_val) = parse_compress_args(global_this, callframe)?; - - let level = get_level(global_this, options_val)?; - - let buffer = coerce_compress_buffer(global_this, buffer_value)?; - - let output = - compress_to_box(buffer.slice(), level).map_err(|failure| failure.throw(global_this))?; - - JSValue::create_buffer_from_box(global_this, output) + pub(crate) fn compress_sync<'a>( + heap: &JsHeap<'a>, + data: &[u8], + options: ZstdCompressionOptions, + ) -> bindgen::Result<'a, JsLocal<'a, BufferSource>> { + let level = level_of(heap, options)?; + let output = compress_to_box(data, level).map_err(|failure| failure.into_error(heap))?; + heap.create_buffer_from_box(output) } - #[bun_jsc::host_fn] - pub(crate) fn decompress_sync( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let (buffer, _) = parse_compress_buffer_and_options(global_this, callframe)?; - - let output = - decompress_to_box(buffer.slice()).map_err(|failure| failure.throw(global_this))?; - - JSValue::create_buffer_from_box(global_this, output) + pub(crate) fn decompress_sync<'a>( + heap: &JsHeap<'a>, + data: &[u8], + ) -> bindgen::Result<'a, JsLocal<'a, BufferSource>> { + let output = decompress_to_box(data).map_err(|failure| failure.into_error(heap))?; + heap.create_buffer_from_box(output) } // --- Async versions --- /// `Bun.zstdCompress` / `Bun.zstdDecompress` off the JS thread. pub(crate) struct ZstdJob { - pub buffer: node::ThreadIsolated>, + pub buffer: PinnedBytes, pub is_compress: bool, pub level: i32, /// Filled in by `run`. @@ -2524,7 +2220,7 @@ pub(crate) mod JSZstd { this: &mut Self, done: bun_jsc::Completion, ) -> Option> { - let input = this.buffer.slice(); + let input = &this.buffer[..]; this.result = if this.is_compress { compress_to_box(input, this.level) @@ -2554,16 +2250,16 @@ pub(crate) mod JSZstd { } } - fn create_job( - cx: &bun_jsc::JsThread<'_>, - buffer: node::ThreadIsolated>, + fn schedule<'a>( + cx: &mut JsScope<'a>, + buffer: PinnedBytes, is_compress: bool, level: i32, ) -> JSValue { let promise = jsc::JSPromiseStrong::init(cx.global()); let promise_value = promise.value(); jsc::Job::::schedule( - cx, + &cx.js_thread(), ZstdJob { buffer, is_compress, @@ -2575,30 +2271,22 @@ pub(crate) mod JSZstd { promise_value } - #[bun_jsc::host_fn] - pub(crate) fn compress( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let cx = global_this.js_thread_of_caller(callframe); - let (buffer, _, level) = get_options_async(global_this, callframe)?; - Ok(create_job(&cx, buffer, true, level)) + pub(crate) fn compress<'a>( + cx: &mut JsScope<'a>, + data: PinnedBytes, + options: ZstdCompressionOptions, + ) -> bindgen::Result<'a, JSValue> { + let level = level_of(&cx.heap, options)?; + Ok(schedule(cx, data, true, level)) } - #[bun_jsc::host_fn] - pub(crate) fn decompress( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let cx = global_this.js_thread_of_caller(callframe); - let (buffer, _, _) = get_options_async(global_this, callframe)?; - Ok(create_job(&cx, buffer, false, 0)) // level is ignored for decompression + pub(crate) fn decompress(cx: &mut JsScope<'_>, data: PinnedBytes) -> JSValue { + schedule(cx, data, false, 0) } } // NOTE: symbols are linked via the `#[unsafe(no_mangle)]` exports above. -// Referenced: Crypto::JSPasswordObject::JSPasswordObject__create, -// bun_jsc::btjs::dump_btjs_trace. +// Referenced: bun_jsc::btjs::dump_btjs_trace. // LazyProperty initializers for stdin/stderr/stdout // diff --git a/src/runtime/api/HashObject.bind.ts b/src/runtime/api/HashObject.bind.ts new file mode 100644 index 000000000000..1c38584708e2 --- /dev/null +++ b/src/runtime/api/HashObject.bind.ts @@ -0,0 +1,32 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::api::hash_object" }); + +const data = b.bytes({ + from: ["string", "buffer", "blob"], + otherwise: "toString", + refuse: ["undefined", "null"], +}); +/** A seed that is neither a number nor a BigInt is not converted: it is 0. */ +const seed = b.any; +const unseeded32 = { args: { data }, returns: b.u32 }; +const seeded32 = { args: { data, seed }, returns: b.u32, length: 1 }; +const seeded64 = { args: { data, seed }, returns: b.u64.asBigInt, length: 1 }; + +export const hash = b.namespace( + { + wyhash: seeded64, + adler32: unseeded32, + crc32: seeded32, + cityHash32: unseeded32, + cityHash64: seeded64, + xxHash32: seeded32, + xxHash64: seeded64, + xxHash3: seeded64, + murmur32v2: seeded32, + murmur32v3: seeded32, + murmur64v2: seeded64, + rapidhash: seeded64, + }, + { call: "wyhash" }, +); diff --git a/src/runtime/api/HashObject.rs b/src/runtime/api/HashObject.rs index 6a55bbdc6836..ecdd18bf1899 100644 --- a/src/runtime/api/HashObject.rs +++ b/src/runtime/api/HashObject.rs @@ -1,306 +1,64 @@ -use crate::webcore::Blob; -use bun_core::Utf8Bytes; -use bun_jsc::{self as jsc, CallFrame, JSFunction, JSGlobalObject, JSValue, JsResult}; +use bun_jsc::bindgen::JsLocal; -// ────────────────────────────────────────────────────────────────────────── -// Hash algorithm abstraction -// -// The underlying hashers have inconsistent interfaces (1-arg vs 2-arg, -// seed-first vs bytes-first, seeded vs unseeded). A trait unifies them: -// every hasher presents a uniform `hash(seed, input) -> Output` and the -// per-type impl absorbs the signature differences. -// ────────────────────────────────────────────────────────────────────────── - -trait HashOutput: Copy { - fn to_js(self, global: &JSGlobalObject) -> JsResult; -} - -impl HashOutput for u32 { - #[inline] - fn to_js(self, _global: &JSGlobalObject) -> JsResult { - Ok(JSValue::js_number(f64::from(self))) - } -} - -impl HashOutput for u64 { - #[inline] - fn to_js(self, global: &JSGlobalObject) -> JsResult { - JSValue::from_uint64_no_truncate(global, self) - } -} - -trait HashAlgorithm { - type Output: HashOutput; - /// `seed` is always passed as u64 from JS; impls truncate to their native - /// seed width. Hashers that take no seed (Adler32) ignore it. - fn hash(seed: u64, input: &[u8]) -> Self::Output; -} - -// ────────────────────────────────────────────────────────────────────────── -// Hasher impls — one unit struct per algorithm. -// Each must produce output **bit-identical** to previous Bun releases -// (pinned by the vector suite in test/js/bun/util/hash.test.js). -// ────────────────────────────────────────────────────────────────────────── - -struct Wyhash; -impl HashAlgorithm for Wyhash { - type Output = u64; - fn hash(seed: u64, input: &[u8]) -> u64 { - bun_wyhash::Wyhash::hash(seed, input) - } -} - -struct Adler32; -impl HashAlgorithm for Adler32 { - type Output = u32; - fn hash(_seed: u64, input: &[u8]) -> u32 { - // Single-arg, seed ignored. - bun_hash::Adler32::hash(input) +/// Each hasher truncates it to its native seed width. +fn seed_of(seed: JsLocal<'_>) -> u64 { + let seed = seed.to_js(); + if seed.is_number() || seed.is_big_int() { + seed.to_uint64_no_truncate() + } else { + 0 } } -/// Use hardware-accelerated CRC32 from zlib -struct Crc32; -impl HashAlgorithm for Crc32 { - type Output = u32; - fn hash(seed: u64, input: &[u8]) -> u32 { - bun_zlib::crc32_bytes(seed as u32, input) - } -} +// Each must produce output **bit-identical** to previous Bun releases (pinned by the vector suite in +// test/js/bun/util/hash.test.js). -struct CityHash32; -impl HashAlgorithm for CityHash32 { - type Output = u32; - fn hash(_seed: u64, input: &[u8]) -> u32 { - // Single-arg, seed ignored (the JS seed is never read; CityHash32 - // has no seeded variant here). - bun_hash::CityHash32::hash(input) - } +pub(crate) fn wyhash(data: &[u8], seed: JsLocal<'_>) -> u64 { + bun_wyhash::Wyhash::hash(seed_of(seed), data) } -struct CityHash64; -impl HashAlgorithm for CityHash64 { - type Output = u64; - fn hash(seed: u64, input: &[u8]) -> u64 { - bun_hash::CityHash64::hash_with_seed(input, seed) - } +pub(crate) fn adler32(data: &[u8]) -> u32 { + bun_hash::Adler32::hash(data) } -struct XxHash32; -impl HashAlgorithm for XxHash32 { - type Output = u32; - fn hash(seed: u64, input: &[u8]) -> u32 { - // sidestep .hash taking in anytype breaking ArgTuple - // downstream by forcing a type signature on the input - bun_hash::XxHash32::hash(seed as u32, input) - } +/// Hardware-accelerated, from zlib. +pub(crate) fn crc32(data: &[u8], seed: JsLocal<'_>) -> u32 { + bun_zlib::crc32_bytes(seed_of(seed) as u32, data) } -struct XxHash64; -impl HashAlgorithm for XxHash64 { - type Output = u64; - fn hash(seed: u64, input: &[u8]) -> u64 { - // sidestep .hash taking in anytype breaking ArgTuple - // downstream by forcing a type signature on the input - bun_hash::XxHash64::hash(seed, input) - } +pub(crate) fn city_hash32(data: &[u8]) -> u32 { + bun_hash::CityHash32::hash(data) } -struct XxHash3; -impl HashAlgorithm for XxHash3 { - type Output = u64; - fn hash(seed: u64, input: &[u8]) -> u64 { - // Runtime-dispatched SIMD xxHash3 (Highway); see - // src/jsc/bindings/xxhash3.cpp. Output is bit-identical to the xxHash - // reference, pinned by the vector suite in test/js/bun/util/hash.test.js. - // - // The seed is truncated to u32 before widening back to XxHash3's - // native u64 — preserve that truncation for output stability. - bun_highway::xxhash3_64(seed as u32 as u64, input) - } +pub(crate) fn city_hash64(data: &[u8], seed: JsLocal<'_>) -> u64 { + bun_hash::CityHash64::hash_with_seed(data, seed_of(seed)) } -struct Murmur32v2; -impl HashAlgorithm for Murmur32v2 { - type Output = u32; - fn hash(seed: u64, input: &[u8]) -> u32 { - bun_hash::Murmur2_32::hash_with_seed(input, seed as u32) - } +pub(crate) fn xx_hash32(data: &[u8], seed: JsLocal<'_>) -> u32 { + bun_hash::XxHash32::hash(seed_of(seed) as u32, data) } -struct Murmur32v3; -impl HashAlgorithm for Murmur32v3 { - type Output = u32; - fn hash(seed: u64, input: &[u8]) -> u32 { - bun_hash::Murmur3_32::hash_with_seed(input, seed as u32) - } +pub(crate) fn xx_hash64(data: &[u8], seed: JsLocal<'_>) -> u64 { + bun_hash::XxHash64::hash(seed_of(seed), data) } -struct Murmur64v2; -impl HashAlgorithm for Murmur64v2 { - type Output = u64; - fn hash(seed: u64, input: &[u8]) -> u64 { - bun_hash::Murmur2_64::hash_with_seed(input, seed) - } +/// Runtime-dispatched SIMD (Highway): src/jsc/bindings/xxhash3.cpp. The seed is truncated to 32 bits. +pub(crate) fn xx_hash3(data: &[u8], seed: JsLocal<'_>) -> u64 { + bun_highway::xxhash3_64(u64::from(seed_of(seed) as u32), data) } -struct Rapidhash; -impl HashAlgorithm for Rapidhash { - type Output = u64; - fn hash(seed: u64, input: &[u8]) -> u64 { - bun_hash::RapidHash::hash(seed, input) - } +pub(crate) fn murmur32v2(data: &[u8], seed: JsLocal<'_>) -> u32 { + bun_hash::Murmur2_32::hash_with_seed(data, seed_of(seed) as u32) } -// ────────────────────────────────────────────────────────────────────────── -// Host functions — one per algorithm, each an explicit monomorphization of -// `hash_wrap`. -// ────────────────────────────────────────────────────────────────────────── - -#[bun_jsc::host_fn] -fn wyhash(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - hash_wrap::(global, frame) +pub(crate) fn murmur32v3(data: &[u8], seed: JsLocal<'_>) -> u32 { + bun_hash::Murmur3_32::hash_with_seed(data, seed_of(seed) as u32) } -#[bun_jsc::host_fn] -fn adler32(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - hash_wrap::(global, frame) +pub(crate) fn murmur64v2(data: &[u8], seed: JsLocal<'_>) -> u64 { + bun_hash::Murmur2_64::hash_with_seed(data, seed_of(seed)) } -// phase-d: explicit export name — bare `#[host_fn]` defaults the C symbol to -// the Rust ident (`crc32`), which collides with `node_zlib_binding::crc32`'s -// shim. The shim ident (`__jsc_host_crc32`) is unchanged, so `create()` below -// keeps resolving. -#[bun_jsc::host_fn(export = "Bun__HashObject__crc32")] -fn crc32(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - hash_wrap::(global, frame) -} - -#[bun_jsc::host_fn] -fn city_hash32(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - hash_wrap::(global, frame) -} - -#[bun_jsc::host_fn] -fn city_hash64(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - hash_wrap::(global, frame) -} - -#[bun_jsc::host_fn] -fn xx_hash32(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - hash_wrap::(global, frame) -} - -#[bun_jsc::host_fn] -fn xx_hash64(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - hash_wrap::(global, frame) -} - -#[bun_jsc::host_fn] -fn xx_hash3(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - hash_wrap::(global, frame) -} - -#[bun_jsc::host_fn] -fn murmur32v2(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - hash_wrap::(global, frame) -} - -#[bun_jsc::host_fn] -fn murmur32v3(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - hash_wrap::(global, frame) -} - -#[bun_jsc::host_fn] -fn murmur64v2(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - hash_wrap::(global, frame) -} - -#[bun_jsc::host_fn] -fn rapidhash(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - hash_wrap::(global, frame) -} - -// ────────────────────────────────────────────────────────────────────────── - -pub(crate) fn create(global: &JSGlobalObject) -> JSValue { - // `Bun.hash` is itself callable (wyhash); the named algorithms hang off it. - JSFunction::create(global, "hash", __jsc_host_wyhash, 1, Default::default()).put_host_functions( - global, - &[ - ("wyhash", __jsc_host_wyhash, 1), - ("adler32", __jsc_host_adler32, 1), - ("crc32", __jsc_host_crc32, 1), - ("cityHash32", __jsc_host_city_hash32, 1), - ("cityHash64", __jsc_host_city_hash64, 1), - ("xxHash32", __jsc_host_xx_hash32, 1), - ("xxHash64", __jsc_host_xx_hash64, 1), - ("xxHash3", __jsc_host_xx_hash3, 1), - ("murmur32v2", __jsc_host_murmur32v2, 1), - ("murmur32v3", __jsc_host_murmur32v3, 1), - ("murmur64v2", __jsc_host_murmur64v2, 1), - ("rapidhash", __jsc_host_rapidhash, 1), - ], - ) -} - -fn hash_wrap(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - // SAFETY: `bun_vm()` never returns null for a Bun-owned global; - // ArgumentsSlice borrows it for the call. - let mut args = jsc::ArgumentsSlice::init(global.bun_vm(), frame.arguments()); - - let mut input: &[u8] = b""; - let input_slice: Utf8Bytes; - // Hoisted so `array_buffer` outlives the borrow stored in `input`. - let array_buffer; - if let Some(arg) = args.next_eat() { - if let Some(blob) = arg.as_class_ref::() { - // TODO: files - input = blob.shared_view(); - } else { - match arg.js_type_loose() { - jsc::JSType::ArrayBuffer - | jsc::JSType::Int8Array - | jsc::JSType::Uint8Array - | jsc::JSType::Uint8ClampedArray - | jsc::JSType::Int16Array - | jsc::JSType::Uint16Array - | jsc::JSType::Int32Array - | jsc::JSType::Uint32Array - | jsc::JSType::Float16Array - | jsc::JSType::Float32Array - | jsc::JSType::Float64Array - | jsc::JSType::BigInt64Array - | jsc::JSType::BigUint64Array - | jsc::JSType::DataView => { - array_buffer = match arg.as_array_buffer(global) { - Some(ab) => ab, - None => { - return Err(global.throw_invalid_arguments(format_args!( - "ArrayBuffer conversion error" - ))); - } - }; - input = array_buffer.byte_slice(); - } - _ => { - input_slice = arg.to_utf8(global)?; - input = input_slice.slice(); - } - } - } - } - - // The per-algorithm hash/hashWithSeed signature differences are absorbed - // into `HashAlgorithm::hash` per-impl above; here we always read an - // optional seed and pass it. - let mut seed: u64 = 0; - if let Some(arg) = args.next_eat() { - if arg.is_number() || arg.is_big_int() { - seed = arg.to_uint64_no_truncate(); - } - } - - let value = H::hash(seed, input); - value.to_js(global) +pub(crate) fn rapidhash(data: &[u8], seed: JsLocal<'_>) -> u64 { + bun_hash::RapidHash::hash(seed_of(seed), data) } diff --git a/src/runtime/api/JSON5Object.bind.ts b/src/runtime/api/JSON5Object.bind.ts new file mode 100644 index 000000000000..b73b8159d218 --- /dev/null +++ b/src/runtime/api/JSON5Object.bind.ts @@ -0,0 +1,24 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::api::json5_object" }); + +export const JSON5 = b.namespace({ + parse: { + args: { + input: b.bytes({ + from: ["string", "buffer", "blob"], + otherwise: "toString", + refuse: ["undefined", "null"], + }), + }, + returns: b.any, + capability: "heap", + throws: true, + }, + stringify: { + args: { value: b.any, replacer: b.any, space: b.any }, + returns: b.any, + capability: "js", + throws: true, + }, +}); diff --git a/src/runtime/api/JSON5Object.rs b/src/runtime/api/JSON5Object.rs index fd99ef0c6c9f..987e50863b9a 100644 --- a/src/runtime/api/JSON5Object.rs +++ b/src/runtime/api/JSON5Object.rs @@ -2,24 +2,18 @@ use bun_collections::HashMap; use bun_core::StackCheck; use bun_core::String as BunString; use bun_js_parser::lexer; -use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult, wtf}; +use bun_jsc::bindgen::{JsHeap, JsLocal, JsScope}; +use bun_jsc::{self as jsc, JSGlobalObject, JSValue, JsError, JsResult, wtf}; use bun_parsers::json5; -pub(crate) fn create(global: &JSGlobalObject) -> JSValue { - jsc::create_host_function_object( - global, - &[ - ("parse", __jsc_host_parse, 1), - ("stringify", __jsc_host_stringify, 3), - ], - ) -} - -#[bun_jsc::host_fn] -fn stringify(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let [value, replacer, space_value] = frame.arguments_as_array::<3>(); - - value.ensure_still_alive(); +pub(crate) fn stringify<'a>( + cx: &mut JsScope<'a>, + value: JsLocal<'a>, + replacer: JsLocal<'a>, + space: JsLocal<'a>, +) -> JsResult { + let global = cx.global(); + let (value, replacer, space_value) = (value.to_js(), replacer.to_js(), space.to_js()); if value.is_undefined() || value.is_symbol() || value.is_function() { return Ok(JSValue::UNDEFINED); @@ -43,16 +37,13 @@ fn stringify(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { stringifier.builder.to_string(global) } -#[bun_jsc::host_fn] -pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - super::with_text_format_source( - global, - frame, - b"input.json5", - super::BlobOrBufferInput::Bytes, - super::NullishInput::Throw, - |bump, log, source| { - let root = match json5::JSON5Parser::parse(source, log, bump) { +pub(crate) fn parse(heap: &JsHeap<'_>, input: &[u8]) -> JsResult { + // SAFETY: parsing runs no JavaScript. Nor does making the result: its keys are primitives, and its properties are + // defined and not set. What throws returns without reading `input` again. + let global = unsafe { heap.global() }; + super::with_text_format_source(global, input, b"input.json5", |bump, log, source| { + let root = + match json5::JSON5Parser::parse(source, log, bump) { Ok(r) => r, Err(json5::ExternalError::OutOfMemory) => return Err(JsError::OutOfMemory), Err(json5::ExternalError::StackOverflow) => { @@ -74,9 +65,8 @@ pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult JSValue { - bun_jsc::create_host_function_object(global, &[("parse", __jsc_host_parse, 1)]) -} - -#[bun_jsc::host_fn] -pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - super::with_text_format_source( - global, - frame, - b"input.jsonc", - super::BlobOrBufferInput::ToString, - super::NullishInput::Throw, - |_arena, log, source| { - // parse_jsonc maps empty input to {}; the public API rejects it like JSON.parse. - if source.contents.is_empty() { - return Err( - global.throw_value(global.create_syntax_error_instance(format_args!( - "JSONC Parse error: Unexpected end of input" - ))), - ); - } - let parsed = match json::ParsedJson::parse_jsonc(source, log) { +pub(crate) fn parse(heap: &JsHeap<'_>, input: &[u8]) -> JsResult { + // SAFETY: parsing runs no JavaScript. Nor does making the result: its keys are primitives, and its properties are + // defined and not set. What throws returns without reading `input` again. + let global = unsafe { heap.global() }; + super::with_text_format_source(global, input, b"input.jsonc", |_, log, source| { + // parse_jsonc maps empty input to {}; the public API rejects it like JSON.parse. + if source.contents.is_empty() { + return Err( + global.throw_value(global.create_syntax_error_instance(format_args!( + "JSONC Parse error: Unexpected end of input" + ))), + ); + } + let parsed = + match json::ParsedJson::parse_jsonc(source, log) { Ok(v) => v, Err(bun_parsers::Error::StackOverflow) => { return Err(global.throw_stack_overflow()); @@ -54,10 +48,9 @@ pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult JSValue { - bun_jsc::create_host_function_object( - global, - &[ - ("parse", __jsc_host_parse, 1), - ("stringify", __jsc_host_stringify, 3), - ], - ) -} - -#[bun_jsc::host_fn] -pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - super::with_text_format_source( - global, - frame, - b"input.toml", - super::BlobOrBufferInput::Bytes, - super::NullishInput::Throw, - |arena, log, source| { - let root = match TOML::parse(source, log, arena, false) { +pub(crate) fn parse(heap: &JsHeap<'_>, input: &[u8]) -> JsResult { + // SAFETY: parsing runs no JavaScript. Nor does making the result: its keys are strings, its properties are defined + // and not set, and a date or a time is `Temporal.*.from` of a primitive string, which looks nothing up. What throws + // returns without reading `input` again. + let global = unsafe { heap.global() }; + super::with_text_format_source(global, input, b"input.toml", |arena, log, source| { + let root = + match TOML::parse(source, log, arena, false) { Ok(v) => v, Err(bun_parsers::Error::StackOverflow) => { return Err(global.throw_stack_overflow()); @@ -49,18 +36,17 @@ pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult JsResult { - // `space` is accepted for signature parity with YAML/JSON5 but ignored: - // TOML output is line-oriented and has no nesting indentation. - let [value, replacer, _space] = frame.arguments_as_array::<3>(); - - value.ensure_still_alive(); +pub(crate) fn stringify<'a>( + cx: &mut JsScope<'a>, + value: JsLocal<'a>, + replacer: JsLocal<'a>, +) -> JsResult { + let global = cx.global(); + let (value, replacer) = (value.to_js(), replacer.to_js()); if value.is_undefined() || value.is_symbol() || value.is_function() { return Ok(JSValue::UNDEFINED); diff --git a/src/runtime/api/XMLObject.bind.ts b/src/runtime/api/XMLObject.bind.ts new file mode 100644 index 000000000000..0956925aecae --- /dev/null +++ b/src/runtime/api/XMLObject.bind.ts @@ -0,0 +1,28 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::api::xml_object" }); + +export const XML = b.namespace({ + parse: { + args: { + /** A string is parsed in its own encoding, and not as UTF-8. */ + input: b.bytes({ + from: ["string", "buffer", "blob"], + otherwise: "toString", + refuse: ["undefined", "null"], + string: "asIs", + held: "handle", + }), + options: b.any, + }, + returns: b.any, + capability: "js", + throws: true, + }, + stringify: { + args: { value: b.any, replacer: b.any, space: b.any }, + returns: b.any, + capability: "js", + throws: true, + }, +}); diff --git a/src/runtime/api/XMLObject.rs b/src/runtime/api/XMLObject.rs index 2a9069035476..e72b76c971e3 100644 --- a/src/runtime/api/XMLObject.rs +++ b/src/runtime/api/XMLObject.rs @@ -1,4 +1,4 @@ -//! `Bun.XML` — `parse()` and `stringify()` host functions. +//! `Bun.XML` — `parse()` and `stringify()`. //! //! `parse` builds one of two shapes from `bun_parsers::xml` (see bun.d.ts //! for the user-facing description): the compact object (default) — @@ -14,24 +14,19 @@ use bun_collections::HashMap; use bun_core::String as BunString; use bun_core::{StackCheck, strings}; use bun_js_parser_jsc::ExprJsc; -use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult, wtf}; +use bun_jsc::bindgen::{JsLocal, JsScope, StringOrJsBytes}; +use bun_jsc::{self as jsc, JSGlobalObject, JSValue, JsError, JsResult, wtf}; use bun_parsers::xml::{self, XML}; -pub(crate) fn create(global: &JSGlobalObject) -> JSValue { - bun_jsc::create_host_function_object( - global, - &[ - ("parse", __jsc_host_parse, 2), - ("stringify", __jsc_host_stringify, 3), - ], - ) -} - -#[bun_jsc::host_fn] -pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn parse<'a>( + cx: &mut JsScope<'a>, + input: StringOrJsBytes<'a>, + options: JsLocal<'a>, +) -> JsResult { + let global = cx.global(); // A function here is reserved for a reviver; reject it rather than read // it as an options object with no keys. - let options = frame.argument(1); + let options = options.to_js(); let compact = if options.is_undefined_or_null() { true } else if options.is_object() && !options.is_callable() { @@ -47,79 +42,83 @@ pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult xml::InputEncoding::Bytes, - super::SourceEncoding::Utf8Text => xml::InputEncoding::Text, - super::SourceEncoding::Latin1Text => xml::InputEncoding::Latin1, - super::SourceEncoding::Utf16Text => xml::InputEncoding::Text, - }; - bun_core::analytics::Features::xml_parse_inc(); - let options = xml::Options { compact, encoding }; - let mut result = if source_encoding == super::SourceEncoding::Utf16Text { - // The scaffold hands the string's code units over as bytes. - let units: &[u16] = bytemuck::cast_slice(&source.contents); - XML::parse_utf16(source, units, log, arena, options) + // parsed byte-per-character as is, a UTF-16 one by its code units. + let text; + let mut utf16 = None; + let (bytes, encoding): (&[u8], _) = match input { + StringOrJsBytes::Bytes(bytes) => (bytes.get(&cx.heap), xml::InputEncoding::Bytes), + StringOrJsBytes::String(string) => { + text = string; + if text.is_8bit() { + (text.latin1(), xml::InputEncoding::Latin1) } else { - XML::parse(source, log, arena, options) + let units = text.utf16(); + utf16 = Some(units); + (bytemuck::cast_slice(units), xml::InputEncoding::Text) + } + } + }; + // SAFETY: parsing runs no JavaScript. Nor does making the result: its keys are primitives, and its properties are + // defined and not set. What throws returns without reading `input` again. + let global = unsafe { cx.heap.global() }; + + super::with_text_format_source(global, bytes, b"input.xml", |arena, log, source| { + bun_core::analytics::Features::xml_parse_inc(); + let options = xml::Options { compact, encoding }; + let mut result = match utf16 { + Some(units) => XML::parse_utf16(source, units, log, arena, options), + None => XML::parse(source, log, arena, options), + }; + let utf8; + let utf8_source; + if matches!(result, Err(bun_parsers::Error::NeedsWiderEncoding)) { + // A character reference the Latin-1 result cannot hold: once + // more, over the same text as UTF-8. + utf8 = strings::allocate_latin1_into_utf8(&source.contents) + .map_err(|_| JsError::OutOfMemory)?; + utf8_source = bun_ast::Source::init_path_string(b"input.xml", &utf8[..]); + *log = bun_ast::Log::init(); + let options = xml::Options { + compact, + encoding: xml::InputEncoding::Text, }; - let utf8; - let utf8_source; - if matches!(result, Err(bun_parsers::Error::NeedsWiderEncoding)) { - // A character reference the Latin-1 result cannot hold: once - // more, over the same text as UTF-8. - utf8 = strings::allocate_latin1_into_utf8(&source.contents) - .map_err(|_| JsError::OutOfMemory)?; - utf8_source = bun_ast::Source::init_path_string(b"input.xml", &utf8[..]); - *log = bun_ast::Log::init(); - let options = xml::Options { - compact, - encoding: xml::InputEncoding::Text, - }; - result = XML::parse(&utf8_source, log, arena, options); - } - let root = match result { - Ok(root) => root, - Err(bun_parsers::Error::StackOverflow) => { - return Err(global.throw_stack_overflow()); - } - Err(bun_parsers::Error::Alloc(_)) => { - return Err(JsError::OutOfMemory); - } - Err(_) => { - if let Some(first_msg) = log.msgs.first() { - return Err(global.throw_value(global.create_syntax_error_instance( - format_args!( - "XML Parse error: {}", - bstr::BStr::new(&first_msg.data.text), - ), - ))); - } + result = XML::parse(&utf8_source, log, arena, options); + } + let root = match result { + Ok(root) => root, + Err(bun_parsers::Error::StackOverflow) => { + return Err(global.throw_stack_overflow()); + } + Err(bun_parsers::Error::Alloc(_)) => { + return Err(JsError::OutOfMemory); + } + Err(_) => { + if let Some(first_msg) = log.msgs.first() { return Err(global.throw_value(global.create_syntax_error_instance( - format_args!("XML Parse error: Unable to parse XML"), + format_args!("XML Parse error: {}", bstr::BStr::new(&first_msg.data.text),), ))); } - }; + return Err( + global.throw_value(global.create_syntax_error_instance(format_args!( + "XML Parse error: Unable to parse XML" + ))), + ); + } + }; - root.to_js(global) - .map_err(|e| bun_js_parser_jsc::to_js_error(e, global)) - }, - ) + root.to_js(global) + .map_err(|e| bun_js_parser_jsc::to_js_error(e, global)) + }) } -#[bun_jsc::host_fn] -fn stringify(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let [value, replacer, space_value] = frame.arguments_as_array::<3>(); - - value.ensure_still_alive(); +pub(crate) fn stringify<'a>( + cx: &mut JsScope<'a>, + value: JsLocal<'a>, + replacer: JsLocal<'a>, + space: JsLocal<'a>, +) -> JsResult { + let global = cx.global(); + let (value, replacer, space_value) = (value.to_js(), replacer.to_js(), space.to_js()); if skipped(value) { return Ok(JSValue::UNDEFINED); diff --git a/src/runtime/api/YAMLObject.bind.ts b/src/runtime/api/YAMLObject.bind.ts new file mode 100644 index 000000000000..88e531ee9bf5 --- /dev/null +++ b/src/runtime/api/YAMLObject.bind.ts @@ -0,0 +1,26 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::api::yaml_object" }); + +export const YAML = b.namespace({ + parse: { + /** A key that is a sequence or a mapping is converted to a string, which can run JavaScript, once the input is parsed. */ + args: { + input: b.bytes({ + from: ["string", "buffer", "blob"], + otherwise: "toString", + shared: "copied", + held: "handle", + }), + }, + returns: b.any, + capability: "js", + throws: true, + }, + stringify: { + args: { value: b.any, replacer: b.any, space: b.any }, + returns: b.any, + capability: "js", + throws: true, + }, +}); diff --git a/src/runtime/api/YAMLObject.rs b/src/runtime/api/YAMLObject.rs index dc97756c967d..1d93d556faf2 100644 --- a/src/runtime/api/YAMLObject.rs +++ b/src/runtime/api/YAMLObject.rs @@ -5,36 +5,38 @@ use bun_ast::{Expr, expr::Data as ExprData}; use bun_collections::{HashMap, StringHashMap}; use bun_core::StackCheck; use bun_core::String as BunString; +use bun_jsc::bindgen::{JsBytes, JsLocal, JsScope}; use bun_jsc::{ - self as jsc, CallFrame, JSGlobalObject, JSPropertyIterator, JSPropertyIteratorOptions, JSValue, - JsError, JsResult, MarkedArgumentBuffer, wtf, + JSGlobalObject, JSPropertyIterator, JSPropertyIteratorOptions, JSValue, JsError, JsResult, + MarkedArgumentBuffer, wtf, }; use bun_parsers::yaml::{CyclicAliases, YAML, YamlParseError}; -pub(crate) fn create(global_this: &JSGlobalObject) -> JSValue { - jsc::create_host_function_object( - global_this, - &[ - ("parse", __jsc_host_parse, 1), - ("stringify", __jsc_host_stringify, 3), - ], - ) -} - -#[bun_jsc::host_fn] -fn stringify(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { - MarkedArgumentBuffer::new(|roots| stringify_impl(global, call_frame, roots)) +pub(crate) fn stringify<'a>( + cx: &mut JsScope<'a>, + value: JsLocal<'a>, + replacer: JsLocal<'a>, + space: JsLocal<'a>, +) -> JsResult { + let global = cx.global(); + MarkedArgumentBuffer::new(|roots| { + stringify_impl( + global, + value.to_js(), + replacer.to_js(), + space.to_js(), + roots, + ) + }) } fn stringify_impl( global: &JSGlobalObject, - call_frame: &CallFrame, + value: JSValue, + replacer: JSValue, + space_value: JSValue, roots: &mut MarkedArgumentBuffer, ) -> JsResult { - let [value, replacer, space_value] = call_frame.arguments_as_array::<3>(); - - value.ensure_still_alive(); - if value.is_undefined() || value.is_symbol() || value.is_function() { return Ok(JSValue::UNDEFINED); } @@ -1062,11 +1064,14 @@ fn is_inf_suffix(str: &BunString, i: usize) -> bool { || (a == 0x49 /* 'I' */ && b == 0x4e /* 'N' */ && c == 0x46/* 'F' */) } -#[bun_jsc::host_fn] -pub(crate) fn parse(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { +pub(crate) fn parse<'a>(cx: &mut JsScope<'a>, input: JsBytes<'a>) -> JsResult { MarkedArgumentBuffer::new(|roots| { let mut pending = Vec::new(); - let result = parse_to_values(global, call_frame, roots, &mut pending)?; + // SAFETY: parsing runs no JavaScript. Nor does making the values: what would is left in `pending`. What throws + // returns without reading `input` again. + let global = unsafe { cx.heap.global() }; + let result = parse_to_values(global, input.get(&cx.heap), roots, &mut pending)?; + let global = cx.global(); for PendingProperty { mapping, key, @@ -1089,21 +1094,15 @@ struct PendingProperty { fn parse_to_values( global: &JSGlobalObject, - call_frame: &CallFrame, + input: &[u8], roots: &mut MarkedArgumentBuffer, pending: &mut Vec, ) -> JsResult { - // `NullishInput::ToString` preserves YAML's coerce-undefined-to-"undefined" behavior. - super::with_text_format_source( - global, - call_frame, - b"input.yaml", - super::BlobOrBufferInput::Bytes, - super::NullishInput::ToString, - |arena, log, source| { - // `ParserCtx::to_js` materializes each `E::Array`/`E::Object` - // once by pointer identity, so a cyclic graph is fine here. - let root = match YAML::parse(source, log, arena, CyclicAliases::Allow) { + super::with_text_format_source(global, input, b"input.yaml", |arena, log, source| { + // `ParserCtx::to_js` materializes each `E::Array`/`E::Object` + // once by pointer identity, so a cyclic graph is fine here. + let root = + match YAML::parse(source, log, arena, CyclicAliases::Allow) { Ok(root) => root, Err(YamlParseError::OutOfMemory) => return Err(JsError::OutOfMemory), Err(YamlParseError::StackOverflow) => return Err(global.throw_stack_overflow()), @@ -1121,20 +1120,19 @@ fn parse_to_values( } }; - let mut ctx = ParserCtx { - seen_objects: HashMap::default(), - stack_check: StackCheck::init(), - global, - pending, - }; + let mut ctx = ParserCtx { + seen_objects: HashMap::default(), + stack_check: StackCheck::init(), + global, + pending, + }; - ctx.to_js(roots, root).map_err(|err| match err { - ToJsError::OutOfMemory => JsError::OutOfMemory, - ToJsError::JsError => JsError::Thrown, - ToJsError::StackOverflow => global.throw_stack_overflow(), - }) - }, - ) + ctx.to_js(roots, root).map_err(|err| match err { + ToJsError::OutOfMemory => JsError::OutOfMemory, + ToJsError::JsError => JsError::Thrown, + ToJsError::StackOverflow => global.throw_stack_overflow(), + }) + }) } struct ParserCtx<'a> { diff --git a/src/runtime/api/csrf_jsc.rs b/src/runtime/api/csrf_jsc.rs index c28167651168..a152f3027481 100644 --- a/src/runtime/api/csrf_jsc.rs +++ b/src/runtime/api/csrf_jsc.rs @@ -2,26 +2,22 @@ use bun_boringssl_sys as boring; use bun_csrf as csrf; +use bun_jsc::JSValue; use bun_jsc::bindgen::{self, JsHeap, JsScope}; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{ErrorCode, JSValue}; use crate::api::crypto::evp::Algorithm as EvpAlgorithm; use crate::crypto::evp; use crate::generated_bindgen::csrf_jsc::{CSRFGenerateOptions, CSRFVerifyOptions}; use crate::node::Encoding as NodeEncoding; -fn invalid_arguments<'a>(heap: &JsHeap<'a>, message: &str) -> bindgen::Error<'a> { - heap.error_with_code(ErrorCode::INVALID_ARG_TYPE, format_args!("{message}")) -} - fn non_empty<'a>( heap: &JsHeap<'a>, value: Option<&'a [u8]>, message: &str, ) -> bindgen::Result<'a, Option<&'a [u8]>> { match value { - Some([]) => Err(invalid_arguments(heap, message)), + Some([]) => Err(heap.invalid_arguments(format_args!("{message}"))), value => Ok(value), } } @@ -37,10 +33,9 @@ fn token_format<'a>( Some(NodeEncoding::Base64) => Ok(csrf::TokenFormat::Base64), Some(NodeEncoding::Base64url) => Ok(csrf::TokenFormat::Base64Url), Some(NodeEncoding::Hex) => Ok(csrf::TokenFormat::Hex), - _ => Err(invalid_arguments( - heap, - "Invalid format: must be 'base64', 'base64url', or 'hex'", - )), + _ => Err(heap.invalid_arguments(format_args!( + "Invalid format: must be 'base64', 'base64url', or 'hex'" + ))), }, } } @@ -58,7 +53,7 @@ fn algorithm<'a>(heap: &JsHeap<'a>, name: Option<&[u8]>) -> bindgen::Result<'a, | EvpAlgorithm::Sha512 | EvpAlgorithm::Sha512_256), ) => Ok(algorithm), - _ => Err(invalid_arguments(heap, "Algorithm not supported")), + _ => Err(heap.invalid_arguments(format_args!("Algorithm not supported"))), } } @@ -125,7 +120,7 @@ pub(crate) fn verify<'a>( bun_analytics::features::csrf_verify.fetch_add(1, core::sync::atomic::Ordering::Relaxed); if token.is_empty() { - return Err(invalid_arguments(heap, "Token must be a non-empty string")); + return Err(heap.invalid_arguments(format_args!("Token must be a non-empty string"))); } let secret = non_empty(heap, options.secret, "Secret must be a non-empty string")?; let session_id = non_empty( diff --git a/src/runtime/crypto/PasswordObject.bind.ts b/src/runtime/crypto/PasswordObject.bind.ts new file mode 100644 index 000000000000..16a66bb86d11 --- /dev/null +++ b/src/runtime/crypto/PasswordObject.bind.ts @@ -0,0 +1,51 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::crypto::password_object" }); + +/** Which of the costs counts depends on `algorithm`, and the others can be anything. */ +export const PasswordHashOptions = b.dictionary( + { + algorithm: b.string.utf8.nullable, + cost: b.any, + timeCost: b.any, + memoryCost: b.any, + }, + { allowArray: true }, +); + +export const PasswordHashAlgorithm = b.union({ + unspecified: b.null, + label: b.string, + options: PasswordHashOptions, +}); + +const stringOrBuffer = b.bytes({ from: ["string", "buffer"] }); +/** For a job to own, and to zero when it is done. */ +const copied = b.bytes({ from: ["string", "buffer"], otherwise: "toString", refuse: ["undefined"], held: "copied" }); + +export const password = b.namespace({ + hash: { + args: { password: copied, algorithm: PasswordHashAlgorithm }, + returns: b.any, + capability: "js", + throws: true, + }, + hashSync: { + args: { password: stringOrBuffer, algorithm: PasswordHashAlgorithm }, + returns: b.string.utf8, + capability: "heap", + throws: true, + }, + verify: { + args: { password: copied, hash: copied, algorithm: b.string.utf8.nullable }, + returns: b.any, + capability: "js", + throws: true, + }, + verifySync: { + args: { password: stringOrBuffer, hash: stringOrBuffer, algorithm: b.string.utf8.nullable }, + returns: b.bool, + capability: "heap", + throws: true, + }, +}); diff --git a/src/runtime/crypto/PasswordObject.rs b/src/runtime/crypto/PasswordObject.rs index 282756e6df27..227212164fa2 100644 --- a/src/runtime/crypto/PasswordObject.rs +++ b/src/runtime/crypto/PasswordObject.rs @@ -4,10 +4,11 @@ use std::io::Write as _; use bun_core::EncodedSlice; use bun_jsc::EncodedSliceJsc as _; -use bun_jsc::{ArrayBuffer, CallFrame, JSFunction, JSGlobalObject, JSValue, JsResult}; +use bun_jsc::bindgen::{self, JsHeap, JsLocal, JsScope}; +use bun_jsc::{JSGlobalObject, JSValue, JsResult}; use bun_jsc::{JSPromise, JSPromiseStrong}; -use crate::node::StringOrBuffer; +use crate::generated_bindgen::password_object::{PasswordHashAlgorithm, PasswordHashOptions}; // The argon2/bcrypt API-surface shim lives at `crypto::pwhash` (this dir); // the implementation is wired there, not here. @@ -43,163 +44,128 @@ impl AlgorithmValue { pub(crate) const DEFAULT: AlgorithmValue = AlgorithmValue::Argon2id(Argon2Params::DEFAULT); - fn from_js(global_object: &JSGlobalObject, value: JSValue) -> JsResult { - if value.is_object() { - if let Some(algorithm_value) = value.get_truthy(global_object, "algorithm")? { - if !algorithm_value.is_string() { - return Err(global_object.throw_invalid_argument_type( - "hash", - "algorithm", - "string", - )); - } + fn with_defaults(algorithm: Algorithm) -> AlgorithmValue { + match algorithm { + Algorithm::Bcrypt => AlgorithmValue::Bcrypt(AlgorithmValue::BCRYPT_DEFAULT), + Algorithm::Argon2id => AlgorithmValue::Argon2id(Argon2Params::DEFAULT), + Algorithm::Argon2d => AlgorithmValue::Argon2d(Argon2Params::DEFAULT), + Algorithm::Argon2i => AlgorithmValue::Argon2i(Argon2Params::DEFAULT), + } + } - let algorithm_string = algorithm_value.to_js_string_view(global_object)?; + fn from_argument<'a>( + heap: &JsHeap<'a>, + algorithm: PasswordHashAlgorithm<'a>, + ) -> bindgen::Result<'a, AlgorithmValue> { + match algorithm { + PasswordHashAlgorithm::Unspecified => Ok(AlgorithmValue::DEFAULT), + PasswordHashAlgorithm::Label(label) => { + Algorithm::from_label(heap, "hash", &label.to_utf8()).map(Self::with_defaults) + } + PasswordHashAlgorithm::Options(options) => Self::from_options(heap, &options), + } + } - let Some(algo) = algorithm_from_string(&algorithm_string) else { - return Err(global_object.throw_invalid_argument_type( - "hash", - "algorithm", - UNKNOWN_PASSWORD_ALGORITHM_MESSAGE, - )); - }; + fn from_options<'a>( + heap: &JsHeap<'a>, + options: &PasswordHashOptions<'a>, + ) -> bindgen::Result<'a, AlgorithmValue> { + let Some(label) = options.algorithm.filter(|label| !label.is_empty()) else { + return Err(invalid_argument_type( + heap, + "hash", + "options.algorithm", + "string", + )); + }; + let invalid = |message: &str| heap.invalid_arguments(format_args!("{message}")); - match algo { - Algorithm::Bcrypt => { - let mut algorithm = AlgorithmValue::Bcrypt(AlgorithmValue::BCRYPT_DEFAULT); - - if let Some(rounds_value) = value.get_truthy(global_object, "cost")? { - if !rounds_value.is_number() { - return Err(global_object - .throw_invalid_argument_type("hash", "cost", "number")); - } - - // Range-check as f64: ToInt32 would wrap e.g. 2^32 + 4 to 4. - let rounds = rounds_value.as_number(); - - if rounds.fract() != 0.0 || !(4.0..=31.0).contains(&rounds) { - return Err(global_object.throw_invalid_arguments(format_args!( - "Rounds must be an integer between 4 and 31" - ))); - } - - algorithm = AlgorithmValue::Bcrypt(rounds as u8); - } - - return Ok(algorithm); - } - Algorithm::Argon2id | Algorithm::Argon2d | Algorithm::Argon2i => { - let mut argon = Argon2Params::default(); - - if let Some(time_value) = value.get_truthy(global_object, "timeCost")? { - if !time_value.is_number() { - return Err(global_object - .throw_invalid_argument_type("hash", "timeCost", "number")); - } - - let time_cost = time_value.as_number(); - - if time_cost < 1.0 || time_cost.is_nan() { - return Err(global_object.throw_invalid_arguments(format_args!( - "Time cost must be greater than 0" - ))); - } - - if time_cost.fract() != 0.0 || time_cost > f64::from(u32::MAX) { - return Err(global_object.throw_invalid_arguments(format_args!( - "Time cost must be an integer between 1 and 4294967295" - ))); - } - - argon.time_cost = time_cost as u32; - } - - if let Some(memory_value) = value.get_truthy(global_object, "memoryCost")? { - if !memory_value.is_number() { - return Err(global_object.throw_invalid_argument_type( - "hash", - "memoryCost", - "number", - )); - } - - let memory_cost = memory_value.as_number(); - - // argon2 requires `memoryCost >= 8 * parallelism`; - // Bun hard-codes `parallelism = 1` (see - // `Argon2Params::to_params`), so the floor is 8. - if memory_cost < 8.0 || memory_cost.is_nan() { - return Err(global_object.throw_invalid_arguments(format_args!( - "Memory cost must be at least 8" - ))); - } - - if memory_cost.fract() != 0.0 || memory_cost > f64::from(u32::MAX) { - return Err(global_object.throw_invalid_arguments(format_args!( - "Memory cost must be an integer between 8 and 4294967295" - ))); - } - - argon.memory_cost = memory_cost as u32; - } - - return Ok(match algo { - Algorithm::Argon2id => AlgorithmValue::Argon2id(argon), - Algorithm::Argon2d => AlgorithmValue::Argon2d(argon), - Algorithm::Argon2i => AlgorithmValue::Argon2i(argon), - Algorithm::Bcrypt => unreachable!(), - }); - } - } - } else { - return Err(global_object.throw_invalid_argument_type( - "hash", - "options.algorithm", - "string", + let algorithm = Algorithm::from_label(heap, "hash", label)?; + if let Algorithm::Bcrypt = algorithm { + let Some(rounds) = cost(heap, options.cost, "cost")? else { + return Ok(Self::with_defaults(algorithm)); + }; + if rounds.fract() != 0.0 || !(4.0..=31.0).contains(&rounds) { + return Err(invalid("Rounds must be an integer between 4 and 31")); + } + return Ok(AlgorithmValue::Bcrypt(rounds as u8)); + } + + let mut argon = Argon2Params::DEFAULT; + if let Some(time_cost) = cost(heap, options.time_cost, "timeCost")? { + if time_cost < 1.0 || time_cost.is_nan() { + return Err(invalid("Time cost must be greater than 0")); + } + if time_cost.fract() != 0.0 || time_cost > f64::from(u32::MAX) { + return Err(invalid( + "Time cost must be an integer between 1 and 4294967295", )); } - } else if value.is_string() { - let algorithm_string = value.to_js_string_view(global_object)?; - - let Some(algo) = algorithm_from_string(&algorithm_string) else { - return Err(global_object.throw_invalid_argument_type( - "hash", - "algorithm", - UNKNOWN_PASSWORD_ALGORITHM_MESSAGE, + argon.time_cost = time_cost as u32; + } + if let Some(memory_cost) = cost(heap, options.memory_cost, "memoryCost")? { + // argon2 requires `memoryCost >= 8 * parallelism`; Bun hard-codes `parallelism = 1` (see + // `Argon2Params::to_params`), so the floor is 8. + if memory_cost < 8.0 || memory_cost.is_nan() { + return Err(invalid("Memory cost must be at least 8")); + } + if memory_cost.fract() != 0.0 || memory_cost > f64::from(u32::MAX) { + return Err(invalid( + "Memory cost must be an integer between 8 and 4294967295", )); - }; - - match algo { - Algorithm::Bcrypt => { - return Ok(AlgorithmValue::Bcrypt(AlgorithmValue::BCRYPT_DEFAULT)); - } - Algorithm::Argon2id => { - return Ok(AlgorithmValue::Argon2id(Argon2Params::default())); - } - Algorithm::Argon2d => { - return Ok(AlgorithmValue::Argon2d(Argon2Params::default())); - } - Algorithm::Argon2i => { - return Ok(AlgorithmValue::Argon2i(Argon2Params::default())); - } } - } else { - return Err(global_object.throw_invalid_argument_type("hash", "algorithm", "string")); + argon.memory_cost = memory_cost as u32; } + Ok(match algorithm { + Algorithm::Argon2id => AlgorithmValue::Argon2id(argon), + Algorithm::Argon2d => AlgorithmValue::Argon2d(argon), + Algorithm::Argon2i => AlgorithmValue::Argon2i(argon), + Algorithm::Bcrypt => unreachable!(), + }) } } -fn algorithm_from_string(s: &bun_core::String) -> Option { - if s.eq_ascii(b"argon2i") { - Some(Algorithm::Argon2i) - } else if s.eq_ascii(b"argon2d") { - Some(Algorithm::Argon2d) - } else if s.eq_ascii(b"argon2id") { - Some(Algorithm::Argon2id) - } else if s.eq_ascii(b"bcrypt") { - Some(Algorithm::Bcrypt) - } else { - None +fn invalid_argument_type<'a>( + heap: &JsHeap<'a>, + function: &str, + field: &str, + typename: &str, +) -> bindgen::Error<'a> { + heap.invalid_arguments(format_args!( + "Expected {field} to be a {typename} for '{function}'." + )) +} + +/// `None` for `undefined`, `null` and `""`. +fn cost<'a>(heap: &JsHeap<'a>, value: JsLocal<'a>, name: &str) -> bindgen::Result<'a, Option> { + let value = value.to_js(); + if value.is_undefined_or_null() || (value.is_string() && !value.to_boolean()) { + return Ok(None); + } + if !value.is_number() { + return Err(invalid_argument_type(heap, "hash", name, "number")); + } + Ok(Some(value.as_number())) +} + +impl Algorithm { + fn from_label<'a>( + heap: &JsHeap<'a>, + function: &str, + label: &[u8], + ) -> bindgen::Result<'a, Algorithm> { + match label { + b"argon2i" => Ok(Algorithm::Argon2i), + b"argon2d" => Ok(Algorithm::Argon2d), + b"argon2id" => Ok(Algorithm::Argon2id), + b"bcrypt" => Ok(Algorithm::Bcrypt), + _ => Err(invalid_argument_type( + heap, + function, + "algorithm", + UNKNOWN_PASSWORD_ALGORITHM_MESSAGE, + )), + } } } @@ -225,12 +191,6 @@ impl Argon2Params { } } -impl Default for Argon2Params { - fn default() -> Self { - Self::DEFAULT - } -} - impl Algorithm { pub(crate) fn get(pw: &[u8]) -> Option { if pw[0] != b'$' { @@ -384,11 +344,9 @@ impl PasswordObject { } // ─────────────────────────────────────────────────────────────────────────── -// JSPasswordObject +// Bun.password // ─────────────────────────────────────────────────────────────────────────── -pub(crate) struct JSPasswordObject; - struct PascalToUpperUnderscoreCaseFormatter<'a> { input: &'a [u8], } @@ -409,58 +367,6 @@ impl fmt::Display for PascalToUpperUnderscoreCaseFormatter<'_> { } } -#[unsafe(no_mangle)] -extern "C" fn JSPasswordObject__create(global_object: &JSGlobalObject) -> JSValue { - let object = JSValue::create_empty_object(global_object, 4); - // `#[bun_jsc::host_fn]` emits an `extern "C"` shim named - // `__jsc_host_`; pass that (not the safe Rust fn) to JSFunction. - object.put( - global_object, - b"hash", - JSFunction::create( - global_object, - "hash", - __jsc_host_js_password_object_hash, - 2, - Default::default(), - ), - ); - object.put( - global_object, - b"hashSync", - JSFunction::create( - global_object, - "hashSync", - __jsc_host_js_password_object_hash_sync, - 2, - Default::default(), - ), - ); - object.put( - global_object, - b"verify", - JSFunction::create( - global_object, - "verify", - __jsc_host_js_password_object_verify, - 2, - Default::default(), - ), - ); - object.put( - global_object, - b"verifySync", - JSFunction::create( - global_object, - "verifySync", - __jsc_host_js_password_object_verify_sync, - 2, - Default::default(), - ), - ); - object -} - // ─── PasswordOp: generic hash/verify off-thread job ─────────────────────── // // Hash and verify jobs differ only in (a) extra input fields, (b) success @@ -468,13 +374,35 @@ extern "C" fn JSPasswordObject__create(global_object: &JSGlobalObject) -> JSValu // both into one `PasswordJob` / `PasswordResult` parameterised on a // `PasswordOp` carrying exactly those three axes. +/// A copy of a password or a hash, zeroed before it is freed. +struct Sensitive(Box<[u8]>); + +impl From> for Sensitive { + fn from(bytes: Vec) -> Self { + Self(bytes.into_boxed_slice()) + } +} + +impl core::ops::Deref for Sensitive { + type Target = [u8]; + fn deref(&self) -> &[u8] { + &self.0 + } +} + +impl Drop for Sensitive { + fn drop(&mut self) { + bun_alloc::free_sensitive(core::mem::take(&mut self.0)); + } +} + pub(crate) trait PasswordOp: Send + 'static { /// Success payload (`Box<[u8]>` for hash, `bool` for verify). type Value: Send; /// "hashing" | "verification" — slotted into the JS Error message. const ERR_VERB: &'static str; /// Off-thread compute. `self` borrows the op so its inputs stay owned by - /// the job and are `free_sensitive`d in the job's / op's `Drop`. + /// the job. fn compute(&self, password: &[u8]) -> Result; /// Convert the success payload to a `JSValue` on the JS thread. fn to_js(value: Self::Value, g: &JSGlobalObject) -> JSValue; @@ -497,16 +425,9 @@ impl PasswordOp for HashOp { } pub(crate) struct VerifyOp { - prev_hash: Box<[u8]>, + prev_hash: Sensitive, algorithm: Option, } -impl Drop for VerifyOp { - fn drop(&mut self) { - // bun.freeSensitive — volatile-zero then free; the job's Drop handles - // `password`, this handles the op-specific `prev_hash`. - bun_alloc::free_sensitive(core::mem::take(&mut self.prev_hash)); - } -} impl PasswordOp for VerifyOp { type Value = bool; const ERR_VERB: &'static str = "verification"; @@ -538,23 +459,23 @@ fn password_error_instance(err: &HashError, verb: &str, g: &JSGlobalObject) -> J instance } +fn password_error<'a>(heap: &JsHeap<'a>, err: &HashError, verb: &str) -> bindgen::Error<'a> { + // SAFETY: it makes an `Error` and a string, and defines `code`. + let global = unsafe { heap.global() }; + bindgen::Error::from_value(JsLocal::new( + heap, + password_error_instance(err, verb, global), + )) +} + /// `Bun.password.hash/verify` off the JS thread: the op and the password are -/// owned copies (zeroed on drop); the promise is the JS side. +/// owned copies; the promise is the JS side. struct PasswordJob { op: Op, - password: Box<[u8]>, + password: Sensitive, value: Option>, } -impl Drop for PasswordJob { - fn drop(&mut self) { - // bun.freeSensitive — volatile-zero the buffer then free; take the Box so - // the field's own Drop sees an empty slice afterwards. Any op-owned - // sensitive buffers (`prev_hash`) are freed by the op's own `Drop`. - bun_alloc::free_sensitive(core::mem::take(&mut self.password)); - } -} - impl bun_jsc::JobContext for PasswordJob { type OffThread = Self; type Js = JSPromiseStrong; @@ -584,293 +505,100 @@ impl bun_jsc::JobContext for PasswordJob { // ─── hash / verify entry points ─────────────────────────────────────────── -impl JSPasswordObject { - /// Shared body of `hash`/`verify`: sync path computes inline and either - /// throws or returns the converted value; async path boxes a - /// `PasswordJob`, refs the loop, and schedules it. - fn run( - cx: &bun_jsc::JsThread<'_>, - password: Box<[u8]>, - op: Op, - ) -> JsResult { - debug_assert!(!password.is_empty()); // caller must check - - if SYNC { - return match op.compute(&password) { - Err(err) => { - let error_instance = password_error_instance(&err, Op::ERR_VERB, cx.global()); - Err(cx.global().throw_value(error_instance)) - } - Ok(v) => Ok(Op::to_js(v, cx.global())), - }; - } - - let promise = JSPromiseStrong::init(cx.global()); - let promise_value = promise.value(); - bun_jsc::Job::>::schedule( - cx, - PasswordJob { - op, - password, - value: None, - }, - promise, - ); - Ok(promise_value) - } - - pub(crate) fn hash( - cx: &bun_jsc::JsThread<'_>, - password: Box<[u8]>, - algorithm: AlgorithmValue, - ) -> JsResult { - Self::run::(cx, password, HashOp { algorithm }) - } +/// Boxes a `PasswordJob`, refs the loop, and schedules it. +fn schedule(cx: &bun_jsc::JsThread<'_>, password: Sensitive, op: Op) -> JSValue { + debug_assert!(!password.is_empty()); // caller must check - pub(crate) fn verify( - cx: &bun_jsc::JsThread<'_>, - password: Box<[u8]>, - prev_hash: Box<[u8]>, - algorithm: Option, - ) -> JsResult { - Self::run::( - cx, + let promise = JSPromiseStrong::init(cx.global()); + let promise_value = promise.value(); + bun_jsc::Job::>::schedule( + cx, + PasswordJob { + op, password, - VerifyOp { - prev_hash, - algorithm, - }, - ) - } + value: None, + }, + promise, + ); + promise_value } -// ─── host functions ─────────────────────────────────────────────────────── - -// Once we have bindings generator, this should be replaced with a generated function -#[bun_jsc::host_fn] -fn js_password_object_hash( - global_object: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let arguments = callframe.arguments(); - - if arguments.len() < 1 { - return Err(global_object.throw_not_enough_arguments("hash", 1, 0)); - } - - let mut algorithm = AlgorithmValue::DEFAULT; +const EMPTY_PASSWORD: &str = "password must not be empty"; - if arguments.len() > 1 && !arguments[1].is_empty_or_undefined_or_null() { - algorithm = AlgorithmValue::from_js(global_object, arguments[1])?; +pub(crate) fn hash<'a>( + cx: &mut JsScope<'a>, + password: Vec, + algorithm: PasswordHashAlgorithm<'a>, +) -> bindgen::Result<'a, JSValue> { + let password = Sensitive::from(password); + let algorithm = AlgorithmValue::from_argument(&cx.heap, algorithm)?; + if password.is_empty() { + return Err(cx.heap.invalid_arguments(format_args!("{EMPTY_PASSWORD}"))); } - // TODO: this most likely should error like `hashSync` instead of stringifying. - // - // fromJS(...) orelse { - // return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray"); - // } - let password_to_hash = StringOrBuffer::from_js_to_owned_slice(global_object, arguments[0])?; - // errdefer bun.default_allocator.free(password_to_hash) — Box<[u8]> drops on `?`. - - if password_to_hash.is_empty() { - return Err( - global_object.throw_invalid_arguments(format_args!("password must not be empty")) - ); - } - - JSPasswordObject::hash::( - &global_object.js_thread_of_caller(callframe), - password_to_hash.into_boxed_slice(), - algorithm, - ) + Ok(schedule(&cx.js_thread(), password, HashOp { algorithm })) } -// Once we have bindings generator, this should be replaced with a generated function -#[bun_jsc::host_fn] -fn js_password_object_hash_sync( - global_object: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let arguments = callframe.arguments(); - - if arguments.len() < 1 { - return Err(global_object.throw_not_enough_arguments("hash", 1, 0)); - } - - let mut algorithm = AlgorithmValue::DEFAULT; - - if arguments.len() > 1 && !arguments[1].is_empty_or_undefined_or_null() { - algorithm = AlgorithmValue::from_js(global_object, arguments[1])?; - } - - let Some(string_or_buffer) = StringOrBuffer::from_js(global_object, arguments[0])? else { - return Err(global_object.throw_invalid_argument_type( - "hash", - "password", - "string or TypedArray", - )); - }; - - if string_or_buffer.slice().is_empty() { - return Err( - global_object.throw_invalid_arguments(format_args!("password must not be empty")) - ); - } - - // The sync path only needs `&[u8]`; copy into a Box to share the async - // signature. - JSPasswordObject::hash::( - &global_object.js_thread_of_caller(callframe), - Box::<[u8]>::from(string_or_buffer.slice()), - algorithm, - ) +pub(crate) fn hash_sync<'a>( + heap: &JsHeap<'a>, + password: &[u8], + algorithm: PasswordHashAlgorithm<'a>, +) -> bindgen::Result<'a, Box<[u8]>> { + let algorithm = AlgorithmValue::from_argument(heap, algorithm)?; + if password.is_empty() { + return Err(heap.invalid_arguments(format_args!("{EMPTY_PASSWORD}"))); + } + PasswordObject::hash(password, algorithm) + .map_err(|err| password_error(heap, &err, HashOp::ERR_VERB)) } -// ─── verify host functions ──────────────────────────────────────────────── - -// Once we have bindings generator, this should be replaced with a generated function -#[bun_jsc::host_fn] -fn js_password_object_verify( - global_object: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let arguments = callframe.arguments(); - - if arguments.len() < 2 { - return Err(global_object.throw_not_enough_arguments("verify", 2, 0)); - } - - let mut algorithm: Option = None; - - if arguments.len() > 2 && !arguments[2].is_empty_or_undefined_or_null() { - if !arguments[2].is_string() { - return Err(global_object.throw_invalid_argument_type("verify", "algorithm", "string")); - } - - let algorithm_string = arguments[2].to_js_string_view(global_object)?; - - let Some(a) = algorithm_from_string(&algorithm_string) else { - return Err(global_object.throw_invalid_argument_type( - "verify", - "algorithm", - UNKNOWN_PASSWORD_ALGORITHM_MESSAGE, - )); - }; - algorithm = Some(a); - } - - // TODO: this most likely should error like `verifySync` instead of stringifying. - // - // fromJS(...) orelse { - // return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray"); - // } - let owned_password = StringOrBuffer::from_js_to_owned_slice(global_object, arguments[0])?; - - // TODO: this most likely should error like `verifySync` instead of stringifying. - // - // fromJS(...) orelse { - // return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray"); - // } - let owned_hash = match StringOrBuffer::from_js_to_owned_slice(global_object, arguments[1]) { - Ok(h) => h, - Err(err) => { - drop(owned_password); - return Err(err); - } - }; - - if owned_hash.is_empty() { - drop(owned_password); - return Ok(JSPromise::resolved_promise_value( - global_object, - JSValue::FALSE, - )); - } +fn verify_algorithm<'a>( + heap: &JsHeap<'a>, + label: Option<&[u8]>, +) -> bindgen::Result<'a, Option> { + label + .map(|label| Algorithm::from_label(heap, "verify", label)) + .transpose() +} - if owned_password.is_empty() { - drop(owned_hash); +pub(crate) fn verify<'a>( + cx: &mut JsScope<'a>, + password: Vec, + prev_hash: Vec, + algorithm: Option<&'a [u8]>, +) -> bindgen::Result<'a, JSValue> { + let password = Sensitive::from(password); + let prev_hash = Sensitive::from(prev_hash); + let algorithm = verify_algorithm(&cx.heap, algorithm)?; + if prev_hash.is_empty() || password.is_empty() { return Ok(JSPromise::resolved_promise_value( - global_object, + cx.global(), JSValue::FALSE, )); } - JSPasswordObject::verify::( - &global_object.js_thread_of_caller(callframe), - owned_password.into_boxed_slice(), - owned_hash.into_boxed_slice(), - algorithm, - ) + Ok(schedule( + &cx.js_thread(), + password, + VerifyOp { + prev_hash, + algorithm, + }, + )) } -// Once we have bindings generator, this should be replaced with a generated function -#[bun_jsc::host_fn] -fn js_password_object_verify_sync( - global_object: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let arguments = callframe.arguments(); - - if arguments.len() < 2 { - return Err(global_object.throw_not_enough_arguments("verify", 2, 0)); - } - - let mut algorithm: Option = None; - - if arguments.len() > 2 && !arguments[2].is_empty_or_undefined_or_null() { - if !arguments[2].is_string() { - return Err(global_object.throw_invalid_argument_type("verify", "algorithm", "string")); - } - - let algorithm_string = arguments[2].to_js_string_view(global_object)?; - - let Some(a) = algorithm_from_string(&algorithm_string) else { - return Err(global_object.throw_invalid_argument_type( - "verify", - "algorithm", - UNKNOWN_PASSWORD_ALGORITHM_MESSAGE, - )); - }; - algorithm = Some(a); - } - - let Some(mut password) = StringOrBuffer::from_js(global_object, arguments[0])? else { - return Err(global_object.throw_invalid_argument_type( - "verify", - "password", - "string or TypedArray", - )); - }; - - let Some(hash_) = StringOrBuffer::from_js(global_object, arguments[1])? else { - drop(password); - return Err(global_object.throw_invalid_argument_type( - "verify", - "hash", - "string or TypedArray", - )); - }; - - if let StringOrBuffer::Buffer(buffer) = &mut password { - buffer.buffer = ArrayBuffer::from_typed_array(global_object, buffer.buffer.value); - } - - if hash_.slice().is_empty() { - return Ok(JSValue::FALSE); - } - - if password.slice().is_empty() { - return Ok(JSValue::FALSE); - } - - // The sync path only needs `&[u8]`; copy into Boxes to share the async - // signature. - JSPasswordObject::verify::( - &global_object.js_thread_of_caller(callframe), - Box::<[u8]>::from(password.slice()), - Box::<[u8]>::from(hash_.slice()), - algorithm, - ) +pub(crate) fn verify_sync<'a>( + heap: &JsHeap<'a>, + password: &[u8], + hash: &[u8], + algorithm: Option<&'a [u8]>, +) -> bindgen::Result<'a, bool> { + let algorithm = verify_algorithm(heap, algorithm)?; + if hash.is_empty() || password.is_empty() { + return Ok(false); + } + PasswordObject::verify(password, hash, algorithm) + .map_err(|err| password_error(heap, &err, VerifyOp::ERR_VERB)) } const UNKNOWN_PASSWORD_ALGORITHM_MESSAGE: &str = "unknown algorithm, expected one of: \"bcrypt\", \"argon2id\", \"argon2d\", \"argon2i\" (default is \"argon2id\")"; diff --git a/src/runtime/node/types.rs b/src/runtime/node/types.rs index 7ce362789056..f9b1f2e11dc8 100644 --- a/src/runtime/node/types.rs +++ b/src/runtime/node/types.rs @@ -8,6 +8,7 @@ use bun_core::ZStr; use bun_core::{self, Utf8Bytes, Utf8WithString, fmt as bun_fmt}; use bun_jsc::bun_string_jsc; use bun_jsc::{StringJsc as _, Utf8WithStringJsc as _}; +pub(crate) use bun_jsc::{ThreadIsolated, ThreadIsolatedArg}; #[cfg(windows)] use bun_paths::WPathBuffer; use bun_paths::{MAX_PATH_BYTES, OSPathBuffer, OSPathSliceZ, PathBuffer}; @@ -238,44 +239,6 @@ impl BlobOrStringOrBuffer { // ────────────────────────────────────────────────────────────────────────── -/// # Safety -/// As the type's `from_js_async` parser or owned constructor builds it — JS -/// buffers pinned and GC-rooted, strings thread-isolated — nothing in it is -/// thread-affine: a work-pool job may read it and the JS thread drops it. -pub(crate) unsafe trait ThreadIsolatedArg {} - -/// A `T` built by its `from_js_async` parser or owned constructor (see -/// [`ThreadIsolatedArg`]); nothing else constructs one. -pub(crate) struct ThreadIsolated(T); - -impl ThreadIsolated { - /// # Safety - /// `value` was built as [`ThreadIsolatedArg`] describes (async parse or - /// owned data only). - #[inline] - pub(crate) unsafe fn new(value: T) -> Self { - Self(value) - } -} - -// SAFETY: `new`'s contract is exactly what makes the value sendable. -unsafe impl Send for ThreadIsolated {} - -impl core::ops::Deref for ThreadIsolated { - type Target = T; - #[inline] - fn deref(&self) -> &T { - &self.0 - } -} - -impl core::ops::DerefMut for ThreadIsolated { - #[inline] - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - /// Parsed from JS it is `StringOrBuffer<'static>`; `Utf8` may instead borrow /// Rust-side bytes for a synchronous call ([`StringOrBuffer::borrowed`]). pub(crate) enum StringOrBuffer<'a> { diff --git a/src/runtime/webcore/Crypto.bind.ts b/src/runtime/webcore/Crypto.bind.ts new file mode 100644 index 000000000000..8a7fad628695 --- /dev/null +++ b/src/runtime/webcore/Crypto.bind.ts @@ -0,0 +1,19 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::webcore::crypto" }); + +/** What is not a string is not converted: it is "hex". */ +export const UUIDEncoding = b.union({ label: b.string.utf8, ignored: b.any }); + +export const randomUUIDv5 = b.fn({ + args: { + name: b.bytes({ from: ["string", "buffer"] }), + /** A string is a UUID or the name of one. A buffer is its 16 bytes. */ + namespace: b.bytes({ from: ["string", "buffer"], string: "asIs", held: "handle" }), + encoding: UUIDEncoding, + }, + returns: b.any, + capability: "heap", + throws: true, + rustPath: "crate::webcore::crypto::random_uuid_v5", +}); diff --git a/src/runtime/webcore/Crypto.rs b/src/runtime/webcore/Crypto.rs index 07315d7e89ea..1af94491e219 100644 --- a/src/runtime/webcore/Crypto.rs +++ b/src/runtime/webcore/Crypto.rs @@ -1,7 +1,9 @@ use bun_core::String as BunString; +use bun_jsc::bindgen::{self, JsHeap, JsLocal, StringOrJsBytes}; use bun_jsc::uuid::{self, UUID, UUID5, UUID7}; use bun_jsc::{CallFrame, JSGlobalObject, JSType, JSValue, JsClass, JsResult, StringJsc}; +use crate::generated_bindgen::crypto::UUIDEncoding; use crate::node::Encoding; // `.classes.ts`-backed type: the C++ JSCell wrapper stays generated C++. @@ -224,131 +226,66 @@ fn bun_random_uuid_v7(global: &JSGlobalObject, callframe: &CallFrame) -> JsResul encoding.encode_with_max_size(global, 32, &uuid.bytes) } -#[bun_jsc::host_fn(export = "Bun__randomUUIDv5")] -fn bun_random_uuid_v5(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let arguments = callframe.arguments_undef::<3>(); - - if arguments.len == 0 || arguments.ptr[0].is_undefined_or_null() { - return Err(global - .err( - bun_jsc::ErrorCode::INVALID_ARG_TYPE, - format_args!("The \"name\" argument must be specified"), +pub(crate) fn random_uuid_v5<'a>( + heap: &JsHeap<'a>, + name: &[u8], + namespace: StringOrJsBytes<'a>, + encoding: UUIDEncoding<'a>, +) -> bindgen::Result<'a, JsLocal<'a>> { + let encoding = match encoding { + UUIDEncoding::Label(label) => Encoding::from(label).ok_or_else(|| { + heap.error_with_code( + bun_jsc::ErrorCode::UNKNOWN_ENCODING, + format_args!("Encoding must be one of base64, base64url, hex, or buffer"), ) - .throw()); - } - - if arguments.len < 2 || arguments.ptr[1].is_undefined_or_null() { - return Err(global - .err( - bun_jsc::ErrorCode::INVALID_ARG_TYPE, - format_args!("The \"namespace\" argument must be specified"), - ) - .throw()); - } - - let encoding: Encoding = 'brk: { - if arguments.len > 2 && !arguments.ptr[2].is_undefined() { - if arguments.ptr[2].is_string() { - break 'brk match Encoding::from_js(arguments.ptr[2], global)? { - Some(e) => e, - None => { - return Err(global - .err( - bun_jsc::ErrorCode::UNKNOWN_ENCODING, - format_args!( - "Encoding must be one of base64, base64url, hex, or buffer" - ), - ) - .throw()); - } - }; - } - } - - break 'brk Encoding::Hex; + })?, + UUIDEncoding::Ignored(_) => Encoding::Hex, }; - - let name_value = arguments.ptr[0]; - let namespace_value = arguments.ptr[1]; - - let name_buffer; - let name: bun_core::Utf8Bytes = 'brk: { - if name_value.is_string() { - break 'brk name_value.to_utf8(global)?; - } else if let Some(array_buffer) = name_value.as_array_buffer(global) { - name_buffer = array_buffer; - break 'brk bun_core::Utf8Bytes::Borrowed(name_buffer.byte_slice()); - } else { - return Err(global - .err( - bun_jsc::ErrorCode::INVALID_ARG_TYPE, - format_args!("The \"name\" argument must be of type string or BufferSource"), - ) - .throw()); - } + let invalid_value = |message: &str| { + heap.error_with_code( + bun_jsc::ErrorCode::INVALID_ARG_VALUE, + format_args!("{message}"), + ) }; - let namespace: [u8; 16] = 'brk: { - if namespace_value.is_string() { - let namespace_str = namespace_value.to_bun_string(global)?; - let namespace_slice = namespace_str.to_utf8(); - - if namespace_slice.slice().len() != 36 { - if let Some(namespace) = uuid::namespaces::get(namespace_slice.slice()) { - break 'brk *namespace; - } - - return Err(global - .err( - bun_jsc::ErrorCode::INVALID_ARG_VALUE, - format_args!("Invalid UUID format for namespace"), - ) - .throw()); + let namespace: [u8; 16] = match namespace { + StringOrJsBytes::String(text) => { + let text = text.to_utf8(); + if text.len() == 36 { + UUID::parse(&text) + .map_err(|_| invalid_value("Invalid UUID format for namespace"))? + .bytes + } else { + *uuid::namespaces::get(&text) + .ok_or_else(|| invalid_value("Invalid UUID format for namespace"))? } - - let Ok(parsed_uuid) = UUID::parse(namespace_slice.slice()) else { - return Err(global - .err( - bun_jsc::ErrorCode::INVALID_ARG_VALUE, - format_args!("Invalid UUID format for namespace"), - ) - .throw()); - }; - break 'brk parsed_uuid.bytes; - } else if let Some(array_buffer) = namespace_value.as_array_buffer(global) { - let slice: &[u8] = array_buffer.byte_slice(); - if slice.len() != 16 { - return Err(global - .err( - bun_jsc::ErrorCode::INVALID_ARG_VALUE, - format_args!("Namespace must be exactly 16 bytes"), - ) - .throw()); - } - break 'brk <[u8; 16]>::try_from(&slice[0..16]).unwrap(); } - - return Err(global - .err( - bun_jsc::ErrorCode::INVALID_ARG_TYPE, - format_args!("The \"namespace\" argument must be a string or buffer"), - ) - .throw()); + StringOrJsBytes::Bytes(bytes) => bytes + .get(heap) + .try_into() + .map_err(|_| invalid_value("Namespace must be exactly 16 bytes"))?, }; - let uuid = UUID5::init(&namespace, name.slice()); + let uuid = UUID5::init(&namespace, name); - if encoding == Encoding::Hex { - let (str, bytes) = BunString::create_uninitialized_latin1(36); - uuid.print( - (&mut bytes[0..36]) - .try_into() - .expect("infallible: size matches"), - ); - return str.into_js(global); + match encoding { + Encoding::Hex => { + let (str, bytes) = BunString::create_uninitialized_latin1(36); + uuid.print( + (&mut bytes[0..36]) + .try_into() + .expect("infallible: size matches"), + ); + bindgen::to_js::string(heap, str) + } + Encoding::Buffer => Ok(heap + .create_buffer_from_box(Box::from(uuid.bytes))? + .as_value()), + encoding => bindgen::to_js::string( + heap, + crate::webcore::encoding::to_bun_string(&uuid.bytes, encoding), + ), } - - encoding.encode_with_max_size(global, 32, &uuid.bytes) } #[unsafe(no_mangle)] diff --git a/src/sql_jsc/jsc.rs b/src/sql_jsc/jsc.rs index b30a81b1f93d..11c345289de0 100644 --- a/src/sql_jsc/jsc.rs +++ b/src/sql_jsc/jsc.rs @@ -817,7 +817,7 @@ unsafe extern "C" { ) -> JSValue; } -/// `bun_jsc::JSValue::put_host_functions`-shaped helper for the SQL binding +/// `bun_jsc::create_host_function_object`-shaped helper for the SQL binding /// objects. Macro (not fn) because each entry's `$f` is a *distinct* fn-item /// ZST routed through [`IntoJSHostFn`] — a `&[(&str, JSHostFn, u32)]` slice /// can't hold heterogeneous safe-Rust signatures. Expands to the same diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index dac16ff3e446..b650d3216cca 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -96,12 +96,63 @@ itAlways("mistakes in a schema are errors when it loads", () => { ); expect(() => b.namespace({ "not-a-name": {} })).toThrow("not-a-name: a function is named by an identifier"); expect(() => b.array(b.null)).toThrow("an array of nothing but null"); - expect(() => b.bufferSource.bytes.allowShared.optional).toThrow( - ".allowShared is for an argument that is always there", + const stringOrBuffer = b.bytes({ from: ["string", "buffer"] }); + expect(() => b.array(stringOrBuffer)).toThrow("the bytes of a buffer are only for an argument"); + expect(() => b.dictionary({ bytes: stringOrBuffer })).toThrow("the bytes of a buffer are only for an argument"); + expect(() => b.union({ bytes: stringOrBuffer })).toThrow("the bytes of a buffer are only for an argument"); + for (const held of ["handle", "copied", "pinned"] as const) { + const bytes = b.bytes({ from: ["string", "buffer"], held }); + expect(() => b.array(bytes)).toThrow("b.bytes is only for an argument"); + expect(() => b.dictionary({ bytes })).toThrow("bytes: b.bytes is only for an argument"); + expect(() => b.union({ bytes })).toThrow("bytes: b.bytes is only for an argument"); + } + for (const absent of ["optional", "nullable", "falsyAsNull"] as const) { + expect(() => stringOrBuffer[absent]).toThrow("b.bytes is for an argument that is always there"); + } + expect(() => stringOrBuffer.default("")).toThrow("cannot have a default"); + expect(() => b.fn({ args: { data: stringOrBuffer }, capability: "js" })).toThrow( + 'data: JavaScript can detach a buffer; held: "handle" borrows the bytes each time they are read', + ); + expect(() => b.bytes({ from: [] })).toThrow("from: it takes nothing"); + expect(() => b.bytes({ from: ["string", "string"] })).toThrow("two kinds are both string"); + expect(() => b.bytes({ from: ["string"], refuse: ["null"] })).toThrow( + 'refuse: without otherwise: "toString" they throw already', ); - expect(() => b.fn({ args: { bytes: b.bufferSource.bytes.allowShared }, capability: "js" })).toThrow( - "bytes: JavaScript can detach a buffer; .allowShared is for a function that cannot run it", + expect(() => b.bytes({ from: ["buffer"], string: "asIs", held: "handle" })).toThrow("string: it takes no string"); + expect(() => b.bytes({ from: ["string"], shared: "copied" })).toThrow("shared: it takes no buffer"); + expect(() => b.bytes({ from: ["buffer"], shared: "copied", held: "copied" })).toThrow( + 'held: "copied" copies every buffer', + ); + for (const held of ["borrowed", "copied", "pinned"] as const) { + expect(() => b.bytes({ from: ["string"], string: "asIs", held })).toThrow( + 'string: "asIs" is only with held: "handle"', + ); + } + expect(() => b.bytes({ from: ["string", "buffer"], string: "asIs", shared: "copied", held: "handle" })).toThrow( + 'string: "asIs" does not copy a shared buffer', ); + expect(() => b.bytes({ from: ["buffer"], shared: "copied", held: "pinned" })).toThrow( + 'held: "pinned" pins a shared buffer; it does not copy one', + ); + expect(() => b.bytes({ from: ["blob"], held: "pinned" })).toThrow('held: "pinned" does not take a Blob'); + const pinned = b.bytes({ from: ["string", "buffer"], held: "pinned" }); + expect(() => b.fn({ args: { data: pinned, more: pinned } })).toThrow( + 'held: "pinned": if one fails, nothing releases the string of the next', + ); + expect(() => b.fn({ args: { data: stringOrBuffer, more: pinned } })).toThrow( + 'held: "pinned" moves the bytes of a small typed array, which another argument that is held: "borrowed" can be', + ); + expect(() => b.u32.asBigInt).toThrow(".asBigInt follows b.u64"); + expect(() => b.u64.allowBigInt.asBigInt).toThrow(".asBigInt follows b.u64"); + for (const value of [ + b.u64.asBigInt, + b.u64.asBigInt.optional, + b.array(b.u64.asBigInt), + b.dictionary({ member: b.array(b.u64.asBigInt.nullable) }), + ]) { + expect(() => b.fn({ args: { value } })).toThrow("value: .asBigInt is a result; an argument is b.u64.allowBigInt"); + } + expect(() => b.namespace({ f: {} }, { call: "g" })).toThrow("call: g is not one of its functions"); for (const element of [b.any, b.bufferSource, b.dictionary({ value: b.any }), b.class({ rustPath: "T" })]) { expect(() => b.array(element)).toThrow("an array is a Vec, where the GC does not see JS values"); } @@ -901,28 +952,19 @@ describe("buffers", () => { expect(bindgen.sumBytes(fixed)).toBe(0); }); - it("rejects shared memory and everything else", () => { - for (const value of [new SharedArrayBuffer(4), new Uint8Array(new SharedArrayBuffer(4)), [1], "s", null, {}]) { - expect(() => bindgen.sumBytes(value)).toThrow( - "bytes must be an ArrayBuffer or ArrayBufferView that is not shared", - ); + it("rejects everything else, and b.bufferSource shared memory too", () => { + const shared = new Uint8Array(new SharedArrayBuffer(4)).fill(1); + for (const value of [shared.buffer, shared]) expect(bindgen.sumBytes(value)).toBe(4); + for (const value of [[1], "s", new String("s"), null, {}]) { + expect(() => bindgen.sumBytes(value)).toThrow("bytes must be an ArrayBuffer or ArrayBufferView"); + } + for (const value of [shared.buffer, shared, [1], "s", null, {}]) { expect(() => bindgen.sumBeforeAndAfterCallback(value, () => {})).toThrow( "buffer must be an ArrayBuffer or ArrayBufferView that is not shared", ); } }); - it("tells an absent buffer from an empty one", () => { - expect(bindgen.sumBytesOptional(null)).toBe(null); - expect(bindgen.sumBytesOptional(undefined)).toBe(null); - expect(bindgen.sumBytesOptional()).toBe(null); - expect(bindgen.sumBytesOptional(new Uint8Array(0))).toBe(0); - expect(bindgen.sumBytesOptional(new Uint8Array([1, 2]))).toBe(3); - const detached = new Uint8Array([1, 2]); - detached.buffer.transfer(); - expect(bindgen.sumBytesOptional(detached)).toBe(0); - }); - it("reads the bytes after every other argument is converted", () => { const transferred = new Uint8Array([1, 2, 3]); const transfers = { toString: () => (transferred.buffer.transfer(), "\x01") }; @@ -934,7 +976,7 @@ describe("buffers", () => { expect(bindgen.sumBytesThenText(view, shrinks)).toBe(2); expect(() => bindgen.sumBytesThenText("no", { toString: () => expect.unreachable() })).toThrow( - "bytes must be an ArrayBuffer or ArrayBufferView that is not shared", + "bytes must be an ArrayBuffer or ArrayBufferView", ); }); @@ -959,13 +1001,354 @@ describe("buffers", () => { }); }); -describe("withValue arguments", () => { - it("hands back the very value that was converted", () => { - const boxed = new String("boxed"); - const custom = { toString: () => "custom" }; - for (const value of ["plain", boxed, custom, 5]) expect(bindgen.withValueEcho(value)).toBe(value); - expect(bindgen.withValueEcho("")).toBe(undefined); +describe("a string or a buffer, as bytes", () => { + const utf8 = (text: string) => [...new TextEncoder().encode(text)]; + const buffers = (bytes: number[], ArrayBufferClass: typeof ArrayBuffer | typeof SharedArrayBuffer = ArrayBuffer) => { + const padded = new Uint8Array(new ArrayBufferClass(bytes.length + 16)); + padded.set(bytes, 8); + const whole = new Uint8Array(new ArrayBufferClass(bytes.length)); + whole.set(bytes); + return [ + whole.buffer, + whole, + new Uint8Array(padded.buffer, 8, bytes.length), + new DataView(padded.buffer, 8, bytes.length), + new Int8Array(padded.buffer, 8, bytes.length), + new Uint16Array(padded.buffer, 8, bytes.length / 2), + new Float64Array(padded.buffer, 8, bytes.length / 8), + new BigUint64Array(padded.buffer, 8, bytes.length / 8), + ]; + }; + const eight = [1, 2, 3, 4, 250, 251, 252, 253]; + const notBytes = [ + undefined, + null, + 1, + 1n, + true, + {}, + [], + [1], + () => {}, + Symbol("s"), + new Date(0), + { toString: () => "x" }, + ]; + + const refusal = (what: string) => + expect.objectContaining({ name: "TypeError", code: "ERR_INVALID_ARG_TYPE", message: `data must be ${what}` }); + const fromStringAndBuffer = [bindgen.bytesOf, bindgen.bytesOfOrToString, bindgen.bytesOfRefusingUndefined]; + + it("takes a string or a String object as UTF-8", () => { + class Derived extends String { + toString() { + return "derived"; + } + } + for (const f of fromStringAndBuffer) { + for (const text of [ + "", + "abc", + "h\xe9llo", + "\u65e5\u672c", + "\u{1f600}", + Buffer.alloc(300, "a").toString(), + Buffer.alloc(300, 0xe9).toString("latin1"), + "a\0b", + ]) { + expect(f(text)).toEqual(utf8(text)); + expect(f(new String(text))).toEqual(utf8(text)); + expect(f(text + text.slice(0, 0) + "!")).toEqual(utf8(text + "!")); + } + expect(f("\ud800")).toEqual([0xef, 0xbf, 0xbd]); + expect(f(new Derived("x"))).toEqual(utf8("derived")); + } + }); + + it("takes the bytes that a buffer or a view shows, shared or not", () => { + for (const f of fromStringAndBuffer) { + for (const ArrayBufferClass of [ArrayBuffer, SharedArrayBuffer]) { + for (const buffer of buffers(eight, ArrayBufferClass)) expect(f(buffer)).toEqual(eight); + for (const buffer of buffers([], ArrayBufferClass)) expect(f(buffer)).toEqual([]); + } + expect(f(Buffer.from(eight))).toEqual(eight); + const detached = new Uint8Array(eight); + detached.buffer.transfer(); + expect(f(detached)).toEqual([]); + expect(f(detached.buffer)).toEqual([]); + const resizable = new ArrayBuffer(8, { maxByteLength: 16 }); + const tracking = new Uint8Array(resizable); + const fixed = new Uint8Array(resizable, 4, 4); + tracking.set(eight); + expect(f(tracking)).toEqual(eight); + resizable.resize(6); + expect(f(tracking)).toEqual(eight.slice(0, 6)); + expect(f(fixed)).toEqual([]); + const growable = new SharedArrayBuffer(4, { maxByteLength: 8 }); + new Uint8Array(growable).set([9, 8, 7, 6]); + expect(f(growable)).toEqual([9, 8, 7, 6]); + growable.grow(6); + expect(f(new Uint8Array(growable))).toEqual([9, 8, 7, 6, 0, 0]); + } + }); + + it('from: "blob" takes the bytes of a Blob, but not of one that is a file', () => { + const s3 = new Bun.S3Client({ accessKeyId: "id", secretAccessKey: "secret", bucket: "bucket" }); + for (const f of [bindgen.bytesOfBufferOrBlob, bindgen.bytesOfOrToString]) { + expect(f(new Blob([new Uint8Array(eight)]))).toEqual(eight); + expect(f(new Blob(["ab", "c"]))).toEqual(utf8("abc")); + expect(f(new Blob([new Uint8Array(eight)]).slice(2, 5))).toEqual(eight.slice(2, 5)); + expect(f(new Blob([]))).toEqual([]); + expect(f(new File(["abc"], "name.txt"))).toEqual(utf8("abc")); + for (const file of [Bun.file(import.meta.path), s3.file("key")]) { + expect(() => f(file)).toThrow( + expect.objectContaining({ + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + message: "File blob cannot be used here", + }), + ); + } + } + expect(bindgen.bytesOfRefusingUndefined(new Blob(["abc"]))).toEqual(utf8("[object Blob]")); + }); + + it("from: takes only what it lists, without converting anything else", () => { + for (const value of [...notBytes, new Blob(["abc"])]) { + expect(() => bindgen.bytesOf(value)).toThrow(refusal("a string, ArrayBuffer or ArrayBufferView")); + } + const bytes = new Uint8Array(eight); + const blob = new Blob([bytes]); + expect(bindgen.bytesOfString("abc")).toEqual(utf8("abc")); + expect(bindgen.bytesOfString(new String("abc"))).toEqual(utf8("abc")); + for (const value of [...notBytes, bytes, bytes.buffer, blob]) { + expect(() => bindgen.bytesOfString(value)).toThrow(refusal("a string")); + } + expect(bindgen.bytesOfBufferOrBlob(bytes)).toEqual(eight); + expect(bindgen.bytesOfBufferOrBlob(bytes.buffer)).toEqual(eight); + expect(bindgen.bytesOfBufferOrBlob(blob)).toEqual(eight); + for (const value of [...notBytes, "abc", new String("abc")]) { + expect(() => bindgen.bytesOfBufferOrBlob(value)).toThrow(refusal("an ArrayBuffer, ArrayBufferView or Blob")); + } + }); + + it("refuse: throws for those, and converts the other", () => { + const f = bindgen.bytesOfRefusingUndefined; + expect(() => f(undefined)).toThrow(refusal("a string, ArrayBuffer or ArrayBufferView")); + expect(() => f()).toThrow(refusal("a string, ArrayBuffer or ArrayBufferView")); + expect(f(null)).toEqual(utf8("null")); + expect(f(5)).toEqual(utf8("5")); }); + + it('otherwise: "toString" is ToString of anything else', () => { + const f = bindgen.bytesOfOrToString; + for (const value of notBytes) { + if (typeof value === "symbol") continue; + expect(f(value)).toEqual(utf8(String(value))); + } + expect(() => f(Symbol("s"))).toThrow( + expect.objectContaining({ name: "TypeError", message: "Cannot convert a symbol to a string" }), + ); + expect(() => + f({ + toString() { + throw new RangeError("from toString"); + }, + }), + ).toThrow(new RangeError("from toString")); + }); + + it.each(["bytesOfThen", "bytesOfThenCopyingShared", "bytesOfThenCopied"] as const)( + "%s reads a buffer after every argument is converted", + name => { + const then = (buffer: ArrayBufferView | ArrayBufferLike, during: () => void) => + bindgen[name](buffer, { + valueOf() { + during(); + return 0; + }, + }); + const written = new Uint8Array(eight); + expect(then(written, () => written.fill(7))).toEqual(new Array(8).fill(7)); + const detached = new Uint8Array(eight); + expect(then(detached, () => void detached.buffer.transfer())).toEqual([]); + const collected = new Uint8Array(eight); + expect( + then(collected, () => { + structuredClone(collected.buffer, { transfer: [collected.buffer] }); + Bun.gc(true); + }), + ).toEqual([]); + const resizable = new ArrayBuffer(8, { maxByteLength: 4096 }); + new Uint8Array(resizable).set(eight); + expect(then(new Uint8Array(resizable), () => resizable.resize(2))).toEqual([1, 2]); + expect(then(resizable, () => resizable.resize(4))).toEqual([1, 2, 0, 0]); + const growable = new SharedArrayBuffer(2, { maxByteLength: 4096 }); + expect(then(growable, () => growable.grow(4))).toEqual([0, 0, 0, 0]); + // A string is converted in its turn: what it is cannot change. + const log: string[] = []; + const text = Object.assign(new String("x"), { + toString() { + log.push("data"); + return "text"; + }, + }); + expect(then(text as any, () => log.push("after"))).toEqual(utf8("text")); + expect(log).toEqual(["data", "after"]); + expect(() => bindgen[name](1, { valueOf: () => log.push("not reached") })).toThrow( + refusal("a string, ArrayBuffer, ArrayBufferView or Blob"), + ); + expect(log).toEqual(["data", "after"]); + }, + ); + + it("copies only a string that is not ASCII", () => { + const ownMemory = bindgen.bytesOfAreSameMemory; + for (const buffer of buffers(eight)) expect(ownMemory(buffer, buffer)).toBe(true); + for (const buffer of buffers(eight, SharedArrayBuffer)) expect(ownMemory(buffer, buffer)).toBe(true); + const blob = new Blob([new Uint8Array(eight)]); + expect(ownMemory(blob, blob)).toBe(true); + // The characters of a string that is ASCII are its UTF-8. + expect(ownMemory("ascii", "ascii")).toBe(true); + expect(ownMemory("\xe9", "\xe9")).toBe(false); + }); + + it('shared: "copied" copies a shared buffer, and nothing else', () => { + const ownMemory = bindgen.bytesOfAreSameMemoryCopyingShared; + for (const buffer of buffers(eight)) expect(ownMemory(buffer, buffer)).toBe(true); + for (const buffer of buffers(eight, SharedArrayBuffer)) expect(ownMemory(buffer, buffer)).toBe(false); + const blob = new Blob([new Uint8Array(eight)]); + expect(ownMemory(blob, blob)).toBe(true); + expect(ownMemory("ascii", "ascii")).toBe(true); + for (const buffer of buffers(eight, SharedArrayBuffer)) + expect(bindgen.bytesOfThenCopyingShared(buffer, 0)).toEqual(eight); + for (const buffer of buffers([], SharedArrayBuffer)) + expect(bindgen.bytesOfThenCopyingShared(buffer, 0)).toEqual([]); + }); + + describe('held: "handle", in a function that can run JavaScript', () => { + it.each(["bytesBeforeAndAfterCallback", "bytesBeforeAndAfterCallbackCopyingShared"] as const)( + "%s reads a buffer each time", + name => { + const f = bindgen[name]; + const same = new Uint8Array(eight); + expect(f(same, () => {})).toEqual([eight, eight]); + expect(f(same, () => void same.fill(2))).toEqual([eight, new Array(8).fill(2)]); + const transferred = new Uint8Array(eight); + expect(f(transferred, () => void transferred.buffer.transfer())).toEqual([eight, []]); + const collected = new Uint8Array(eight); + expect( + f(collected, () => { + structuredClone(collected.buffer, { transfer: [collected.buffer] }); + Bun.gc(true); + }), + ).toEqual([eight, []]); + const resizable = new ArrayBuffer(8, { maxByteLength: 1 << 20 }); + new Uint8Array(resizable).set(eight); + expect(f(resizable, () => resizable.resize(2))).toEqual([eight, [1, 2]]); + expect(f(new Uint8Array(resizable), () => resizable.resize(4))).toEqual([ + [1, 2], + [1, 2, 0, 0], + ]); + expect(f(new Blob([new Uint8Array(eight)]), () => Bun.gc(true))).toEqual([eight, eight]); + for (const text of ["", "abc", "h\xe9llo", "\u65e5\u672c", Buffer.alloc(300, 0xe9).toString("latin1")]) { + expect(f(text, () => Bun.gc(true))).toEqual([utf8(text), utf8(text)]); + } + expect(f(5, () => {})).toEqual([utf8("5"), utf8("5")]); + expect(f({ toString: () => "made " + "here" }, () => Bun.gc(true))).toEqual([ + utf8("made here"), + utf8("made here"), + ]); + }, + ); + + it('a shared buffer is read each time, or copied once with shared: "copied"', () => { + const shared = new Uint8Array(new SharedArrayBuffer(8)); + shared.set(eight); + expect(bindgen.bytesBeforeAndAfterCallback(shared, () => void shared.fill(2))).toEqual([ + eight, + new Array(8).fill(2), + ]); + shared.set(eight); + expect(bindgen.bytesBeforeAndAfterCallbackCopyingShared(shared, () => void shared.fill(2))).toEqual([ + eight, + eight, + ]); + }); + }); + + it('held: "copied" is a copy of the bytes', () => { + const copied = (value: unknown) => bindgen.bytesOfThenCopied(value, 0); + for (const ArrayBufferClass of [ArrayBuffer, SharedArrayBuffer]) { + for (const buffer of buffers(eight, ArrayBufferClass)) expect(copied(buffer)).toEqual(eight); + for (const buffer of buffers([], ArrayBufferClass)) expect(copied(buffer)).toEqual([]); + } + expect(copied(new Blob([new Uint8Array(eight)]))).toEqual(eight); + for (const text of ["", "abc", "h\xe9llo", "\u65e5\u672c", "\ud800", Buffer.alloc(300, 0xe9).toString("latin1")]) { + expect(copied(text)).toEqual(utf8(text)); + expect(copied(new String(text))).toEqual(utf8(text)); + } + expect(() => copied(5)).toThrow(refusal("a string, ArrayBuffer, ArrayBufferView or Blob")); + }); + + it('held: "pinned" is read by another thread, after JavaScript ran', () => { + const pinned = bindgen.pinnedBytesAfterCallback; + for (const text of ["", "abc", "h\xe9llo", "\u65e5\u672c", Buffer.alloc(300, 0xe9).toString("latin1")]) { + expect(pinned(text, () => Bun.gc(true))).toEqual(utf8(text)); + expect(pinned(new String(text), () => Bun.gc(true))).toEqual(utf8(text)); + } + for (const ArrayBufferClass of [ArrayBuffer, SharedArrayBuffer]) { + for (const buffer of buffers(eight, ArrayBufferClass)) expect(pinned(buffer, () => {})).toEqual(eight); + for (const buffer of buffers([], ArrayBufferClass)) expect(pinned(buffer, () => {})).toEqual([]); + } + // In place: a write is seen. + const written = new Uint8Array(eight); + expect(pinned(written, () => void written.fill(2))).toEqual(new Array(8).fill(2)); + const kept = new Uint8Array(eight); + expect( + pinned(kept, () => { + // What is pinned is copied and not detached. + kept.buffer.transfer(); + expect(kept.byteLength).toBe(8); + }), + ).toEqual(eight); + kept.buffer.transfer(); + expect(kept.byteLength).toBe(0); + // A shrink unmaps pages, so a resizable buffer is copied. + const resizable = new ArrayBuffer(8, { maxByteLength: 1 << 20 }); + new Uint8Array(resizable).set(eight); + expect(pinned(resizable, () => resizable.resize(0))).toEqual(eight); + for (const value of [...notBytes, new Blob(["abc"])]) { + expect(() => pinned(value, () => {})).toThrow(refusal("a string, ArrayBuffer or ArrayBufferView")); + } + }); + + it('string: "asIs" leaves a string in its own encoding', () => { + const f = bindgen.stringAsIsOrBytes; + expect(f("")).toEqual([1]); + expect(f("abc")).toEqual([1, 0x61, 0x62, 0x63]); + expect(f("h\xe9")).toEqual([1, 0x68, 0xe9]); + expect(f("\u65e5")).toEqual([2, 0xe5, 0x65]); + expect(f("\ud800")).toEqual([2, 0x00, 0xd8]); + expect(f(new String("h\xe9"))).toEqual([1, 0x68, 0xe9]); + expect(f(Buffer.alloc(20, "a").toString() + Buffer.alloc(20, 0xe9).toString("latin1"))).toEqual([ + 1, + ...new Array(20).fill(0x61), + ...new Array(20).fill(0xe9), + ]); + expect(f(5)).toEqual([1, 0x35]); + expect(f(undefined)).toEqual([1, ...utf8("undefined")]); + for (const ArrayBufferClass of [ArrayBuffer, SharedArrayBuffer]) { + for (const buffer of buffers(eight, ArrayBufferClass)) expect(f(buffer)).toEqual([0, ...eight]); + for (const buffer of buffers([], ArrayBufferClass)) expect(f(buffer)).toEqual([0]); + } + expect(f(new Blob([new Uint8Array(eight)]))).toEqual([0, ...eight]); + }); +}); + +it("b.u64.asBigInt returns a BigInt, whatever the value", () => { + for (const value of [0n, 1n, 2n ** 31n, 2n ** 53n + 1n, 2n ** 64n - 1n]) expect(bindgen.asBigInt(value)).toBe(value); + expect(bindgen.asBigInt(5)).toBe(5n); }); describe("errors", () => { @@ -973,7 +1356,13 @@ describe("errors", () => { expect(() => bindgen.fail("type")).toThrow(new TypeError("a type error")); expect(() => bindgen.fail("range")).toThrow(new RangeError("a range error")); expect(() => bindgen.fail("plain")).toThrow(new Error("an error")); + expect(() => bindgen.fail("code")).toThrow( + expect.objectContaining({ name: "TypeError", code: "ERR_INVALID_ARG_VALUE", message: "an error with a code" }), + ); expect(() => bindgen.fail("syscall")).toThrow(expect.objectContaining({ code: "ENOENT", syscall: "open" })); + expect(() => bindgen.fail("outOfMemory")).toThrow( + expect.objectContaining({ name: "RangeError", message: "Out of memory" }), + ); const value = { thrown: true }; let caught; try { diff --git a/test/internal/source-lints/vm-thread-door.inventory.json b/test/internal/source-lints/vm-thread-door.inventory.json index 3c9b10ff322f..67dd0ee979a6 100644 --- a/test/internal/source-lints/vm-thread-door.inventory.json +++ b/test/internal/source-lints/vm-thread-door.inventory.json @@ -53,6 +53,11 @@ "WatchChangedPaths" ] }, + "src/jsc/thread_isolated.rs": { + "unsafe impl Send": [ + "ThreadIsolated" + ] + }, "src/jsc/web_worker.rs": { "thread spawn": 1, "unsafe impl Send": [ @@ -184,11 +189,6 @@ "PathWatcherManager" ] }, - "src/runtime/node/types.rs": { - "unsafe impl Send": [ - "ThreadIsolated" - ] - }, "src/runtime/shell/IOReader.rs": { "unsafe impl Send": [ "IOReader" diff --git a/test/js/bun/jsonc/jsonc.test.ts b/test/js/bun/jsonc/jsonc.test.ts index 8c50c5739de5..38a9b48a9c10 100644 --- a/test/js/bun/jsonc/jsonc.test.ts +++ b/test/js/bun/jsonc/jsonc.test.ts @@ -154,9 +154,9 @@ test("Bun.JSONC.parse throws a SyntaxError on invalid input", () => { }); test("Bun.JSONC.parse throws on undefined and null input", () => { - expect(() => Bun.JSONC.parse(undefined as any)).toThrow("Expected a string to parse"); - expect(() => (Bun.JSONC.parse as any)()).toThrow("Expected a string to parse"); - expect(() => Bun.JSONC.parse(null as any)).toThrow("Expected a string to parse"); + expect(() => Bun.JSONC.parse(undefined as any)).toThrow("input must be a string"); + expect(() => (Bun.JSONC.parse as any)()).toThrow("input must be a string"); + expect(() => Bun.JSONC.parse(null as any)).toThrow("input must be a string"); }); test("Bun.JSONC.parse stringifies non-string input like JSON.parse does", () => { diff --git a/test/js/bun/toml/toml.test.ts b/test/js/bun/toml/toml.test.ts index 43de97d2b4d2..272a3ce40310 100644 --- a/test/js/bun/toml/toml.test.ts +++ b/test/js/bun/toml/toml.test.ts @@ -51,6 +51,13 @@ describe("input types", () => { expect(TOML.parse(sab)).toEqual(expected); }); + test("a Blob whose bytes are not in memory is refused, and does not parse as nothing", () => { + const s3 = new Bun.S3Client({ accessKeyId: "id", secretAccessKey: "secret", bucket: "bucket" }); + for (const file of [Bun.file(import.meta.path), s3.file("key")]) { + expect(() => TOML.parse(file)).toThrow("File blob cannot be used here"); + } + }); + test("Blob parses synchronously", () => { expect(TOML.parse(new Blob([doc]))).toEqual(expected); }); @@ -62,6 +69,40 @@ describe("input types", () => { expect(TOML.parse(new DataView(sab))).toEqual(expected); }); + test("parses a SharedArrayBuffer that another thread is writing", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + /* js */ ` + const { Worker } = require("node:worker_threads"); + const bytes = new Uint8Array(new SharedArrayBuffer(1 << 14)).fill("\\n".charCodeAt(0)); + const writer = new Worker( + \`const { workerData: bytes, parentPort } = require("node:worker_threads"); + parentPort.postMessage("ready"); + // Each pass over the buffer flips every byte, so that a byte the parser reads twice differs. + for (let i = 0; ; i++) bytes[i % bytes.length] = (Math.floor(i / bytes.length) % 2 ? "#" : "\\\\n").charCodeAt(0);\`, + { eval: true, workerData: bytes }, + ); + writer.once("message", () => { + for (let i = 0; i < 4000; i++) { + try { + Bun.TOML.parse(bytes); + } catch {} + } + console.log("done"); + process.exit(0); + }); + `, + ], + env: bunEnv, + stderr: "inherit", + }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + expect(stdout).toBe("done\n"); + expect(exitCode).toBe(0); + }); + test("non-string values are coerced via toString", () => { expect(TOML.parse({ toString: () => "a = 1" } as any)).toEqual({ a: 1 }); // A number coerces to a string that is not valid TOML. diff --git a/test/js/bun/util/hash.test.js b/test/js/bun/util/hash.test.js index 619e54ec897b..a2039299e3ec 100644 --- a/test/js/bun/util/hash.test.js +++ b/test/js/bun/util/hash.test.js @@ -247,6 +247,85 @@ describe("xxHash32 / xxHash64 reference vectors", () => { }); }); +describe("what Bun.hash and its algorithms take", () => { + const functions = [Bun.hash, ...Object.values(Bun.hash)]; + const bytes = new TextEncoder().encode("hello world!"); + + it("there are thirteen functions, and Bun.hash is wyhash", () => { + expect(functions).toHaveLength(13); + expect(Bun.hash).toBeFunction(); + expect(Bun.hash.name).toBe("hash"); + for (const hash of functions) expect(hash.length).toBe(1); + expect(Bun.hash("a", 5n)).toBe(Bun.hash.wyhash("a", 5n)); + }); + + it("the same bytes hash the same, whatever holds them", () => { + const shared = new Uint8Array(new SharedArrayBuffer(bytes.length)); + shared.set(bytes); + const padded = new Uint8Array(bytes.length + 8); + padded.set(bytes, 4); + for (const hash of functions) { + const expected = hash("hello world!"); + for (const data of [ + bytes, + bytes.buffer, + Buffer.from(bytes), + new DataView(bytes.buffer), + new Uint32Array(bytes.buffer), + padded.subarray(4, 4 + bytes.length), + shared, + shared.buffer, + new Blob([bytes]), + new String("hello world!"), + { toString: () => "hello world!" }, + ]) { + expect(hash(data)).toBe(expected); + } + } + }); + + it("a Blob that is a file is refused, and does not hash as nothing", () => { + const s3 = new Bun.S3Client({ accessKeyId: "id", secretAccessKey: "secret", bucket: "bucket" }); + for (const hash of functions) { + expect(() => hash(Bun.file(import.meta.path))).toThrow("File blob cannot be used here"); + expect(() => hash(s3.file("key"))).toThrow("File blob cannot be used here"); + } + }); + + it("nothing to hash is refused, and does not hash as its name", () => { + const refusal = expect.objectContaining({ + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + message: "data must be a string, ArrayBuffer, ArrayBufferView or Blob", + }); + for (const hash of functions) { + expect(() => hash()).toThrow(refusal); + expect(() => hash(undefined)).toThrow(refusal); + expect(() => hash(null, 1)).toThrow(refusal); + } + }); + + it("anything else is converted to a string", () => { + for (const hash of functions) { + for (const value of [0, 5, NaN, false, 10n, [1, 2], {}]) expect(hash(value)).toBe(hash(String(value))); + } + }); + + it("a seed that is neither a number nor a BigInt is not converted", () => { + const seeded = functions.filter(hash => hash("a", 1) !== hash("a")); + expect(seeded).toHaveLength(11); + const unconverted = { + valueOf() { + throw new Error("the seed was converted"); + }, + }; + for (const hash of functions) { + for (const seed of [undefined, null, "1", true, [1], unconverted]) expect(hash("a", seed)).toBe(hash("a", 0)); + } + for (const hash of seeded) expect(hash("a", 1n)).toBe(hash("a", 1)); + }); +}); + it("does not crash when changing Int32Array constructor with Bun.hash.xxHash32 as species", () => { const arr = new Int32Array(); function foo(a4) { diff --git a/test/js/bun/util/password.test.ts b/test/js/bun/util/password.test.ts index 2d9a01c55288..a38c2a83619f 100644 --- a/test/js/bun/util/password.test.ts +++ b/test/js/bun/util/password.test.ts @@ -73,7 +73,11 @@ describe("hash", () => { test("password is required", () => { // @ts-expect-error - expect(() => hash()).toThrow(); + expect(() => hash()).toThrow(expect.objectContaining({ name: "TypeError", code: "ERR_INVALID_ARG_TYPE" })); + // @ts-expect-error + expect(() => hash(undefined)).toThrow("password must be a string"); + // @ts-expect-error + expect(() => hash(undefined, "bcrypt")).toThrow("password must be a string"); }); test("invalid algorithm throws", () => { @@ -241,10 +245,15 @@ describe("verify", () => { describe("arguments parsing", () => { for (let verify of [password.verify, password.verifySync]) { test("minimum args", () => { + const invalidArgument = expect.objectContaining({ name: "TypeError", code: "ERR_INVALID_ARG_TYPE" }); + // @ts-expect-error + expect(() => verify()).toThrow(invalidArgument); + // @ts-expect-error + expect(() => verify("")).toThrow(invalidArgument); // @ts-expect-error - expect(() => verify()).toThrow(); + expect(() => verify(undefined, "$")).toThrow("password must be a string"); // @ts-expect-error - expect(() => verify("")).toThrow(); + expect(() => verify("$", undefined)).toThrow("hash must be a string"); }); test("empty values return false", async () => { diff --git a/test/js/bun/util/randomUUIDv5.test.ts b/test/js/bun/util/randomUUIDv5.test.ts index 769a98399786..70ee7e0a2b75 100644 --- a/test/js/bun/util/randomUUIDv5.test.ts +++ b/test/js/bun/util/randomUUIDv5.test.ts @@ -367,6 +367,35 @@ describe("randomUUIDv5", () => { expect(result).toEqual(uuid.v5("test", uuid.v5.DNS)); }); + test("a buffer is read once every argument is converted", () => { + const detaching = (buffer: Uint8Array, text: string) => + Object.assign(new String(), { + toString() { + buffer.buffer.transfer(); + return text; + }, + }) as unknown as string; + + const name = new TextEncoder().encode("www.example.com"); + expect(Bun.randomUUIDv5(name, detaching(name, "dns"))).toBe(Bun.randomUUIDv5("", "dns")); + + const namespace = new Uint8Array(16); + expect(() => Bun.randomUUIDv5("a", namespace, detaching(namespace, "hex") as "hex")).toThrow( + "Namespace must be exactly 16 bytes", + ); + }); + + test("an encoding that is not a string is not converted", () => { + const unconverted = { + toString() { + throw new Error("the encoding was converted"); + }, + }; + for (const encoding of [undefined, null, 5, {}, unconverted]) { + expect(Bun.randomUUIDv5("test", "dns", encoding as any)).toBe(uuid.v5("test", uuid.v5.DNS)); + } + }); + test("consistent across multiple calls", () => { const results: string[] = []; for (let i = 0; i < 100; i++) { diff --git a/test/js/bun/util/zstd.test.ts b/test/js/bun/util/zstd.test.ts index 210f58880006..880b3bea894b 100644 --- a/test/js/bun/util/zstd.test.ts +++ b/test/js/bun/util/zstd.test.ts @@ -581,14 +581,14 @@ describe("sync compression argument handling", () => { expect(zstdDecompressSync(compressed).byteLength).toBe(0); }); - it("zstdCompressSync evaluates the options object before validating the input", () => { + it("zstdCompressSync refuses an input that is not bytes before it evaluates the options object", () => { expect(() => zstdCompressSync(42 as any, { get level(): never { throw new Error("level option was read"); }, }), - ).toThrow("level option was read"); + ).toThrow("data must be a string, ArrayBuffer or ArrayBufferView"); }); it("gzipSync evaluates the options object before capturing the input", () => { @@ -613,24 +613,117 @@ describe("sync compression argument handling", () => { expect(inflateSync(compressed).byteLength).toBe(0); }); - it("gunzipSync evaluates the options object before validating the input", () => { + it("gunzipSync refuses an input that is not bytes before it evaluates the options object", () => { expect(() => gunzipSync(42 as any, { get windowBits(): never { throw new Error("windowBits option was read"); }, }), - ).toThrow("windowBits option was read"); + ).toThrow("data must be a string, ArrayBuffer or ArrayBufferView"); }); - it("inflateSync evaluates the options object before validating the input", () => { + it("inflateSync refuses an input that is not bytes before it evaluates the options object", () => { expect(() => inflateSync(42 as any, { get windowBits(): never { throw new Error("windowBits option was read"); }, }), - ).toThrow("windowBits option was read"); + ).toThrow("data must be a string, ArrayBuffer or ArrayBufferView"); + }); + + it("a numeric option is converted to a number, and then treated as that number is", () => { + const input = "hello hello hello"; + const beyond32Bits = [ + 2 ** 32 + 1, + "4294967297", + 4294967297n, + 2n ** 64n + 1n, + { valueOf: () => 2 ** 32 + 1 }, + "Infinity", + ]; + for (const [compress, outOfRange] of [ + [gzipSync, "Zlib error: Invalid argument"], + [deflateSync, "Zlib error: Invalid argument"], + [zstdCompressSync, "Compression level must be between 1 and 22"], + ] as [(data: string, options: any) => Uint8Array, string][]) { + const expected = compress(input, { level: 1 }); + for (const level of ["1", 1n, Object(1n), [1], { valueOf: () => 1 }, { valueOf: () => 1n }]) { + expect(compress(input, { level })).toEqual(expected); + } + for (const level of beyond32Bits) expect(() => compress(input, { level })).toThrow(outOfRange); + } + for (const [decompress, compressed, bits] of [ + [gunzipSync, gzipSync(input), 31], + [inflateSync, deflateSync(input), -15], + ] as [(data: Uint8Array, options: any) => Uint8Array, Uint8Array, number][]) { + for (const windowBits of [bits, String(bits), BigInt(bits), { valueOf: () => bits }]) { + expect(new TextDecoder().decode(decompress(compressed, { windowBits }))).toBe(input); + } + for (const windowBits of beyond32Bits) { + expect(() => decompress(compressed, { windowBits })).toThrow("Zlib error: Invalid argument"); + } + } + }); + + it("zstdCompress and zstdCompressSync take the same levels", async () => { + const input = "hello hello hello"; + for (const level of [undefined, 1, 22, "1", 1n, Object(1n), { valueOf: () => 1 }]) { + expect(await zstdCompress(input, { level } as any)).toEqual(zstdCompressSync(input, { level } as any)); + } + for (const level of [0, 23, NaN, 2 ** 32 + 1, "4294967297", 4294967297n, { valueOf: () => 2 ** 32 + 1 }]) { + for (const compress of [zstdCompress, zstdCompressSync]) { + expect(() => compress(input, { level } as any)).toThrow("Compression level must be between 1 and 22"); + } + } + }); + + it("zstdDecompress and zstdDecompressSync take no options", async () => { + const compressed = zstdCompressSync("hello"); + for (const ignored of [undefined, null, 5, "x", {}, { level: 99 }]) { + expect( + ( + await (zstdDecompress as (data: Uint8Array, ignored: unknown) => Promise)(compressed, ignored) + ).toString(), + ).toBe("hello"); + expect( + (zstdDecompressSync as (data: Uint8Array, ignored: unknown) => Buffer)(compressed, ignored).toString(), + ).toBe("hello"); + } + }); + + it("takes the bytes of every kind of buffer, and a string as UTF-8", () => { + const text = "h\u00e9llo w\u00f6rld, \u65e5\u672c\u8a9e!"; + const bytes = new TextEncoder().encode(text); + const shared = new Uint8Array(new SharedArrayBuffer(bytes.length)); + shared.set(bytes); + const padded = new Uint8Array(bytes.length + 8); + padded.set(bytes, 4); + const inputs = [ + text, + new String(text), + bytes, + bytes.buffer, + Buffer.from(bytes), + new DataView(bytes.buffer), + padded.subarray(4, 4 + bytes.length), + shared, + shared.buffer, + ]; + for (const [compress, decompress] of [ + [gzipSync, gunzipSync], + [deflateSync, inflateSync], + [zstdCompressSync, zstdDecompressSync], + ] as ((data: any) => Uint8Array)[][]) { + const expected = compress(text); + for (const input of inputs) expect(compress(input)).toEqual(expected); + const sharedCompressed = new Uint8Array(new SharedArrayBuffer(expected.length)); + sharedCompressed.set(expected); + for (const compressed of [expected, sharedCompressed, new DataView(expected.slice().buffer)]) { + expect(new TextDecoder().decode(decompress(compressed))).toBe(text); + } + } }); // An empty result must not register a GC-time deallocator: the backing Vec is diff --git a/test/js/bun/xml/xml.test.ts b/test/js/bun/xml/xml.test.ts index d48445581826..99046ece8656 100644 --- a/test/js/bun/xml/xml.test.ts +++ b/test/js/bun/xml/xml.test.ts @@ -50,6 +50,13 @@ describe("input types", () => { expect(XML.parse(sab)).toEqual(expected); }); + test("a Blob whose bytes are not in memory is refused, and does not parse as nothing", () => { + const s3 = new Bun.S3Client({ accessKeyId: "id", secretAccessKey: "secret", bucket: "bucket" }); + for (const file of [Bun.file(import.meta.path), s3.file("key")]) { + expect(() => XML.parse(file)).toThrow("File blob cannot be used here"); + } + }); + test("Blob parses synchronously", () => { expect(XML.parse(new Blob([doc]))).toEqual(expected); }); diff --git a/test/js/bun/yaml/yaml.test.ts b/test/js/bun/yaml/yaml.test.ts index d0d7469f5465..0dd67edfb87b 100644 --- a/test/js/bun/yaml/yaml.test.ts +++ b/test/js/bun/yaml/yaml.test.ts @@ -305,6 +305,47 @@ development: sharedArray.set(bytes); expect(YAML.parse(sharedArray)).toEqual({ type: "shared", value: 123 }); }); + + test("a Blob whose bytes are not in memory is refused, and does not parse as nothing", () => { + const s3 = new Bun.S3Client({ accessKeyId: "id", secretAccessKey: "secret", bucket: "bucket" }); + for (const file of [Bun.file(import.meta.path), s3.file("key")]) { + expect(() => YAML.parse(file)).toThrow("File blob cannot be used here"); + } + }); + + test("parses a SharedArrayBuffer that another thread is writing", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + /* js */ ` + const { Worker } = require("node:worker_threads"); + const bytes = new Uint8Array(new SharedArrayBuffer(1 << 14)).fill("\\n".charCodeAt(0)); + const writer = new Worker( + \`const { workerData: bytes, parentPort } = require("node:worker_threads"); + parentPort.postMessage("ready"); + // Each pass over the buffer flips every byte, so that a byte the parser reads twice differs. + for (let i = 0; ; i++) bytes[i % bytes.length] = (Math.floor(i / bytes.length) % 2 ? "-" : "\\\\n").charCodeAt(0);\`, + { eval: true, workerData: bytes }, + ); + writer.once("message", () => { + for (let i = 0; i < 200; i++) { + try { + Bun.YAML.parse(bytes); + } catch {} + } + console.log("done"); + process.exit(0); + }); + `, + ], + env: bunEnv, + stderr: "inherit", + }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + expect(stdout).toBe("done\n"); + expect(exitCode).toBe(0); + }); } test("handles File (which is a Blob)", () => { @@ -1191,6 +1232,15 @@ folded: > }); }); + describe("a key that cannot be converted to a string", () => { + test("throws what the conversion throws", () => { + expect(() => YAML.parse("? {toString: x}\n: 1\n")).toThrow(new TypeError("No default value")); + expect(() => YAML.parse("a: 1\n? [b, {toString: x, valueOf: y}]\n: 2\n")).toThrow( + new TypeError("No default value"), + ); + }); + }); + describe("compact collection as key", () => { test("compact sequence", () => { expect(YAML.parse("? - a\n: v\n")).toEqual({ a: "v" });