diff --git a/bench/snippets/buffer-read-write.mjs b/bench/snippets/buffer-read-write.mjs new file mode 100644 index 000000000000..7bd931daac6d --- /dev/null +++ b/bench/snippets/buffer-read-write.mjs @@ -0,0 +1,105 @@ +// Buffer.prototype.read* / write* — the fixed-width accessors that JSC JIT-compiles into +// bounds-checked loads/stores (see JSBuffer.cpp / JavaScriptCore BufferAccessorRegistry). +// +// Three shapes: +// - a tight loop over one buffer (constant offset): mostly measures call/loop overhead +// - a loop over increasing offsets on one buffer: the load/store + bounds check per iteration +// - one access on each of many distinct buffers: previously paid a hidden DataView allocation +// plus a structure transition per buffer +import { bench, group, run } from "../runner.mjs"; + +const size = 4096; +const buf = Buffer.alloc(size); +for (let i = 0; i < size; i++) buf[i] = (i * 37 + 11) & 0xff; + +const many = Array.from({ length: 1024 }, () => Buffer.alloc(64)); + +group("constant offset (10 accesses per iteration)", () => { + bench("readInt32LE(0)", () => { + let s = 0; + for (let i = 0; i < 10; i++) s += buf.readInt32LE(0); + return s; + }); + bench("writeInt32LE(v, 0)", () => { + for (let i = 0; i < 10; i++) buf.writeInt32LE(i, 0); + }); +}); + +group("varying offset over one buffer", () => { + bench("readInt8", () => { + let s = 0; + for (let i = 0; i < size; i++) s += buf.readInt8(i); + return s; + }); + bench("readUInt8", () => { + let s = 0; + for (let i = 0; i < size; i++) s += buf.readUInt8(i); + return s; + }); + bench("readInt16BE", () => { + let s = 0; + for (let i = 0; i < size; i += 2) s += buf.readInt16BE(i); + return s; + }); + bench("readInt32LE", () => { + let s = 0; + for (let i = 0; i < size; i += 4) s += buf.readInt32LE(i); + return s; + }); + bench("readUInt32BE", () => { + let s = 0; + for (let i = 0; i < size; i += 4) s += buf.readUInt32BE(i); + return s; + }); + bench("readFloatLE", () => { + let s = 0; + for (let i = 0; i < size; i += 4) s += buf.readFloatLE(i); + return s; + }); + bench("readDoubleLE", () => { + let s = 0; + for (let i = 0; i < size; i += 8) s += buf.readDoubleLE(i); + return s; + }); + bench("readBigInt64LE", () => { + let s = 0n; + for (let i = 0; i < size; i += 8) s += buf.readBigInt64LE(i); + return s; + }); + bench("writeUInt8", () => { + for (let i = 0; i < size; i++) buf.writeUInt8(i & 0xff, i); + }); + bench("writeInt16BE", () => { + for (let i = 0; i < size; i += 2) buf.writeInt16BE(i, i); + }); + bench("writeInt32LE", () => { + for (let i = 0; i < size; i += 4) buf.writeInt32LE(i, i); + }); + bench("writeUInt32BE", () => { + for (let i = 0; i < size; i += 4) buf.writeUInt32BE(i, i); + }); + bench("writeFloatLE", () => { + for (let i = 0; i < size; i += 4) buf.writeFloatLE(i + 0.5, i); + }); + bench("writeDoubleLE", () => { + for (let i = 0; i < size; i += 8) buf.writeDoubleLE(i + 0.5, i); + }); +}); + +group("one access on each of 1024 buffers", () => { + bench("readInt32LE", () => { + let s = 0; + for (let i = 0; i < many.length; i++) s += many[i].readInt32LE(0); + return s; + }); + bench("writeInt32LE", () => { + for (let i = 0; i < many.length; i++) many[i].writeInt32LE(i, 0); + }); + bench("readDoubleLE", () => { + let s = 0; + for (let i = 0; i < many.length; i++) s += many[i].readDoubleLE(0); + return s; + }); +}); + +await run(); diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index f84dd77e0e19..3d09368916ca 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "549170099226f816a4b204ea1d8fa102fb79eefa"; +export const WEBKIT_VERSION = "autobuild-preview-pr-330-8debd979"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index 1407381f81fd..a33ca117ddaf 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -338,7 +338,6 @@ declare function $controller(): TODO; declare function $createFIFO(): TODO; declare function $createUninitializedArrayBuffer(size: number): ArrayBuffer; declare function $data(): TODO; -declare function $dataView(): TODO; declare function $decode(): TODO; declare function $dirname(): TODO; declare function $disturbed(): TODO; @@ -692,8 +691,6 @@ declare function $toClass(fn: Function, name: string, base?: Function | undefine declare function $min(a: number, b: number): number; -declare function $checkBufferRead(buf: Buffer, offset: number, byteLength: number): undefined; - /** * Schedules a callback to be invoked as a microtask. */ diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index b6495c2eea2b..8fe6a5819061 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -61,7 +61,6 @@ using namespace JSC; macro(byobRequest) \ macro(bytes) \ macro(cancel) \ - macro(checkBufferRead) \ macro(checks) \ macro(cloneArrayBuffer) \ macro(close) \ @@ -74,7 +73,6 @@ using namespace JSC; macro(createUninitializedArrayBuffer) \ macro(ctimeMs) \ macro(data) \ - macro(dataView) \ macro(decode) \ macro(dest) \ macro(dirname) \ diff --git a/src/js/builtins/JSBufferPrototype.ts b/src/js/builtins/JSBufferPrototype.ts index 81bb7507ef87..f3e9edc3bb58 100644 --- a/src/js/builtins/JSBufferPrototype.ts +++ b/src/js/builtins/JSBufferPrototype.ts @@ -1,675 +1,8 @@ -// The fastest way as of April 2022 is to use DataView. -// DataView has intrinsics that cause inlining - interface BufferExt extends Buffer { - $dataView?: DataView; - toString(encoding?: BufferEncoding, start?: number, end?: number): string; toString(offset: number, length: number, encoding?: BufferEncoding): string; } -export function setBigUint64(this: BufferExt, offset, value, le) { - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setBigUint64( - offset, - value, - le, - ); -} - -export function readInt8(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined) $checkBufferRead(this, offset, 1); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getInt8(offset); -} - -export function readUInt8(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined) $checkBufferRead(this, offset, 1); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getUint8(offset); -} - -export function readInt16LE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 1] === undefined) - $checkBufferRead(this, offset, 2); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getInt16(offset, true); -} - -export function readInt16BE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 1] === undefined) - $checkBufferRead(this, offset, 2); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getInt16(offset, false); -} - -export function readUInt16LE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 1] === undefined) - $checkBufferRead(this, offset, 2); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getUint16(offset, true); -} - -export function readUInt16BE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 1] === undefined) - $checkBufferRead(this, offset, 2); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getUint16(offset, false); -} - -export function readInt32LE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 3] === undefined) - $checkBufferRead(this, offset, 4); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getInt32(offset, true); -} - -export function readInt32BE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 3] === undefined) - $checkBufferRead(this, offset, 4); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getInt32(offset, false); -} - -export function readUInt32LE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 3] === undefined) - $checkBufferRead(this, offset, 4); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getUint32(offset, true); -} - -export function readUInt32BE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 3] === undefined) - $checkBufferRead(this, offset, 4); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getUint32(offset, false); -} - -export function readIntLE(this: BufferExt, offset, byteLength) { - if (offset === undefined) throw $ERR_INVALID_ARG_TYPE("offset", "number", offset); - if (typeof byteLength !== "number") throw $ERR_INVALID_ARG_TYPE("byteLength", "number", byteLength); - const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); - switch (byteLength) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: { - // Infinity must fall through to boundsError() so it reports the - // ">= 0 and <= N" range like Node, not "an integer". - if (typeof offset !== "number" || ((offset | 0) !== offset && offset !== Infinity && offset !== -Infinity)) - require("internal/validators").validateInteger(offset, "offset"); - let thisLength; - if (!(offset >= 0 && offset <= (thisLength = this.length) - byteLength)) - require("internal/buffer").boundsError(offset, (thisLength ?? this.length) - byteLength); - } - } - switch (byteLength) { - case 1: { - return view.getInt8(offset); - } - case 2: { - return view.getInt16(offset, true); - } - case 3: { - const val = view.getUint16(offset, true) + view.getUint8(offset + 2) * 2 ** 16; - return val | ((val & (2 ** 23)) * 0x1fe); - } - case 4: { - return view.getInt32(offset, true); - } - case 5: { - const last = view.getUint8(offset + 4); - return (last | ((last & (2 ** 7)) * 0x1fffffe)) * 2 ** 32 + view.getUint32(offset, true); - } - case 6: { - const last = view.getUint16(offset + 4, true); - return (last | ((last & (2 ** 15)) * 0x1fffe)) * 2 ** 32 + view.getUint32(offset, true); - } - } - require("internal/buffer").boundsError(byteLength, 6, "byteLength"); -} - -export function readIntBE(this: BufferExt, offset, byteLength) { - if (offset === undefined) throw $ERR_INVALID_ARG_TYPE("offset", "number", offset); - if (typeof byteLength !== "number") throw $ERR_INVALID_ARG_TYPE("byteLength", "number", byteLength); - const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); - switch (byteLength) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: { - // Infinity must fall through to boundsError() so it reports the - // ">= 0 and <= N" range like Node, not "an integer". - if (typeof offset !== "number" || ((offset | 0) !== offset && offset !== Infinity && offset !== -Infinity)) - require("internal/validators").validateInteger(offset, "offset"); - let thisLength; - if (!(offset >= 0 && offset <= (thisLength = this.length) - byteLength)) - require("internal/buffer").boundsError(offset, (thisLength ?? this.length) - byteLength); - } - } - switch (byteLength) { - case 1: { - return view.getInt8(offset); - } - case 2: { - return view.getInt16(offset, false); - } - case 3: { - const val = view.getUint16(offset + 1, false) + view.getUint8(offset) * 2 ** 16; - return val | ((val & (2 ** 23)) * 0x1fe); - } - case 4: { - return view.getInt32(offset, false); - } - case 5: { - const last = view.getUint8(offset); - return (last | ((last & (2 ** 7)) * 0x1fffffe)) * 2 ** 32 + view.getUint32(offset + 1, false); - } - case 6: { - const last = view.getUint16(offset, false); - return (last | ((last & (2 ** 15)) * 0x1fffe)) * 2 ** 32 + view.getUint32(offset + 2, false); - } - } - require("internal/buffer").boundsError(byteLength, 6, "byteLength"); -} - -export function readUIntLE(this: BufferExt, offset, byteLength) { - if (offset === undefined) throw $ERR_INVALID_ARG_TYPE("offset", "number", offset); - if (typeof byteLength !== "number") throw $ERR_INVALID_ARG_TYPE("byteLength", "number", byteLength); - const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); - switch (byteLength) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: { - // Infinity must fall through to boundsError() so it reports the - // ">= 0 and <= N" range like Node, not "an integer". - if (typeof offset !== "number" || ((offset | 0) !== offset && offset !== Infinity && offset !== -Infinity)) - require("internal/validators").validateInteger(offset, "offset"); - let thisLength; - if (!(offset >= 0 && offset <= (thisLength = this.length) - byteLength)) - require("internal/buffer").boundsError(offset, (thisLength ?? this.length) - byteLength); - } - } - switch (byteLength) { - case 1: { - return view.getUint8(offset); - } - case 2: { - return view.getUint16(offset, true); - } - case 3: { - return view.getUint16(offset, true) + view.getUint8(offset + 2) * 2 ** 16; - } - case 4: { - return view.getUint32(offset, true); - } - case 5: { - return view.getUint8(offset + 4) * 2 ** 32 + view.getUint32(offset, true); - } - case 6: { - return view.getUint16(offset + 4, true) * 2 ** 32 + view.getUint32(offset, true); - } - } - require("internal/buffer").boundsError(byteLength, 6, "byteLength"); -} - -export function readUIntBE(this: BufferExt, offset, byteLength) { - if (offset === undefined) throw $ERR_INVALID_ARG_TYPE("offset", "number", offset); - if (typeof byteLength !== "number") throw $ERR_INVALID_ARG_TYPE("byteLength", "number", byteLength); - const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); - switch (byteLength) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: { - // Infinity must fall through to boundsError() so it reports the - // ">= 0 and <= N" range like Node, not "an integer". - if (typeof offset !== "number" || ((offset | 0) !== offset && offset !== Infinity && offset !== -Infinity)) - require("internal/validators").validateInteger(offset, "offset"); - let thisLength; - if (!(offset >= 0 && offset <= (thisLength = this.length) - byteLength)) - require("internal/buffer").boundsError(offset, (thisLength ?? this.length) - byteLength); - } - } - switch (byteLength) { - case 1: { - return view.getUint8(offset); - } - case 2: { - return view.getUint16(offset, false); - } - case 3: { - return view.getUint16(offset + 1, false) + view.getUint8(offset) * 2 ** 16; - } - case 4: { - return view.getUint32(offset, false); - } - case 5: { - return view.getUint8(offset) * 2 ** 32 + view.getUint32(offset + 1, false); - } - case 6: { - return view.getUint16(offset, false) * 2 ** 32 + view.getUint32(offset + 2, false); - } - } - require("internal/buffer").boundsError(byteLength, 6, "byteLength"); -} - -export function readFloatLE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 3] === undefined) - $checkBufferRead(this, offset, 4); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getFloat32(offset, true); -} - -export function readFloatBE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 3] === undefined) - $checkBufferRead(this, offset, 4); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getFloat32(offset, false); -} - -export function readDoubleLE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 7] === undefined) - $checkBufferRead(this, offset, 8); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getFloat64(offset, true); -} - -export function readDoubleBE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 7] === undefined) - $checkBufferRead(this, offset, 8); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getFloat64(offset, false); -} - -export function readBigInt64LE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 7] === undefined) - $checkBufferRead(this, offset, 8); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getBigInt64(offset, true); -} - -export function readBigInt64BE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 7] === undefined) - $checkBufferRead(this, offset, 8); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getBigInt64(offset, false); -} - -export function readBigUInt64LE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 7] === undefined) - $checkBufferRead(this, offset, 8); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getBigUint64(offset, true); -} - -export function readBigUInt64BE(this: BufferExt, offset) { - if (offset === undefined) offset = 0; - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 7] === undefined) - $checkBufferRead(this, offset, 8); - return (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).getBigUint64(offset, false); -} - -export function writeInt8(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = -0x80; - const max = 0x7f; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined) require("internal/buffer").writeU_Int8(this, value, offset, min, max); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt8(offset, value); - return offset + 1; -} - -export function writeUInt8(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = 0; - const max = 0xff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined) require("internal/buffer").writeU_Int8(this, value, offset, min, max); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint8(offset, value); - return offset + 1; -} - -export function writeInt16LE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = -0x8000; - const max = 0x7fff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined || this[offset + 1] === undefined) require("internal/buffer").checkInt(this, value, offset, min, max, 2); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt16(offset, value, true); - return offset + 2; -} - -export function writeInt16BE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = -0x8000; - const max = 0x7fff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined || this[offset + 1] === undefined) require("internal/buffer").checkInt(this, value, offset, min, max, 2); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt16(offset, value, false); - return offset + 2; -} - -export function writeUInt16LE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = 0; - const max = 0xffff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined || this[offset + 1] === undefined) require("internal/buffer").checkInt(this, value, offset, min, max, 2); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint16(offset, value, true); - return offset + 2; -} - -export function writeUInt16BE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = 0; - const max = 0xffff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined || this[offset + 1] === undefined) require("internal/buffer").checkInt(this, value, offset, min, max, 2); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint16(offset, value, false); - return offset + 2; -} - -export function writeInt32LE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = -0x80000000; - const max = 0x7fffffff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined || this[offset + 3] === undefined) require("internal/buffer").checkInt(this, value, offset, min, max, 4); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt32(offset, value, true); - return offset + 4; -} - -export function writeInt32BE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = -0x80000000; - const max = 0x7fffffff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined || this[offset + 3] === undefined) require("internal/buffer").checkInt(this, value, offset, min, max, 4); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setInt32(offset, value, false); - return offset + 4; -} - -export function writeUInt32LE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = 0; - const max = 0xffffffff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined || this[offset + 3] === undefined) require("internal/buffer").checkInt(this, value, offset, min, max, 4); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint32(offset, value, true); - return offset + 4; -} - -export function writeUInt32BE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - const min = 0; - const max = 0xffffffff; - // prettier-ignore - if (typeof offset !== "number" || value < min || value > max || this[offset] === undefined || this[offset + 3] === undefined) require("internal/buffer").checkInt(this, value, offset, min, max, 4); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setUint32(offset, value, false); - return offset + 4; -} - -export function writeIntLE(this: BufferExt, value, offset, byteLength) { - const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); - value = +value; - - switch (byteLength) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: { - const max = 2 ** (8 * byteLength - 1) - 1; - require("internal/buffer").checkInt(this, value, offset, -max - 1, max, byteLength); - break; - } - default: { - require("internal/buffer").boundsError(byteLength, 6, "byteLength"); - break; - } - } - switch (byteLength) { - case 1: { - view.setInt8(offset, value); - break; - } - case 2: { - view.setInt16(offset, value, true); - break; - } - case 3: { - view.setUint16(offset, value & 0xffff, true); - view.setInt8(offset + 2, Math.floor(value * 2 ** -16)); - break; - } - case 4: { - view.setInt32(offset, value, true); - break; - } - case 5: { - view.setUint32(offset, value | 0, true); - view.setInt8(offset + 4, Math.floor(value * 2 ** -32)); - break; - } - case 6: { - view.setUint32(offset, value | 0, true); - view.setInt16(offset + 4, Math.floor(value * 2 ** -32), true); - break; - } - } - return offset + byteLength; -} - -export function writeIntBE(this: BufferExt, value, offset, byteLength) { - const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); - value = +value; - - switch (byteLength) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: { - const max = 2 ** (8 * byteLength - 1) - 1; - require("internal/buffer").checkInt(this, value, offset, -max - 1, max, byteLength); - break; - } - default: { - require("internal/buffer").boundsError(byteLength, 6, "byteLength"); - break; - } - } - switch (byteLength) { - case 1: { - view.setInt8(offset, value); - break; - } - case 2: { - view.setInt16(offset, value, false); - break; - } - case 3: { - view.setUint16(offset + 1, value & 0xffff, false); - view.setInt8(offset, Math.floor(value * 2 ** -16)); - break; - } - case 4: { - view.setInt32(offset, value, false); - break; - } - case 5: { - view.setUint32(offset + 1, value | 0, false); - view.setInt8(offset, Math.floor(value * 2 ** -32)); - break; - } - case 6: { - view.setUint32(offset + 2, value | 0, false); - view.setInt16(offset, Math.floor(value * 2 ** -32), false); - break; - } - } - return offset + byteLength; -} - -export function writeUIntLE(this: BufferExt, value, offset, byteLength) { - const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); - value = +value; - - switch (byteLength) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: { - require("internal/buffer").checkInt(this, value, offset, 0, 2 ** (8 * byteLength) - 1, byteLength); - break; - } - default: { - require("internal/buffer").boundsError(byteLength, 6, "byteLength"); - break; - } - } - switch (byteLength) { - case 1: { - view.setUint8(offset, value); - break; - } - case 2: { - view.setUint16(offset, value, true); - break; - } - case 3: { - view.setUint16(offset, value & 0xffff, true); - view.setUint8(offset + 2, Math.floor(value * 2 ** -16)); - break; - } - case 4: { - view.setUint32(offset, value, true); - break; - } - case 5: { - view.setUint32(offset, value | 0, true); - view.setUint8(offset + 4, Math.floor(value * 2 ** -32)); - break; - } - case 6: { - view.setUint32(offset, value | 0, true); - view.setUint16(offset + 4, Math.floor(value * 2 ** -32), true); - break; - } - } - return offset + byteLength; -} - -export function writeUIntBE(this: BufferExt, value, offset, byteLength) { - const view = (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)); - value = +value; - - switch (byteLength) { - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: { - require("internal/buffer").checkInt(this, value, offset, 0, 2 ** (8 * byteLength) - 1, byteLength); - break; - } - default: { - require("internal/buffer").boundsError(byteLength, 6, "byteLength"); - break; - } - } - switch (byteLength) { - case 1: { - view.setUint8(offset, value); - break; - } - case 2: { - view.setUint16(offset, value, false); - break; - } - case 3: { - view.setUint16(offset + 1, value & 0xffff, false); - view.setUint8(offset, Math.floor(value * 2 ** -16)); - break; - } - case 4: { - view.setUint32(offset, value, false); - break; - } - case 5: { - view.setUint32(offset + 1, value | 0, false); - view.setUint8(offset, Math.floor(value * 2 ** -32)); - break; - } - case 6: { - view.setUint32(offset + 2, value | 0, false); - view.setUint16(offset, Math.floor(value * 2 ** -32), false); - break; - } - } - return offset + byteLength; -} - -export function writeFloatLE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - // prettier-ignore - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 3] === undefined) require("internal/buffer").checkBounds(this, offset, 4); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat32(offset, value, true); - return offset + 4; -} - -export function writeFloatBE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - // prettier-ignore - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 3] === undefined) require("internal/buffer").checkBounds(this, offset, 4); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat32(offset, value, false); - return offset + 4; -} - -export function writeDoubleLE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - // prettier-ignore - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 7] === undefined) require("internal/buffer").checkBounds(this, offset, 8); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat64(offset, value, true); - return offset + 8; -} - -export function writeDoubleBE(this: BufferExt, value, offset) { - if (offset === undefined) offset = 0; - value = +value; - // prettier-ignore - if (typeof offset !== "number" || this[offset] === undefined || this[offset + 7] === undefined) require("internal/buffer").checkBounds(this, offset, 8); - (this.$dataView ||= new DataView(this.buffer, this.byteOffset, this.byteLength)).setFloat64(offset, value, false); - return offset + 8; -} - export function toJSON(this: BufferExt) { const type = "Buffer"; const data = Array.from(this); diff --git a/src/js/internal/buffer.ts b/src/js/internal/buffer.ts deleted file mode 100644 index 691b51738f5b..000000000000 --- a/src/js/internal/buffer.ts +++ /dev/null @@ -1,50 +0,0 @@ -const { validateNumber } = require("internal/validators"); - -function boundsError(value, length, type?) { - if (Math.floor(value) !== value) { - validateNumber(value, type); - throw $ERR_OUT_OF_RANGE(type || "offset", "an integer", value); - } - if (length < 0) throw $ERR_BUFFER_OUT_OF_BOUNDS(); - throw $ERR_OUT_OF_RANGE(type || "offset", `>= ${type ? 1 : 0} and <= ${length}`, value); -} - -function checkBounds(buf, offset, byteLength) { - validateNumber(offset, "offset"); - if (buf[offset] === undefined || buf[offset + byteLength - 1] === undefined) - boundsError(offset, buf.length - byteLength); -} - -function checkInt(buf, value, offset, min, max, byteLength) { - if (value > max || value < min) { - const n = typeof min === "bigint" ? "n" : ""; - let range; - if (byteLength > 4) { - if (min === 0 || min === 0n) { - range = `>= 0${n} and < 2${n} ** ${byteLength * 8}${n}`; - } else { - range = `>= -(2${n} ** ${byteLength * 8 - 1}${n}) and ` + `< 2${n} ** ${byteLength * 8 - 1}${n}`; - } - } else { - range = `>= ${min}${n} and <= ${max}${n}`; - } - throw $ERR_OUT_OF_RANGE("value", range, value); - } - checkBounds(buf, offset, byteLength); -} - -function writeU_Int8(buf, value, offset, min, max) { - // `checkInt()` can not be used here because it checks two entries. - validateNumber(offset, "offset"); - if (value > max || value < min) { - throw $ERR_OUT_OF_RANGE("value", `>= ${min} and <= ${max}`, value); - } - if (buf[offset] === undefined) boundsError(offset, buf.length - 1); -} - -export default { - boundsError, - checkBounds, - checkInt, - writeU_Int8, -}; diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index ffdae2557a6a..938f4068a7d6 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -78,7 +78,11 @@ #include // #include +#include #include +#include +#include +#include #include #include #include @@ -129,6 +133,50 @@ JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64BE); JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64LE); JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64BE); +// Fixed-width readers / writers. Each is registered as a JSC "buffer accessor" (BufferAccessorIntrinsic + +// runtime/BufferAccessorRegistry.h) so DFG/FTL compile call sites into bounds-checked loads / stores. +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readInt8); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readUInt8); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readInt16LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readInt16BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readUInt16LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readUInt16BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readInt32LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readInt32BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readUInt32LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readUInt32BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readFloatLE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readFloatBE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readDoubleLE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readDoubleBE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readBigInt64LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readBigInt64BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readBigUInt64LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readBigUInt64BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeInt8); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUInt8); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeInt16LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeInt16BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUInt16LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUInt16BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeInt32LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeInt32BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUInt32LE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUInt32BE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeFloatLE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeFloatBE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeDoubleLE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeDoubleBE); +// Variable-width (byteLength 1..6) readers / writers. JIT-inlined when byteLength is a constant 1, 2 or 4. +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readIntLE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readIntBE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readUIntLE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_readUIntBE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeIntLE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeIntBE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUIntLE); +JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUIntBE); + extern "C" EncodedJSValue WebCore_BufferEncodingType_toJS(JSC::JSGlobalObject* lexicalGlobalObject, WebCore::BufferEncodingType encoding) { // clang-format off @@ -2759,7 +2807,7 @@ extern "C" JSC_DECLARE_JIT_OPERATION_WITHOUT_WTF_INTERNAL(jsBufferConstructorAll extern "C" JSC_DECLARE_JIT_OPERATION_WITHOUT_WTF_INTERNAL(jsBufferConstructorAllocUnsafeWithoutTypeChecks, JSUint8Array*, (JSC::JSGlobalObject * lexicalGlobalObject, void* thisValue, int size)); extern "C" JSC_DECLARE_JIT_OPERATION_WITHOUT_WTF_INTERNAL(jsBufferConstructorAllocUnsafeSlowWithoutTypeChecks, JSUint8Array*, (JSC::JSGlobalObject * lexicalGlobalObject, void* thisValue, int size)); -static size_t validateOffsetBigInt64(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue offsetVal, size_t byteLength) +static size_t validateOffsetBigInt64(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue offsetVal, size_t byteLength, bool viewHasLength = true) { // Node's checkBounds/boundsError validates the offset's type and // integer-ness before reporting a too-short buffer, and reports a @@ -2782,6 +2830,12 @@ static size_t validateOffsetBigInt64(JSC::JSGlobalObject* lexicalGlobalObject, J } } + if (!viewHasLength) [[unlikely]] { + // A DataView receiver has no `length`, so boundsError() compares against NaN. + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, jsNumber(offsetD)); + return 0; + } + if (byteLength < 8) [[unlikely]] { auto* error = Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_BUFFER_OUT_OF_BOUNDS, "Attempt to access memory outside buffer bounds"_s); scope.throwException(lexicalGlobalObject, error); @@ -3025,15 +3079,457 @@ template void write_int64_be(uint8_t* buffer, I value) buffer[7] = val[0]; } +// Fixed-width read* / write* (readInt8 ... writeDoubleBE, plus the BigInt64 reads). JSC JITs call +// sites of these (BufferAccessorIntrinsic) and OSR-exits back here for anything it doesn't speculate, +// so these functions own the error behavior: they follow lib/internal/buffer.js exactly. +namespace { + +// Any ArrayBufferView receiver is accepted (byte-length semantics); anything else is +// ERR_INVALID_ARG_TYPE("buf"). +static JSC::JSArrayBufferView* bufferAccessReceiver(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue thisValue) +{ + auto* view = dynamicDowncast(thisValue); + if (!view) [[unlikely]] { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "buf"_s, "Buffer"_s, thisValue); + return nullptr; + } + return view; +} + +// validateNumber(offset, 'offset') +static bool bufferAccessCheckOffsetType(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue offsetValue) +{ + if (!offsetValue.isNumber()) [[unlikely]] { + Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetValue); + return false; + } + return true; +} + +// boundsError(offset, byteLength - byteSize): the same ERR_OUT_OF_RANGE / ERR_BUFFER_OUT_OF_BOUNDS as +// lib/internal/buffer.js. Returns the offset when it is in range after all (an integral double). +static std::optional bufferAccessCheckOffsetBounds(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSValue offsetValue, size_t viewLength, size_t byteSize, bool viewHasLength = true) +{ + double offset = offsetValue.asNumber(); + // Math.floor(value) !== value: NaN and fractions are "an integer"; +-Infinity get the range error. + if (std::floor(offset) != offset) [[unlikely]] { + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, "an integer"_s, offsetValue); + return std::nullopt; + } + if (!viewHasLength) [[unlikely]] { + // A DataView receiver has no `length`, so boundsError() compares against NaN. + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, ">= 0 and <= NaN"_s, offsetValue); + return std::nullopt; + } + if (viewLength < byteSize) [[unlikely]] { + Bun::ERR::BUFFER_OUT_OF_BOUNDS(scope, lexicalGlobalObject, ""_s); + return std::nullopt; + } + size_t maxOffset = viewLength - byteSize; + if (!(offset >= 0 && offset <= static_cast(maxOffset))) [[unlikely]] { + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "offset"_s, makeString(">= 0 and <= "_s, maxOffset), offsetValue); + return std::nullopt; + } + return static_cast(offset); +} + +// The unsigned integer type a scalar's bytes are moved in. +template struct BufferAccessStorage { + using Type = std::make_unsigned_t; +}; +template<> struct BufferAccessStorage { + using Type = uint32_t; +}; +template<> struct BufferAccessStorage { + using Type = uint64_t; +}; + +// Reads / stores in the accessor's byte order, on an unaligned pointer. +template +static ALWAYS_INLINE Storage bufferAccessLoad(const uint8_t* address) +{ + Storage value = WTF::unalignedLoad(address); + if constexpr (!isLittleEndian && sizeof(Storage) > 1) + value = WTF::flipBytes(value); + return value; +} + +template +static ALWAYS_INLINE void bufferAccessStore(uint8_t* address, Storage value) +{ + if constexpr (!isLittleEndian && sizeof(Storage) > 1) + value = WTF::flipBytes(value); + WTF::unalignedStore(address, value); +} + +// read*(offset = 0). `T` is the scalar type (int8_t ... double, int64_t / uint64_t for the BigInt reads). +template +static JSC::EncodedJSValue bufferRead(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + using Storage = typename BufferAccessStorage::Type; + constexpr size_t byteSize = sizeof(T); + JSValue thisValue = callFrame->thisValue(); + JSValue offsetValue = callFrame->argument(0); + + auto* view = dynamicDowncast(thisValue); + size_t offset; + // The fast path: an int32 (or missing) offset inside a real ArrayBufferView -- what the JIT'd form assumes. + if (view && view->type() != JSC::DataViewType && (offsetValue.isInt32() || offsetValue.isUndefined())) [[likely]] { + int32_t offset32 = offsetValue.isInt32() ? offsetValue.asInt32() : 0; + size_t viewLength = view->length(); + if (offset32 >= 0 && static_cast(offset32) + byteSize <= viewLength) [[likely]] { + offset = offset32; + goto fastPath; + } + } + + { + // The slow path: same errors, in the same order, as the JS implementation. + if (offsetValue.isUndefined()) + offsetValue = jsNumber(0); + if (!bufferAccessCheckOffsetType(lexicalGlobalObject, scope, offsetValue)) + return {}; + if (!view) [[unlikely]] { + bufferAccessReceiver(lexicalGlobalObject, scope, thisValue); + return {}; + } + auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->length(), byteSize, view->type() != JSC::DataViewType); + if (!checkedOffset) + return {}; + offset = *checkedOffset; + } + +fastPath: + const uint8_t* address = static_cast(view->vector()) + offset; + Storage raw = bufferAccessLoad(address); + if constexpr (std::is_floating_point_v) { + // The bytes may be any NaN; a boxed JS double must be the purified one (DataView does the same). + return JSValue::encode(jsNumber(JSC::purifyNaN(static_cast(std::bit_cast(raw))))); + } else if constexpr (byteSize == 8) { + // readBigInt64* / readBigUInt64* + RELEASE_AND_RETURN(scope, JSValue::encode(JSBigInt::makeHeapBigIntOrBigInt32(lexicalGlobalObject, std::bit_cast(raw)))); + } else + return JSValue::encode(jsNumber(std::bit_cast(raw))); +} + +// write*(value, offset = 0). Ints: `value = +value` then checkInt() / writeU_Int8() (which range-check the +// number and throw ERR_OUT_OF_RANGE("value")); the stored bytes are the number truncated the way a +// DataView / typed array store truncates (ToInt32). Floats: checkBounds() only. +template +static JSC::EncodedJSValue bufferWrite(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + using Storage = typename BufferAccessStorage::Type; + constexpr size_t byteSize = sizeof(T); + constexpr bool isFloat = std::is_floating_point_v; + JSValue thisValue = callFrame->thisValue(); + JSValue valueValue = callFrame->argument(0); + JSValue offsetValue = callFrame->argument(1); + + auto* view = dynamicDowncast(thisValue); + + // `value = +value` comes first (it can run user code and throw), before any offset validation. + double number; + if (valueValue.isNumber()) [[likely]] + number = valueValue.asNumber(); + else { + number = valueValue.toNumber(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + } + + // The value range check of writeU_Int8() / checkInt() (floats and NaN are never out of range). + auto valueIsInRange = [&] { + if constexpr (isFloat) + return true; + else + return !(number < static_cast(std::numeric_limits::min()) || number > static_cast(std::numeric_limits::max())); + }; + auto throwValueOutOfRange = [&] { + // Node reports the coerced number ("Received 40000" for the string "40000"), not the argument. + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "value"_s, static_cast(std::numeric_limits::min()), static_cast(std::numeric_limits::max()), jsNumber(number)); + }; + + size_t offset; + // The fast path: an in-range value at an int32 (or missing) offset inside a real ArrayBufferView. + if (view && view->type() != JSC::DataViewType && (offsetValue.isInt32() || offsetValue.isUndefined())) [[likely]] { + int32_t offset32 = offsetValue.isInt32() ? offsetValue.asInt32() : 0; + size_t viewLength = view->length(); + if (offset32 >= 0 && static_cast(offset32) + byteSize <= viewLength && valueIsInRange()) [[likely]] { + offset = offset32; + goto fastPath; + } + } + + { + // The slow path. The odd argument-validation order is Node's own: writeU_Int8() (the one-byte + // writers) validates the offset before the value's range, checkInt() (2 and 4 bytes) the other + // way around; the float writers have no value range at all (checkBounds()). + if (offsetValue.isUndefined()) + offsetValue = jsNumber(0); + if constexpr (byteSize == 1) { + if (!bufferAccessCheckOffsetType(lexicalGlobalObject, scope, offsetValue)) + return {}; + if (!valueIsInRange()) + return throwValueOutOfRange(); + } else if constexpr (!isFloat) { + if (!valueIsInRange()) + return throwValueOutOfRange(); + if (!bufferAccessCheckOffsetType(lexicalGlobalObject, scope, offsetValue)) + return {}; + } else { + if (!bufferAccessCheckOffsetType(lexicalGlobalObject, scope, offsetValue)) + return {}; + } + if (!view) [[unlikely]] { + bufferAccessReceiver(lexicalGlobalObject, scope, thisValue); + return {}; + } + auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->length(), byteSize, view->type() != JSC::DataViewType); + if (!checkedOffset) + return {}; + offset = *checkedOffset; + } + +fastPath: + uint8_t* address = static_cast(view->vector()) + offset; + if constexpr (isFloat) { + if constexpr (byteSize == 4) + bufferAccessStore(address, std::bit_cast(static_cast(number))); + else + bufferAccessStore(address, std::bit_cast(number)); + } else if constexpr (std::is_signed_v) + bufferAccessStore(address, static_cast(JSC::toInt32(number))); + else + bufferAccessStore(address, static_cast(JSC::toUInt32(number))); + return JSValue::encode(jsNumber(offset + byteSize)); +} + +} // namespace + +#define DEFINE_BUFFER_READ(name, T, isLittleEndian) \ + JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_##name, (JSGlobalObject * lexicalGlobalObject, CallFrame * callFrame)) \ + { \ + return bufferRead(lexicalGlobalObject, callFrame); \ + } +#define DEFINE_BUFFER_WRITE(name, T, isLittleEndian) \ + JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_##name, (JSGlobalObject * lexicalGlobalObject, CallFrame * callFrame)) \ + { \ + return bufferWrite(lexicalGlobalObject, callFrame); \ + } + +DEFINE_BUFFER_READ(readInt8, int8_t, true) +DEFINE_BUFFER_READ(readUInt8, uint8_t, true) +DEFINE_BUFFER_READ(readInt16LE, int16_t, true) +DEFINE_BUFFER_READ(readInt16BE, int16_t, false) +DEFINE_BUFFER_READ(readUInt16LE, uint16_t, true) +DEFINE_BUFFER_READ(readUInt16BE, uint16_t, false) +DEFINE_BUFFER_READ(readInt32LE, int32_t, true) +DEFINE_BUFFER_READ(readInt32BE, int32_t, false) +DEFINE_BUFFER_READ(readUInt32LE, uint32_t, true) +DEFINE_BUFFER_READ(readUInt32BE, uint32_t, false) +DEFINE_BUFFER_READ(readFloatLE, float, true) +DEFINE_BUFFER_READ(readFloatBE, float, false) +DEFINE_BUFFER_READ(readDoubleLE, double, true) +DEFINE_BUFFER_READ(readDoubleBE, double, false) +DEFINE_BUFFER_READ(readBigInt64LE, int64_t, true) +DEFINE_BUFFER_READ(readBigInt64BE, int64_t, false) +DEFINE_BUFFER_READ(readBigUInt64LE, uint64_t, true) +DEFINE_BUFFER_READ(readBigUInt64BE, uint64_t, false) +DEFINE_BUFFER_WRITE(writeInt8, int8_t, true) +DEFINE_BUFFER_WRITE(writeUInt8, uint8_t, true) +DEFINE_BUFFER_WRITE(writeInt16LE, int16_t, true) +DEFINE_BUFFER_WRITE(writeInt16BE, int16_t, false) +DEFINE_BUFFER_WRITE(writeUInt16LE, uint16_t, true) +DEFINE_BUFFER_WRITE(writeUInt16BE, uint16_t, false) +DEFINE_BUFFER_WRITE(writeInt32LE, int32_t, true) +DEFINE_BUFFER_WRITE(writeInt32BE, int32_t, false) +DEFINE_BUFFER_WRITE(writeUInt32LE, uint32_t, true) +DEFINE_BUFFER_WRITE(writeUInt32BE, uint32_t, false) +DEFINE_BUFFER_WRITE(writeFloatLE, float, true) +DEFINE_BUFFER_WRITE(writeFloatBE, float, false) +DEFINE_BUFFER_WRITE(writeDoubleLE, double, true) +DEFINE_BUFFER_WRITE(writeDoubleBE, double, false) + +#undef DEFINE_BUFFER_READ +#undef DEFINE_BUFFER_WRITE + +// Variable-width readers/writers: read(U)Int{LE,BE}(offset, byteLength) and +// write(U)Int{LE,BE}(value, offset, byteLength) with byteLength 1..6, per lib/internal/buffer.js. +namespace { + +// The byteLength argument: the two errors boundsError(byteLength, 6, "byteLength") produces. +static EncodedJSValue throwBufferInvalidByteLength(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, JSValue byteLengthValue) +{ + double byteLength = byteLengthValue.asNumber(); + if (std::floor(byteLength) != byteLength) + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "byteLength"_s, "an integer"_s, byteLengthValue); + return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "byteLength"_s, ">= 1 and <= 6"_s, byteLengthValue); +} + +// checkInt()'s value range for byteLength <= 4 and the ">= -(2 ** N) and < 2 ** N" wording it uses +// for the 5- and 6-byte widths. +static bool bufferWriteVarWidthCheckValue(JSC::JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& scope, double number, size_t byteLength, bool isSigned) +{ + double min = isSigned ? -std::pow(2.0, 8.0 * byteLength - 1) : 0; + double max = (isSigned ? std::pow(2.0, 8.0 * byteLength - 1) : std::pow(2.0, 8.0 * byteLength)) - 1; + if (!(number > max || number < min)) [[likely]] + return true; + if (byteLength > 4) { + if (isSigned) + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "value"_s, makeString(">= -(2 ** "_s, 8 * byteLength - 1, ") and < 2 ** "_s, 8 * byteLength - 1), jsNumber(number)); + else + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "value"_s, makeString(">= 0 and < 2 ** "_s, 8 * byteLength), jsNumber(number)); + } else + Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "value"_s, min, max, jsNumber(number)); + return false; +} + +template +static JSC::EncodedJSValue bufferReadVarWidth(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue offsetValue = callFrame->argument(0); + JSValue byteLengthValue = callFrame->argument(1); + + if (offsetValue.isUndefined()) [[unlikely]] + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetValue); + if (!byteLengthValue.isNumber()) [[unlikely]] + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "byteLength"_s, "number"_s, byteLengthValue); + double byteLengthNumber = byteLengthValue.asNumber(); + if (!(byteLengthNumber >= 1 && byteLengthNumber <= 6 && std::floor(byteLengthNumber) == byteLengthNumber)) [[unlikely]] + return throwBufferInvalidByteLength(lexicalGlobalObject, scope, byteLengthValue); + size_t byteLength = static_cast(byteLengthNumber); + + if (!bufferAccessCheckOffsetType(lexicalGlobalObject, scope, offsetValue)) [[unlikely]] + return {}; + auto* view = dynamicDowncast(callFrame->thisValue()); + if (!view) [[unlikely]] { + bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); + return {}; + } + auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->length(), byteLength, view->type() != JSC::DataViewType); + RETURN_IF_EXCEPTION(scope, {}); + if (!checkedOffset) + return {}; + + const uint8_t* address = static_cast(view->vector()) + *checkedOffset; + // Assemble the little-endian byte sequence, then reinterpret (matches the DataView arithmetic + // in lib/internal/buffer.js for every width, including the 3/5/6-byte sign extension). + uint64_t bits = 0; + for (size_t i = 0; i < byteLength; ++i) + bits |= static_cast(address[i]) << (8 * (isLittleEndian ? i : byteLength - 1 - i)); + if constexpr (isSigned) { + unsigned shift = 64 - 8 * byteLength; + int64_t value = static_cast(bits << shift) >> shift; + return JSValue::encode(jsNumber(static_cast(value))); + } else + return JSValue::encode(jsNumber(static_cast(bits))); +} + +template +static JSC::EncodedJSValue bufferWriteVarWidth(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue valueValue = callFrame->argument(0); + JSValue offsetValue = callFrame->argument(1); + JSValue byteLengthValue = callFrame->argument(2); + + if (!byteLengthValue.isNumber()) [[unlikely]] { + // boundsError(byteLength, 6, "byteLength") -> validateNumber(byteLength, "byteLength") + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "byteLength"_s, "number"_s, byteLengthValue); + } + double byteLengthNumber = byteLengthValue.asNumber(); + if (!(byteLengthNumber >= 1 && byteLengthNumber <= 6 && std::floor(byteLengthNumber) == byteLengthNumber)) [[unlikely]] + return throwBufferInvalidByteLength(lexicalGlobalObject, scope, byteLengthValue); + size_t byteLength = static_cast(byteLengthNumber); + + // value = +value: after the byteLength dispatch, as in lib/internal/buffer.js + double number; + if (valueValue.isNumber()) [[likely]] + number = valueValue.asNumber(); + else { + number = valueValue.toNumber(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + } + + // The one-byte writers dispatch to writeU_Int8(), which validates the offset before the value's + // range; wider widths go through checkInt(), which is the other way around. + if (byteLength == 1) { + if (!bufferAccessCheckOffsetType(lexicalGlobalObject, scope, offsetValue)) [[unlikely]] + return {}; + if (!bufferWriteVarWidthCheckValue(lexicalGlobalObject, scope, number, byteLength, isSigned)) [[unlikely]] + return {}; + } else { + if (!bufferWriteVarWidthCheckValue(lexicalGlobalObject, scope, number, byteLength, isSigned)) [[unlikely]] + return {}; + if (!bufferAccessCheckOffsetType(lexicalGlobalObject, scope, offsetValue)) [[unlikely]] + return {}; + } + auto* view = dynamicDowncast(callFrame->thisValue()); + if (!view) [[unlikely]] { + bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); + return {}; + } + // checkBounds(): the offset type was validated above; the range check is boundsError(). + auto checkedOffset = bufferAccessCheckOffsetBounds(lexicalGlobalObject, scope, offsetValue, view->length(), byteLength, view->type() != JSC::DataViewType); + RETURN_IF_EXCEPTION(scope, {}); + if (!checkedOffset) + return {}; + + // The stored bytes are the JS arithmetic of lib/internal/buffer.js: the low 32 bits are + // ToInt32(value); the high bytes (5/6-byte widths) are ToInt32(Math.floor(value * 2 ** -32)); + // for 3 bytes the high byte is Math.floor(value * 2 ** -16). + uint8_t bytes[8] = {}; + auto putLittleEndian = [&](size_t at, uint64_t chunk, size_t width) { + for (size_t i = 0; i < width; ++i) + bytes[at + i] = static_cast(chunk >> (8 * i)); + }; + switch (byteLength) { + case 1: + case 2: + case 4: + putLittleEndian(0, static_cast(JSC::toInt32(number)), byteLength); + break; + case 3: + putLittleEndian(0, static_cast(JSC::toInt32(number)), 2); + putLittleEndian(2, static_cast(JSC::toInt32(std::floor(number / 65536.0))), 1); + break; + case 5: + case 6: + putLittleEndian(0, static_cast(JSC::toInt32(number)), 4); + putLittleEndian(4, static_cast(JSC::toInt32(std::floor(number / 4294967296.0))), byteLength - 4); + break; + } + uint8_t* address = static_cast(view->vector()) + *checkedOffset; + for (size_t i = 0; i < byteLength; ++i) + address[i] = bytes[isLittleEndian ? i : byteLength - 1 - i]; + return JSValue::encode(jsNumber(*checkedOffset + byteLength)); +} + +} // namespace + +JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_readIntLE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { return bufferReadVarWidth(lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_readIntBE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { return bufferReadVarWidth(lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_readUIntLE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { return bufferReadVarWidth(lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_readUIntBE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { return bufferReadVarWidth(lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeIntLE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { return bufferWriteVarWidth(lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeIntBE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { return bufferWriteVarWidth(lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUIntLE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { return bufferWriteVarWidth(lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeUIntBE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { return bufferWriteVarWidth(lexicalGlobalObject, callFrame); } + JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64LE, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = JSC::getVM(lexicalGlobalObject); auto scope = DECLARE_THROW_SCOPE(vm); auto* castedThis = dynamicDowncast(callFrame->thisValue()); - if (!castedThis) [[unlikely]] - return throwVMError(lexicalGlobalObject, scope, "Expected ArrayBufferView"_s); - auto byteLength = castedThis->byteLength(); auto valueVal = callFrame->argument(0); auto offsetVal = callFrame->argument(1); @@ -3049,7 +3545,16 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64LE, (JSGlobalObj if (bigint->sign() && limb - 0x8000000000000000 > 0x7fffffffffffffff) return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "value"_s, ">= -(2n ** 63n) and < 2n ** 63n"_s, valueVal); int64_t value = static_cast(limb); - size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength); + if (!castedThis) [[unlikely]] { + // The offset's type is validated before the receiver in every accessor, so a non-number + // offset still wins over a garbage receiver. + if (!offsetVal.isUndefined() && !offsetVal.isNumber()) + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetVal); + bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); + return {}; + } + size_t byteLength = castedThis->length(); + size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength, castedThis->type() != JSC::DataViewType); RETURN_IF_EXCEPTION(scope, {}); write_int64_le(static_cast(castedThis->vector()) + offset, value); return JSValue::encode(jsNumber(offset + 8)); @@ -3061,9 +3566,6 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64BE, (JSGlobalObj auto scope = DECLARE_THROW_SCOPE(vm); auto* castedThis = dynamicDowncast(callFrame->thisValue()); - if (!castedThis) [[unlikely]] - return throwVMError(lexicalGlobalObject, scope, "Expected ArrayBufferView"_s); - auto byteLength = castedThis->byteLength(); auto valueVal = callFrame->argument(0); auto offsetVal = callFrame->argument(1); @@ -3079,7 +3581,16 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigInt64BE, (JSGlobalObj if (bigint->sign() && limb - 0x8000000000000000 > 0x7fffffffffffffff) return Bun::ERR::OUT_OF_RANGE(scope, lexicalGlobalObject, "value"_s, ">= -(2n ** 63n) and < 2n ** 63n"_s, valueVal); int64_t value = static_cast(limb); - size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength); + if (!castedThis) [[unlikely]] { + // The offset's type is validated before the receiver in every accessor, so a non-number + // offset still wins over a garbage receiver. + if (!offsetVal.isUndefined() && !offsetVal.isNumber()) + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetVal); + bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); + return {}; + } + size_t byteLength = castedThis->length(); + size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength, castedThis->type() != JSC::DataViewType); RETURN_IF_EXCEPTION(scope, {}); write_int64_be(static_cast(castedThis->vector()) + offset, value); return JSValue::encode(jsNumber(offset + 8)); @@ -3091,9 +3602,6 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64LE, (JSGlobalOb auto scope = DECLARE_THROW_SCOPE(vm); auto* castedThis = dynamicDowncast(callFrame->thisValue()); - if (!castedThis) [[unlikely]] - return throwVMError(lexicalGlobalObject, scope, "Expected ArrayBufferView"_s); - auto byteLength = castedThis->byteLength(); auto valueVal = callFrame->argument(0); auto offsetVal = callFrame->argument(1); @@ -3108,7 +3616,16 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64LE, (JSGlobalOb uint64_t value = valueVal.toBigUInt64(lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, {}); - size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength); + if (!castedThis) [[unlikely]] { + // The offset's type is validated before the receiver in every accessor, so a non-number + // offset still wins over a garbage receiver. + if (!offsetVal.isUndefined() && !offsetVal.isNumber()) + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetVal); + bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); + return {}; + } + size_t byteLength = castedThis->length(); + size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength, castedThis->type() != JSC::DataViewType); RETURN_IF_EXCEPTION(scope, {}); write_int64_le(static_cast(castedThis->vector()) + offset, value); return JSValue::encode(jsNumber(offset + 8)); @@ -3120,9 +3637,6 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64BE, (JSGlobalOb auto scope = DECLARE_THROW_SCOPE(vm); auto* castedThis = dynamicDowncast(callFrame->thisValue()); - if (!castedThis) [[unlikely]] - return throwVMError(lexicalGlobalObject, scope, "Expected ArrayBufferView"_s); - auto byteLength = castedThis->byteLength(); auto valueVal = callFrame->argument(0); auto offsetVal = callFrame->argument(1); @@ -3137,7 +3651,16 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferPrototypeFunction_writeBigUInt64BE, (JSGlobalOb uint64_t value = valueVal.toBigUInt64(lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, {}); - size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength); + if (!castedThis) [[unlikely]] { + // The offset's type is validated before the receiver in every accessor, so a non-number + // offset still wins over a garbage receiver. + if (!offsetVal.isUndefined() && !offsetVal.isNumber()) + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "offset"_s, "number"_s, offsetVal); + bufferAccessReceiver(lexicalGlobalObject, scope, callFrame->thisValue()); + return {}; + } + size_t byteLength = castedThis->length(); + size_t offset = validateOffsetBigInt64(lexicalGlobalObject, scope, offsetVal, byteLength, castedThis->type() != JSC::DataViewType); RETURN_IF_EXCEPTION(scope, {}); write_int64_be(static_cast(castedThis->vector()) + offset, value); return JSValue::encode(jsNumber(offset + 8)); @@ -3169,34 +3692,34 @@ static const HashTableValue JSBufferPrototypeTableValues[] { "latin1Write"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_latin1Write, 3 } }, { "offset"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, jsBufferPrototypeOffsetCodeGenerator, 0 } }, { "parent"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, jsBufferPrototypeParentCodeGenerator, 0 } }, - { "readBigInt64"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadBigInt64LECodeGenerator, 1 } }, - { "readBigInt64BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadBigInt64BECodeGenerator, 1 } }, - { "readBigInt64LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadBigInt64LECodeGenerator, 1 } }, - { "readBigUInt64"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadBigUInt64LECodeGenerator, 1 } }, - { "readBigUInt64BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadBigUInt64BECodeGenerator, 1 } }, - { "readBigUInt64LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadBigUInt64LECodeGenerator, 1 } }, - { "readDouble"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadDoubleLECodeGenerator, 1 } }, - { "readDoubleBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadDoubleBECodeGenerator, 1 } }, - { "readDoubleLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadDoubleLECodeGenerator, 1 } }, - { "readFloat"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadFloatLECodeGenerator, 1 } }, - { "readFloatBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadFloatBECodeGenerator, 1 } }, - { "readFloatLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadFloatLECodeGenerator, 1 } }, - { "readInt16"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadInt16LECodeGenerator, 1 } }, - { "readInt16BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadInt16BECodeGenerator, 1 } }, - { "readInt16LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadInt16LECodeGenerator, 1 } }, - { "readInt32"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadInt32LECodeGenerator, 1 } }, - { "readInt32BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadInt32BECodeGenerator, 1 } }, - { "readInt32LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadInt32LECodeGenerator, 1 } }, - { "readInt8"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadInt8CodeGenerator, 2 } }, - { "readIntBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadIntBECodeGenerator, 1 } }, - { "readIntLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadIntLECodeGenerator, 1 } }, - { "readUInt16BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadUInt16BECodeGenerator, 1 } }, - { "readUInt16LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadUInt16LECodeGenerator, 1 } }, - { "readUInt32BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadUInt32BECodeGenerator, 1 } }, - { "readUInt32LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadUInt32LECodeGenerator, 1 } }, - { "readUInt8"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadUInt8CodeGenerator, 1 } }, - { "readUIntBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadUIntBECodeGenerator, 1 } }, - { "readUIntLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeReadUIntLECodeGenerator, 1 } }, + { "readBigInt64"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readBigInt64LE, 1 } }, + { "readBigInt64BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readBigInt64BE, 1 } }, + { "readBigInt64LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readBigInt64LE, 1 } }, + { "readBigUInt64"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readBigUInt64LE, 1 } }, + { "readBigUInt64BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readBigUInt64BE, 1 } }, + { "readBigUInt64LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readBigUInt64LE, 1 } }, + { "readDouble"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readDoubleLE, 1 } }, + { "readDoubleBE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readDoubleBE, 1 } }, + { "readDoubleLE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readDoubleLE, 1 } }, + { "readFloat"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readFloatLE, 1 } }, + { "readFloatBE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readFloatBE, 1 } }, + { "readFloatLE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readFloatLE, 1 } }, + { "readInt16"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt16LE, 1 } }, + { "readInt16BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt16BE, 1 } }, + { "readInt16LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt16LE, 1 } }, + { "readInt32"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt32LE, 1 } }, + { "readInt32BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt32BE, 1 } }, + { "readInt32LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt32LE, 1 } }, + { "readInt8"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readInt8, 1 } }, + { "readIntBE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readIntBE, 2 } }, + { "readIntLE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readIntLE, 2 } }, + { "readUInt16BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUInt16BE, 1 } }, + { "readUInt16LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUInt16LE, 1 } }, + { "readUInt32BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUInt32BE, 1 } }, + { "readUInt32LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUInt32LE, 1 } }, + { "readUInt8"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUInt8, 1 } }, + { "readUIntBE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUIntBE, 2 } }, + { "readUIntLE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_readUIntLE, 2 } }, { "slice"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_slice, 2 } }, { "subarray"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_slice, 2 } }, @@ -3213,32 +3736,32 @@ static const HashTableValue JSBufferPrototypeTableValues[] { "utf8Slice"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_utf8Slice, 2 } }, { "utf8Write"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_utf8Write, 3 } }, { "write"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_write, 4 } }, - { "writeBigInt64BE"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeBigInt64BE, 3 } }, - { "writeBigInt64LE"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeBigInt64LE, 3 } }, - { "writeBigUInt64BE"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeBigUInt64BE, 3 } }, - { "writeBigUInt64LE"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeBigUInt64LE, 3 } }, - { "writeDouble"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteDoubleLECodeGenerator, 1 } }, - { "writeDoubleBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteDoubleBECodeGenerator, 1 } }, - { "writeDoubleLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteDoubleLECodeGenerator, 1 } }, - { "writeFloat"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteFloatLECodeGenerator, 1 } }, - { "writeFloatBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteFloatBECodeGenerator, 1 } }, - { "writeFloatLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteFloatLECodeGenerator, 1 } }, - { "writeInt16BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteInt16BECodeGenerator, 1 } }, - { "writeInt16LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteInt16LECodeGenerator, 1 } }, - { "writeInt32BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteInt32BECodeGenerator, 1 } }, - { "writeInt32LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteInt32LECodeGenerator, 1 } }, - { "writeInt8"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteInt8CodeGenerator, 1 } }, - { "writeIntBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteIntBECodeGenerator, 1 } }, - { "writeIntLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteIntLECodeGenerator, 1 } }, - { "writeUInt16"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUInt16LECodeGenerator, 1 } }, - { "writeUInt16BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUInt16BECodeGenerator, 1 } }, - { "writeUInt16LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUInt16LECodeGenerator, 1 } }, - { "writeUInt32"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUInt32LECodeGenerator, 1 } }, - { "writeUInt32BE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUInt32BECodeGenerator, 1 } }, - { "writeUInt32LE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUInt32LECodeGenerator, 1 } }, - { "writeUInt8"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUInt8CodeGenerator, 1 } }, - { "writeUIntBE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUIntBECodeGenerator, 1 } }, - { "writeUIntLE"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, jsBufferPrototypeWriteUIntLECodeGenerator, 1 } }, + { "writeBigInt64BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeBigInt64BE, 3 } }, + { "writeBigInt64LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeBigInt64LE, 3 } }, + { "writeBigUInt64BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeBigUInt64BE, 3 } }, + { "writeBigUInt64LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeBigUInt64LE, 3 } }, + { "writeDouble"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeDoubleLE, 2 } }, + { "writeDoubleBE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeDoubleBE, 2 } }, + { "writeDoubleLE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeDoubleLE, 2 } }, + { "writeFloat"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeFloatLE, 2 } }, + { "writeFloatBE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeFloatBE, 2 } }, + { "writeFloatLE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeFloatLE, 2 } }, + { "writeInt16BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeInt16BE, 2 } }, + { "writeInt16LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeInt16LE, 2 } }, + { "writeInt32BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeInt32BE, 2 } }, + { "writeInt32LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeInt32LE, 2 } }, + { "writeInt8"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeInt8, 2 } }, + { "writeIntBE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeIntBE, 3 } }, + { "writeIntLE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeIntLE, 3 } }, + { "writeUInt16"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt16LE, 2 } }, + { "writeUInt16BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt16BE, 2 } }, + { "writeUInt16LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt16LE, 2 } }, + { "writeUInt32"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt32LE, 2 } }, + { "writeUInt32BE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt32BE, 2 } }, + { "writeUInt32LE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt32LE, 2 } }, + { "writeUInt8"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUInt8, 2 } }, + { "writeUIntBE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUIntBE, 3 } }, + { "writeUIntLE"_s, static_cast(JSC::PropertyAttribute::Function), JSC::BufferAccessorIntrinsic, { HashTableValue::NativeFunctionType, jsBufferPrototypeFunction_writeUIntLE, 3 } }, }; // TODO: add this as a feature to the hash table generator mechanism above so that we can avoid all the unnecessary extra calls to `Identifier::fromString` and `this->getDirect`. @@ -3250,10 +3773,92 @@ static const HashTableValue JSBufferPrototypeTableValues[] this->putDirect(vm, alias_ident, original, PropertyAttribute::Builtin | 0); \ } while (false); +// The accessor descriptor JSC's DFG needs, derived from the same template arguments the host +// function is instantiated with so the two cannot disagree. +template +static void registerBufferAccessor(JSC::NativeFunction function) +{ + JSC::DFG::DataViewData data {}; + data.byteSize = sizeof(T); + data.isSigned = std::is_signed_v && !std::is_floating_point_v; + data.isFloatingPoint = std::is_floating_point_v; + data.isResizable = false; + data.isLittleEndian = triState(isLittleEndian); + JSC::registerBufferAccessor(JSC::toTagged(function), { data, isWrite }); +} + +// read(U)Int{LE,BE} / write(U)Int{LE,BE}: the width comes from the byteLength argument, so the JIT +// only inlines call sites whose byteLength is a constant 1, 2 or 4. +template +static void registerBufferVarWidthAccessor(JSC::NativeFunction function) +{ + JSC::DFG::DataViewData data {}; + data.byteSize = 0; + data.isSigned = isSigned; + data.isFloatingPoint = false; + data.isResizable = false; + data.isLittleEndian = triState(isLittleEndian); + JSC::registerBufferAccessor(JSC::toTagged(function), { data, isWrite, /* byteLengthFromArgument */ true }); +} + +// Process-global (the descriptor belongs to the function pointer): once, before the functions are +// reachable from JS. +static void registerBufferAccessorsWithJSC() +{ + static std::once_flag registered; + std::call_once(registered, [] { + registerBufferAccessor(jsBufferPrototypeFunction_readInt8); + registerBufferAccessor(jsBufferPrototypeFunction_readUInt8); + registerBufferAccessor(jsBufferPrototypeFunction_readInt16LE); + registerBufferAccessor(jsBufferPrototypeFunction_readInt16BE); + registerBufferAccessor(jsBufferPrototypeFunction_readUInt16LE); + registerBufferAccessor(jsBufferPrototypeFunction_readUInt16BE); + registerBufferAccessor(jsBufferPrototypeFunction_readInt32LE); + registerBufferAccessor(jsBufferPrototypeFunction_readInt32BE); + registerBufferAccessor(jsBufferPrototypeFunction_readUInt32LE); + registerBufferAccessor(jsBufferPrototypeFunction_readUInt32BE); + registerBufferAccessor(jsBufferPrototypeFunction_readFloatLE); + registerBufferAccessor(jsBufferPrototypeFunction_readFloatBE); + registerBufferAccessor(jsBufferPrototypeFunction_readDoubleLE); + registerBufferAccessor(jsBufferPrototypeFunction_readDoubleBE); + registerBufferAccessor(jsBufferPrototypeFunction_readBigInt64LE); + registerBufferAccessor(jsBufferPrototypeFunction_readBigInt64BE); + registerBufferAccessor(jsBufferPrototypeFunction_readBigUInt64LE); + registerBufferAccessor(jsBufferPrototypeFunction_readBigUInt64BE); + registerBufferAccessor(jsBufferPrototypeFunction_writeInt8); + registerBufferAccessor(jsBufferPrototypeFunction_writeUInt8); + registerBufferAccessor(jsBufferPrototypeFunction_writeInt16LE); + registerBufferAccessor(jsBufferPrototypeFunction_writeInt16BE); + registerBufferAccessor(jsBufferPrototypeFunction_writeUInt16LE); + registerBufferAccessor(jsBufferPrototypeFunction_writeUInt16BE); + registerBufferAccessor(jsBufferPrototypeFunction_writeInt32LE); + registerBufferAccessor(jsBufferPrototypeFunction_writeInt32BE); + registerBufferAccessor(jsBufferPrototypeFunction_writeUInt32LE); + registerBufferAccessor(jsBufferPrototypeFunction_writeUInt32BE); + registerBufferAccessor(jsBufferPrototypeFunction_writeFloatLE); + registerBufferAccessor(jsBufferPrototypeFunction_writeFloatBE); + registerBufferAccessor(jsBufferPrototypeFunction_writeDoubleLE); + registerBufferAccessor(jsBufferPrototypeFunction_writeDoubleBE); + registerBufferAccessor(jsBufferPrototypeFunction_writeBigInt64LE); + registerBufferAccessor(jsBufferPrototypeFunction_writeBigInt64BE); + registerBufferAccessor(jsBufferPrototypeFunction_writeBigUInt64LE); + registerBufferAccessor(jsBufferPrototypeFunction_writeBigUInt64BE); + registerBufferVarWidthAccessor(jsBufferPrototypeFunction_readIntLE); + registerBufferVarWidthAccessor(jsBufferPrototypeFunction_readIntBE); + registerBufferVarWidthAccessor(jsBufferPrototypeFunction_readUIntLE); + registerBufferVarWidthAccessor(jsBufferPrototypeFunction_readUIntBE); + registerBufferVarWidthAccessor(jsBufferPrototypeFunction_writeIntLE); + registerBufferVarWidthAccessor(jsBufferPrototypeFunction_writeIntBE); + registerBufferVarWidthAccessor(jsBufferPrototypeFunction_writeUIntLE); + registerBufferVarWidthAccessor(jsBufferPrototypeFunction_writeUIntBE); + }); +} + void JSBufferPrototype::finishCreation(VM& vm, JSC::JSGlobalObject* globalThis) { Base::finishCreation(vm); JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); + registerBufferAccessorsWithJSC(); reifyStaticProperties(vm, JSBuffer::info(), JSBufferPrototypeTableValues, *this); ALIAS("toLocaleString", "toString"); diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index a45cf0016dfe..6cfe4b027885 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -2969,40 +2969,6 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionToClass, (JSC::JSGlobalObject * globalObject, return JSValue::encode(jsUndefined()); } -JSC_DEFINE_HOST_FUNCTION(jsFunctionCheckBufferRead, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - - auto bufVal = callFrame->argument(0); - auto offsetVal = callFrame->argument(1); - auto byteLengthVal = callFrame->argument(2); - - // Mirrors Node's read-path validation (lib/internal/buffer.js): validateNumber - // on the offset, then boundsError(). A non-integer offset (NaN, 1.01) reports - // "an integer", but a finite-floor value like Infinity or a negative integer - // reports the ">= 0 and <= N" range, matching boundsError's MathFloor check. - if (!offsetVal.isNumber()) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "offset"_s, "number"_s, offsetVal); - double offset = offsetVal.asNumber(); - - if (!bufVal.isCell()) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "buf"_s, "Buffer"_s, bufVal); - auto* buf = dynamicDowncast(bufVal.asCell()); - if (!buf) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "buf"_s, "Buffer"_s, bufVal); - size_t byteLength = byteLengthVal.asNumber(); - ssize_t type = ((ssize_t)buf->length()) - byteLength; - - // A non-integer offset (NaN, 1.01) is "an integer" even when numerically - // within bounds — the caller only reaches here because buf[offset] was - // undefined, which for a fractional index happens regardless of bounds. - if (std::floor(offset) != offset) { - return Bun::ERR::OUT_OF_RANGE(scope, globalObject, "offset"_s, "an integer"_s, offsetVal); - } - if (!(offset >= 0 && offset <= (double)type)) { - if (type < 0) return Bun::ERR::BUFFER_OUT_OF_BOUNDS(scope, globalObject, ""_s); - return Bun::ERR::OUT_OF_RANGE(scope, globalObject, "offset"_s, makeString(">= 0 and <= "_s, type), offsetVal); - } - return JSValue::encode(jsUndefined()); -} EncodedJSValue GlobalObject::assignToStream(JSValue stream, JSValue controller) { auto& vm = this->vm(); @@ -3102,7 +3068,6 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm) GlobalPropertyInfo(builtinNames.toClassPrivateName(), JSFunction::create(vm, this, 1, String(), jsFunctionToClass, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), GlobalPropertyInfo(builtinNames.inheritsPrivateName(), JSFunction::create(vm, this, 1, String(), jsFunctionInherits, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), GlobalPropertyInfo(builtinNames.makeAbortErrorPrivateName(), JSFunction::create(vm, this, 1, String(), jsFunctionMakeAbortError, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), - GlobalPropertyInfo(builtinNames.checkBufferReadPrivateName(), JSFunction::create(vm, this, 1, String(), jsFunctionCheckBufferRead, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), }; addStaticGlobals(staticGlobals); diff --git a/test/js/node/buffer-jit.test.ts b/test/js/node/buffer-jit.test.ts new file mode 100644 index 000000000000..c8d6ccf2c0d0 --- /dev/null +++ b/test/js/node/buffer-jit.test.ts @@ -0,0 +1,525 @@ +// JIT behavior of Buffer.prototype.read* / write*: these are native functions that JSC's DFG/FTL +// compile into bounds-checked loads/stores on the receiver's storage (JSBuffer.cpp + +// JavaScriptCore's BufferAccessorRegistry). Plain Buffer semantics live in buffer.test.js; this +// file pins the *compiler* behavior: that the JIT path is really taken and converges, that every +// speculation failure lands back on the correct host behavior, that loads and stores are not +// mis-ordered or mis-CSE'd, and that swapping the method is respected. +// +// It runs each scenario in a fresh subprocess with the concurrent JIT off and a deterministic tier-up +// policy, so numberOfDFGCompiles() is meaningful. +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +async function run(source: string, extraEnv: Record = {}) { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", source], + env: { + ...bunEnv, + BUN_JSC_useConcurrentJIT: "0", + // Tier up quickly and deterministically, but not so eagerly that profiling is skipped. + BUN_JSC_jitPolicyScale: "0.05", + ...extraEnv, + }, + stderr: "pipe", + stdout: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr, exitCode }; +} + +// Shared prelude: helpers + a deterministic buffer. +const prelude = ` +const { numberOfDFGCompiles, noInline } = require("bun:jsc"); +function assert(condition, message) { if (!condition) throw new Error("Assertion failed: " + message); } +const buf = Buffer.alloc(256); +const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); +for (let i = 0; i < buf.length; i++) buf[i] = (i * 37 + 11) & 0xff; +const N = 20000; +`; + +describe.concurrent("Buffer accessor JIT", () => { + test("the JIT path is actually taken, and compile counts converge", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + function read(b, o) { return b.readInt32LE(o); } + function write(b, v, o) { return b.writeInt32LE(v, o); } + noInline(read); noInline(write); + for (let i = 0; i < N; i++) { + assert(read(buf, i & 63) === dv.getInt32(i & 63, true), "read"); + assert(write(buf, i, (i & 63) + 128) === (i & 63) + 132, "write"); + assert(dv.getInt32((i & 63) + 128, true) === i, "write store"); + } + const readCompiles = numberOfDFGCompiles(read); + const writeCompiles = numberOfDFGCompiles(write); + // Compiled at least once (the intrinsic did not stop the DFG from taking these), and no + // OSR-exit -> recompile storm: a well-behaved call site converges in a handful of compiles. + assert(readCompiles >= 1 && readCompiles <= 4, "read compiles: " + readCompiles); + assert(writeCompiles >= 1 && writeCompiles <= 4, "write compiles: " + writeCompiles); + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }, 30_000); + + test("each speculation exit converges instead of looping", async () => { + // Warm up on the fast path, then keep triggering one exit kind at the same call site: the + // site must fall back to a stable state (bounded recompiles), not exit -> recompile forever. + const { stdout, stderr, exitCode } = await run( + prelude + + ` + function readAt(b, o) { return b.readInt32LE(o); } + noInline(readAt); + function writeAt(b, v, o) { return b.writeInt8(v, o); } + noInline(writeAt); + for (let i = 0; i < N; i++) { + readAt(buf, i & 63); + writeAt(buf, i & 127, i & 63); + } + // Carries the real accessor, so the call reaches it and fails the receiver check itself + // (rather than throwing "not a function" at the property lookup). + const badReceiver = { length: 4, readInt32LE: Buffer.prototype.readInt32LE }; + const detached = Buffer.alloc(8); + structuredClone(detached.buffer, { transfer: [detached.buffer] }); + const exits = [ + () => readAt(buf, buf.length), // out of bounds + () => readAt(buf, -1), // negative offset + () => readAt(buf, 1.5), // fractional offset + () => readAt(buf, "4"), // wrong offset type + () => readAt(badReceiver, 0), // wrong receiver: a plain object + () => readAt(detached, 0), // detached + () => writeAt(buf, 200, 0), // value out of int8 range + () => writeAt(buf, 1.5, 0), // fractional value (host truncates; no throw) + ]; + for (const trigger of exits) { + for (let i = 0; i < 2000; i++) { + try { trigger(); } catch {} + readAt(buf, i & 63); // and the fast path keeps working in between + } + } + const compiles = Math.max(numberOfDFGCompiles(readAt), numberOfDFGCompiles(writeAt)); + assert(compiles <= 8, "compile count did not converge: " + compiles); + assert(readAt(buf, 12) === dv.getInt32(12, true), "still correct after all the exits"); + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }, 30_000); + + test("host semantics survive every exit path (results, not just compile counts)", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + function read(b, o) { return b.readUInt16BE(o); } + function write(b, v, o) { return b.writeUInt16LE(v, o); } + noInline(read); noInline(write); + for (let i = 0; i < N; i++) { read(buf, i & 63); write(buf, i & 0xffff, (i & 63) + 128); } + // Non-int32 but valid inputs the JIT does not speculate on must produce the host result. + assert(read(buf, 4.0) === dv.getUint16(4, false), "integral double offset"); + const scratch = Buffer.alloc(8); + assert(scratch.writeUInt16LE(1.5, 0) === 2, "fractional value returns offset + 2"); + assert(scratch.readUInt16LE(0) === 1, "fractional value truncates"); + assert(scratch.writeUInt16LE(NaN, 0) === 2 && scratch.readUInt16LE(0) === 0, "NaN stores 0"); + assert(scratch.writeUInt16LE({ valueOf() { return 7; } }, 2) === 4 && scratch.readUInt16LE(2) === 7, "valueOf value"); + let coerced = 0; + const counting = { valueOf() { coerced++; return 300; } }; + for (let i = 0; i < 1000; i++) { + try { scratch.writeInt8(counting, 0); } catch (e) { assert(e.code === "ERR_OUT_OF_RANGE", "range code"); } + try { scratch.writeInt8(counting, 100); } catch (e) { assert(e.code === "ERR_OUT_OF_RANGE", "value checked before offset for 1-byte writes? no: offset first"); } + } + assert(coerced === 2000, "value coerced exactly once per call, even when it then throws: " + coerced); + // The BigInt writers: too-wide BigInts throw; the widest valid ones store. + // NaN and +-Infinity, which the value range check treats differently (Node stores 0 for NaN + // but throws for the infinities), must keep doing so once the write is JIT-compiled. + for (let i = 0; i < 2000; i++) { + assert(scratch.writeInt8(NaN, 0) === 1 && scratch.readInt8(0) === 0, "NaN stores 0 (int8)"); + assert(scratch.writeUInt16LE(NaN, 0) === 2 && scratch.readUInt16LE(0) === 0, "NaN stores 0 (uint16)"); + assert(scratch.writeUIntLE(NaN, 0, 3) === 3 && scratch.readUIntLE(0, 3) === 0, "NaN stores 0 (var width)"); + for (const [f, what] of [[() => scratch.writeInt8(Infinity, 0), "int8 +Inf"], [() => scratch.writeInt8(-Infinity, 0), "int8 -Inf"], [() => scratch.writeIntLE(Infinity, 0, 4), "var-width +Inf"]]) { + try { f(); assert(false, what + " should throw"); } catch (e) { assert(e.code === "ERR_OUT_OF_RANGE", what + ": " + e.code); } + } + } + + const bb = Buffer.alloc(8); + const bd = new DataView(bb.buffer, bb.byteOffset, 8); + for (let i = 0; i < 2000; i++) { + assert(bb.writeBigInt64LE(-(2n ** 63n), 0) === 8 && bd.getBigInt64(0, true) === -(2n ** 63n), "int64 min"); + assert(bb.writeBigUInt64BE(2n ** 64n - 1n, 0) === 8 && bd.getBigUint64(0, false) === 2n ** 64n - 1n, "uint64 max"); + try { bb.writeBigInt64LE(2n ** 63n, 0); assert(false, "should throw"); } catch (e) { assert(e.code === "ERR_OUT_OF_RANGE", "int64 too big"); } + try { bb.writeBigUInt64LE(-1n, 0); assert(false, "should throw"); } catch (e) { assert(e.code === "ERR_OUT_OF_RANGE", "uint64 negative"); } + } + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }, 30_000); + + test("no stale reads: a store between two loads of the same offset is observed", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + function readWriteRead(b, o, v) { + const before = b.readInt32LE(o); + b.writeInt32LE(v, o); + const after = b.readInt32LE(o); // must not be CSE'd with 'before' + return before * 3 + after; // use both so neither is dead + } + noInline(readWriteRead); + for (let i = 0; i < N; i++) { + const o = (i & 31) * 4; + const before = dv.getInt32(o, true); + assert(readWriteRead(buf, o, i) === before * 3 + i, "write between reads at iteration " + i); + } + // Same, but the store goes through a plain typed-array element write and a DataView. + function readAroundOtherStores(b, o, v) { + const a = b.readUInt8(o); + b[o] = v & 0xff; + const c = b.readUInt8(o); + dv.setUint8(o, (v + 1) & 0xff); + const d = b.readUInt8(o); + return [a, c, d]; + } + noInline(readAroundOtherStores); + for (let i = 0; i < N; i++) { + const o = i & 127; + const a0 = buf[o]; + const [a, c, d] = readAroundOtherStores(buf, o, i); + assert(a === a0 && c === (i & 0xff) && d === ((i + 1) & 0xff), "typed array / DataView stores observed at " + i); + } + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }, 30_000); + + test("two Buffers over the same ArrayBuffer are never mis-aliased", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + const backing = new ArrayBuffer(64); + const a = Buffer.from(backing); // views over the SAME memory + const b = Buffer.from(backing, 8, 32); + const raw = new Uint8Array(backing); + function crossViewReadAfterWrite(x) { + const before = a.readInt32LE(8); // a[8..12) is b[0..4) + b.writeInt32LE(x, 0); // store through the other view + const after = a.readInt32LE(8); // must observe it: no CSE across the store + return { before, after }; + } + noInline(crossViewReadAfterWrite); + let previous = raw[8] | (raw[9] << 8) | (raw[10] << 16) | (raw[11] << 24); + for (let i = 0; i < N; i++) { + const { before, after } = crossViewReadAfterWrite(i); + assert(before === previous && after === i, "cross-view store observed at " + i + ": " + before + "/" + after); + previous = i; + } + // Overlapping views + a loop the compiler will try to hoist loads out of. + function sumWhileWriting(iters) { + let sum = 0; + for (let i = 0; i < iters; i++) { + sum += a.readUInt8(12); + b.writeUInt8((sum + i) & 0xff, 4); // b[4] is a[12]: the load above cannot be hoisted + } + return sum; + } + noInline(sumWhileWriting); + a.writeUInt8(3, 12); + let expected = 0, cell = 3; + for (let i = 0; i < 500; i++) { expected += cell; cell = (expected + i) & 0xff; } + for (let i = 0; i < 200; i++) { + a.writeUInt8(3, 12); + assert(sumWhileWriting(500) === expected, "loop with aliasing store"); + } + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }, 30_000); + + test("a loop-carried load is not kept alive across a call that mutates the buffer", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + let mutations = 0; + function mutate(b) { b.writeUInt8((++mutations) & 0xff, 0); } + noInline(mutate); + function readAroundCall(b, iters) { + let last = 0; + for (let i = 0; i < iters; i++) { + const v = b.readUInt8(0); // loop-invariant address, but the call below may write it + mutate(b); + last = v; + } + return last; + } + noInline(readAroundCall); + for (let i = 0; i < 300; i++) { + buf.writeUInt8(200, 0); + mutations = 0; + const last = readAroundCall(buf, 100); + assert(last === (99 & 0xff), "the read is re-done each iteration after the call: got " + last); + } + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }, 30_000); + + test("replacing or shadowing the method after tier-up takes effect", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + function readViaMethod(b, o) { return b.readInt32LE(o); } + noInline(readViaMethod); + for (let i = 0; i < N; i++) assert(readViaMethod(buf, i & 63) === dv.getInt32(i & 63, true), "warm"); + + // 1. Shadow on one instance: only that receiver changes behavior. + const special = Buffer.alloc(16); + special.readInt32LE = function () { return 424242; }; + for (let i = 0; i < 5000; i++) { + assert(readViaMethod(special, 0) === 424242, "instance shadow at " + i); + assert(readViaMethod(buf, 4) === dv.getInt32(4, true), "normal buffer unaffected at " + i); + } + + // 2. Replace on the prototype: every receiver changes behavior, immediately. + const original = Buffer.prototype.readInt32LE; + Buffer.prototype.readInt32LE = function (o) { return -original.call(this, o) - 1; }; + for (let i = 0; i < 5000; i++) { + assert(readViaMethod(buf, i & 63) === -dv.getInt32(i & 63, true) - 1, "prototype replaced at " + i); + } + Buffer.prototype.readInt32LE = original; + for (let i = 0; i < 5000; i++) { + assert(readViaMethod(buf, i & 63) === dv.getInt32(i & 63, true), "prototype restored at " + i); + } + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }, 30_000); + + test("resizable and growable receivers keep tracking the length after tier-up", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + function readEnd(b) { return b.readUInt16LE(b.length - 2); } + function readAt(b, o) { return b.readUInt16LE(o); } + noInline(readEnd); noInline(readAt); + // Warm on fixed-size buffers first so the resizable ones arrive after optimization. + for (let i = 0; i < N; i++) { readEnd(buf); readAt(buf, i & 63); } + + const rab = new ArrayBuffer(16, { maxByteLength: 128 }); + const tracking = Buffer.from(rab); // length-tracking view + tracking.writeUInt16LE(0xabcd, 14); + for (let i = 0; i < 5000; i++) assert(readEnd(tracking) === 0xabcd, "before grow"); + rab.resize(128); + tracking.writeUInt16LE(0x1234, 126); + for (let i = 0; i < 5000; i++) { + assert(readEnd(tracking) === 0x1234, "after grow"); + assert(readAt(tracking, 126) === 0x1234, "read into the grown region"); + } + rab.resize(8); + for (let i = 0; i < 2000; i++) { + try { readAt(tracking, 14); assert(false, "must throw after shrink"); } + catch (e) { assert(e.code === "ERR_OUT_OF_RANGE" || e.code === "ERR_BUFFER_OUT_OF_BOUNDS", "shrink error: " + e.code); } + } + + const gsab = new SharedArrayBuffer(16, { maxByteLength: 128 }); + const shared = Buffer.from(gsab); + shared.writeUInt16LE(0x5678, 14); + for (let i = 0; i < 5000; i++) assert(readEnd(shared) === 0x5678, "shared before grow"); + gsab.grow(128); + shared.writeUInt16LE(0x9abc, 126); + for (let i = 0; i < 5000; i++) assert(readEnd(shared) === 0x9abc, "shared after grow"); + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }, 30_000); + + // Differential fuzzer: an identical seeded operation stream runs once with the JIT and once with + // BUN_JSC_useJIT=0; the two traces (return values, error codes/messages, and the buffer bytes + // after every write) must match. Ported from JSTests/stress/buffer-accessor-jit-differential.js. + const fuzzerSource = ` + let seed = 0x9e3779b1; + function rand() { seed ^= seed << 13; seed |= 0; seed ^= seed >>> 17; seed ^= seed << 5; seed |= 0; return (seed >>> 0) / 4294967296; } + function pick(list) { return list[(rand() * list.length) | 0]; } + function randInt(lo, hi) { return lo + ((rand() * (hi - lo + 1)) | 0); } + const names = Object.getOwnPropertyNames(Buffer.prototype).filter(n => /^(read|write)(U?Int|Float|Double|Big)/.test(n)); + const readers = names.filter(n => n.startsWith("read")), writers = names.filter(n => n.startsWith("write")); + function describeName(name) { + const isWrite = name.startsWith("write"), isFloat = /Float|Double/.test(name), isBigInt = /Big/.test(name); + const isVarWidth = /Int(LE|BE)$/.test(name) && !/(8|16|32|64)/.test(name), isSigned = !/UInt/.test(name); + const byteSize = /Double/.test(name) ? 8 : /Float/.test(name) ? 4 : isBigInt ? 8 : isVarWidth ? 0 : Number(name.match(/(8|16|32|64)/)[0]) / 8; + return { isWrite, isFloat, isBigInt, isVarWidth, isSigned, byteSize }; + } + function cleanValue(shape, size) { + if (shape.isBigInt) return pick(shape.isSigned ? [-(2n ** 63n), 2n ** 63n - 1n, 0n, -1n, BigInt(randInt(-1e6, 1e6))] : [0n, 2n ** 64n - 1n, 12345678901234567890n, BigInt(randInt(0, 1e6))]); + if (shape.isFloat) return pick([() => rand() * 1e6 - 5e5, () => Math.fround(rand() * 100), () => -0, () => Infinity, () => 2 ** -1074, () => 1e300])(); + const min = shape.isSigned ? -(2 ** (8 * size - 1)) : 0, max = shape.isSigned ? 2 ** (8 * size - 1) - 1 : 2 ** (8 * size) - 1; + return pick([() => min, () => max, () => randInt(min, max), () => randInt(min, max), () => 0])(); + } + const dirtyValue = () => pick([() => (rand() * 2 ** 32) | 0, () => -((rand() * 2 ** 31) | 0), () => 2 ** 31, () => 2 ** 32, () => -(2 ** 31) - 1, + () => rand() * 1e6 - 5e5, () => 0.5, () => -0.5, () => -0, () => NaN, () => Infinity, () => -Infinity, () => 2 ** 53 + 1, + () => "42", () => "abc", () => "", () => true, () => false, () => null, () => undefined, + () => 5n, () => 2n ** 63n, () => 2n ** 64n, () => -1n, () => -(2n ** 63n) - 1n, () => Symbol("v")])(); + const dirtyOffset = length => pick([() => randInt(0, length + 3), () => -randInt(1, 8), () => length - randInt(0, 8), () => rand() * length, + () => -0, () => 2 ** 31 + randInt(0, 8), () => 2 ** 32, () => 2 ** 53 + 2, () => NaN, () => Infinity, () => -Infinity, + () => undefined, () => null, () => String(randInt(0, length)), () => "not a number", () => true, () => Symbol("s"), () => 3n])(); + const dirtyByteLength = () => pick([() => randInt(1, 6), () => 0, () => 7, () => -1, () => 2.5, () => NaN, () => "4", () => undefined, () => 9n])(); + function makeReceiver() { + return pick([() => Buffer.alloc(32), () => Buffer.from(new ArrayBuffer(64), 8, 24), () => Buffer.alloc(7), + () => Buffer.from(new ArrayBuffer(16, { maxByteLength: 64 })), + () => Buffer.from(new ArrayBuffer(48, { maxByteLength: 64 }), 8, 16)])(); + } + function makeInvoker(name) { + if (!/^[A-Za-z0-9]+$/.test(name)) throw new Error("bad name " + name); + return new Function("return function invoke_" + name + "(receiver, args, box) { try { let result;" + + " switch (args.length) { case 0: result = receiver." + name + "(); break;" + + " case 1: result = receiver." + name + "(args[0]); break;" + + " case 2: result = receiver." + name + "(args[0], args[1]); break;" + + " default: result = receiver." + name + "(args[0], args[1], args[2]); break; }" + + " box.value = result; box.error = null; } catch (e) { box.value = undefined;" + + " box.error = e === null || typeof e !== 'object' ? 'throw:' + String(e) : 'throw:' + e.constructor.name + ':' + (e.code === undefined ? '' : e.code) + ':' + e.message; } };")(); + } + const invokers = new Map(); + const invokerFor = name => { let f = invokers.get(name); if (!f) invokers.set(name, (f = makeInvoker(name))); return f; }; + const fmt = v => typeof v === "bigint" ? v + "n" : typeof v === "symbol" ? "Symbol" : Object.is(v, -0) ? "-0" : String(v); + let digest = 0x811c9dc5; + const mix = str => { for (let i = 0; i < str.length; i++) { digest ^= str.charCodeAt(i); digest = Math.imul(digest, 0x01000193); } }; + const box = { value: undefined, error: null }; + let ops = 0; + for (let round = 0; round < 40; ++round) { + const receiver = makeReceiver(); + const name = pick(rand() < 0.5 ? readers : writers); + const shape = describeName(name), clean = rand() < 0.6, invoke = invokerFor(name); + const width = shape.isVarWidth ? randInt(1, 6) : shape.byteSize; + let resizeCountdown = clean ? Infinity : 100 + randInt(0, 400); + for (let step = 0; step < 850; ++step) { + const args = []; + const maxOffset = receiver.length - width; + if (clean) { + if (maxOffset < 0) break; + if (shape.isWrite) args.push(cleanValue(shape, width)); + args.push(randInt(0, maxOffset)); + if (shape.isVarWidth) args.push(width); + } else { + if (shape.isWrite) args.push(dirtyValue()); + if (rand() < 0.9 || shape.isVarWidth) args.push(dirtyOffset(receiver.length)); + if (shape.isVarWidth) args.push(dirtyByteLength()); + while (args.length && args[args.length - 1] === undefined && rand() < 0.3) args.pop(); + if (args.length && typeof args[args.length - 1] === "symbol" && rand() < 0.5) args[args.length - 1] = 0; + } + invoke(receiver, args, box); + ops++; + let bytes; + try { bytes = Array.prototype.join.call(receiver, ","); } catch { bytes = ""; } + mix(name + "|" + args.map(fmt).join(",") + "=>" + (box.error === null ? fmt(box.value) : box.error) + "|" + bytes); + if (--resizeCountdown === 0) { + resizeCountdown = 100 + randInt(0, 400); + const ab = receiver.buffer; + if (typeof ab.resize === "function" && ab.resizable) { try { ab.resize(randInt(0, ab.maxByteLength)); } catch {} } + } + } + } + console.log("digest=" + (digest >>> 0).toString(16) + " ops=" + ops); + `; + + test("differential fuzzer: JIT and useJIT=0 agree on every result, error and byte", async () => { + const [jit, reference] = await Promise.all([run(fuzzerSource), run(fuzzerSource, { BUN_JSC_useJIT: "0" })]); + expect(jit.stderr).toBe(""); + expect(reference.stderr).toBe(""); + expect(jit.exitCode).toBe(0); + expect(reference.exitCode).toBe(0); + const parse = (out: string) => + Object.fromEntries( + out + .trim() + .split(/\s+/) + .map(kv => kv.split("=")), + ); + const jitResult = parse(jit.stdout), + referenceResult = parse(reference.stdout); + // A meaningful volume actually ran, and the JIT arm reproduces the interpreter's trace exactly. + expect(Number(referenceResult.ops)).toBeGreaterThan(20_000); + expect(jitResult.ops).toBe(referenceResult.ops); + expect(jitResult.digest).toBe(referenceResult.digest); + }, 120_000); + + test("a >2GB receiver stays optimized: no exit storm, and OOB still throws", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + let big; + try { big = new Uint8Array(3 * 2**30); } catch { console.log("OK"); process.exit(0); } + Object.setPrototypeOf(big, Buffer.prototype); + const small = Buffer.alloc(64); + function readAt(b, o) { return b.readInt32LE(o); } + function writeAt(b, v, o) { return b.writeInt32LE(v, o); } + noInline(readAt); noInline(writeAt); + const top = 2 ** 31 - 4; + for (let i = 0; i < N * 10; i++) { + assert(writeAt(big, i, 100) === 104, "write low"); + assert(readAt(big, 100) === i, "read low"); + assert(writeAt(big, ~i, top) === top + 4, "write at the int32 offset ceiling"); + assert(readAt(big, top) === ~i, "read at the int32 offset ceiling"); + assert(writeAt(small, i, 60) === 64 && readAt(small, 60) === i, "the same site with a small receiver"); + } + const compiles = Math.max(numberOfDFGCompiles(readAt), numberOfDFGCompiles(writeAt)); + assert(compiles <= 3, "the large receiver caused recompiles: " + compiles); + let threw = 0; + for (let i = 0; i < 200; i++) { try { readAt(big, big.length - 3); } catch (e) { threw += e.code === "ERR_OUT_OF_RANGE"; } } + assert(threw === 200, "straddling the end of a >2GB view throws"); + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }, 120_000); + + test("views with 2GB and ~4GB byteOffsets read and write correctly after tier-up", async () => { + const { stdout, stderr, exitCode } = await run( + prelude + + ` + let ab; + try { ab = new ArrayBuffer(4 * 2**30); } catch { console.log("OK"); process.exit(0); } + const tailOffset = 4 * 2**30 - 64; + const tail = Buffer.from(ab, tailOffset, 64); + const wide = Buffer.from(ab, 2**31); + const raw = new DataView(ab); + function readAt(v, o) { return v.readInt32LE(o); } + function writeAt(v, x, o) { return v.writeInt32LE(x, o); } + noInline(readAt); noInline(writeAt); + for (let i = 0; i < N; i++) { + assert(writeAt(tail, i, 8) === 12 && readAt(tail, 8) === i, "~4GB byteOffset view"); + assert(writeAt(wide, ~i, wide.length - 4) === wide.length && readAt(wide, wide.length - 4) === ~i, "2GB byteOffset view"); + } + assert(raw.getInt32(tailOffset + 8, true) === N - 1, "the store landed at byteOffset + offset"); + assert(raw.getInt32(2**31 + wide.length - 4, true) === ~(N - 1), "the store landed at the 2GB byteOffset"); + let threw = false; + try { readAt(tail, 61); } catch (e) { threw = e.code === "ERR_OUT_OF_RANGE"; } + assert(threw, "straddling the end of the tiny high-offset view throws"); + console.log("OK"); + `, + ); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); + }, 120_000); +}); diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index 65d94b53c996..18a5c8b86aaa 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -4647,3 +4647,295 @@ it.skipIf(os.totalmem() < 10 * 1024 ** 3)( expect(exitCode).toBe(0); }, ); + +// The fixed-width read* / write* accessors are C++ host functions that JSC's DFG/FTL compile into +// bounds-checked loads / stores (JSBuffer.cpp + JavaScriptCore's BufferAccessorRegistry). They must +// keep agreeing with a DataView reference after tier-up, and everything the JIT does not speculate +// (bad offsets, out-of-range values, other receivers) must keep throwing exactly as before. +describe("read*/write* after JIT tier-up", () => { + const buf = Buffer.alloc(64); + const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); + for (let i = 0; i < buf.length; i++) buf[i] = (i * 37 + 11) & 0xff; + + const readers = [ + ["readInt8", 1, o => dv.getInt8(o)], + ["readUInt8", 1, o => dv.getUint8(o)], + ["readInt16LE", 2, o => dv.getInt16(o, true)], + ["readInt16BE", 2, o => dv.getInt16(o, false)], + ["readUInt16LE", 2, o => dv.getUint16(o, true)], + ["readUInt16BE", 2, o => dv.getUint16(o, false)], + ["readInt32LE", 4, o => dv.getInt32(o, true)], + ["readInt32BE", 4, o => dv.getInt32(o, false)], + ["readUInt32LE", 4, o => dv.getUint32(o, true)], + ["readUInt32BE", 4, o => dv.getUint32(o, false)], + ["readFloatLE", 4, o => dv.getFloat32(o, true)], + ["readFloatBE", 4, o => dv.getFloat32(o, false)], + ["readDoubleLE", 8, o => dv.getFloat64(o, true)], + ["readDoubleBE", 8, o => dv.getFloat64(o, false)], + ["readBigInt64LE", 8, o => dv.getBigInt64(o, true)], + ["readBigInt64BE", 8, o => dv.getBigInt64(o, false)], + ["readBigUInt64LE", 8, o => dv.getBigUint64(o, true)], + ["readBigUInt64BE", 8, o => dv.getBigUint64(o, false)], + ]; + + function codeOf(fn) { + try { + fn(); + } catch (e) { + return e.code; + } + return "no throw"; + } + + it("reads match a DataView across many iterations, and out-of-bounds keeps throwing", () => { + for (const [name, byteSize, reference] of readers) { + const read = new Function("b", "o", `return b.${name}(o);`); + let mismatches = 0; + for (let i = 0; i < 1500; i++) { + const o = i & 31; + if (read(buf, o) !== reference(o)) mismatches++; + } + expect(mismatches).toBe(0); + expect(read(buf, 64 - byteSize)).toBe(reference(64 - byteSize)); + let outOfBounds = 0, + negative = 0, + fractional = 0, + wrongType = 0; + for (let i = 0; i < 50; i++) { + if (codeOf(() => read(buf, 64 - byteSize + 1)) === "ERR_OUT_OF_RANGE") outOfBounds++; + if (codeOf(() => read(buf, -1)) === "ERR_OUT_OF_RANGE") negative++; + if (codeOf(() => read(buf, 1.5)) === "ERR_OUT_OF_RANGE") fractional++; + if (codeOf(() => read(buf, "0")) === "ERR_INVALID_ARG_TYPE") wrongType++; + } + expect([outOfBounds, negative, fractional, wrongType]).toEqual([50, 50, 50, 50]); + } + }); + + it("writes match a DataView across many iterations, and range checks keep throwing", () => { + const cases = [ + ["writeInt8", 1, o => dv.getInt8(o), i => (i & 0xff) - 128], + ["writeUInt8", 1, o => dv.getUint8(o), i => i & 0xff], + ["writeInt16LE", 2, o => dv.getInt16(o, true), i => (i & 0xffff) - 0x8000], + ["writeUInt16BE", 2, o => dv.getUint16(o, false), i => i & 0xffff], + ["writeInt32LE", 4, o => dv.getInt32(o, true), i => (-i * 1000) | 0], + ["writeUInt32BE", 4, o => dv.getUint32(o, false), i => 2147483648 + i], + ["writeFloatLE", 4, o => dv.getFloat32(o, true), i => Math.fround(i / 3)], + ["writeDoubleBE", 8, o => dv.getFloat64(o, false), i => -i - 0.5], + ]; + for (const [name, byteSize, reference, value] of cases) { + const write = new Function("b", "v", "o", `return b.${name}(v, o);`); + let mismatches = 0; + for (let i = 0; i < 1500; i++) { + const o = i & 31; + const v = value(i); + if (write(buf, v, o) !== o + byteSize || reference(o) !== v) mismatches++; + } + expect(mismatches).toBe(0); + let outOfBounds = 0; + for (let i = 0; i < 50; i++) if (codeOf(() => write(buf, value(i), 64)) === "ERR_OUT_OF_RANGE") outOfBounds++; + expect(outOfBounds).toBe(50); + } + let ranges = 0; + for (let i = 0; i < 2000; i++) { + if (codeOf(() => buf.writeInt8(128, 0)) === "ERR_OUT_OF_RANGE") ranges++; + if (codeOf(() => buf.writeUInt16LE(65536, 0)) === "ERR_OUT_OF_RANGE") ranges++; + if (codeOf(() => buf.writeUInt32BE(-1, 0)) === "ERR_OUT_OF_RANGE") ranges++; + } + expect(ranges).toBe(6000); + expect(() => buf.writeInt16LE("40000", 0)).toThrow("Received 40000"); + }); + + it("BigInt writes match a DataView across many iterations, and 64-bit range checks keep throwing", () => { + const values = [0n, 1n, -1n, 2n ** 32n + 7n, 2n ** 63n - 1n, -(2n ** 63n)]; + let mismatches = 0; + for (let i = 0; i < 1500; i++) { + const o = (i & 7) * 8; + const v = values[i % values.length]; + if (buf.writeBigInt64LE(v, o) !== o + 8 || dv.getBigInt64(o, true) !== v) mismatches++; + if (buf.writeBigInt64BE(v, o) !== o + 8 || dv.getBigInt64(o, false) !== v) mismatches++; + if (v >= 0n) { + if (buf.writeBigUInt64LE(v, o) !== o + 8 || dv.getBigUint64(o, true) !== v) mismatches++; + if (buf.writeBigUInt64BE(v, o) !== o + 8 || dv.getBigUint64(o, false) !== v) mismatches++; + } + } + expect(mismatches).toBe(0); + expect(buf.writeBigUInt64LE(2n ** 64n - 1n, 0)).toBe(8); + expect(dv.getBigUint64(0, true)).toBe(2n ** 64n - 1n); + let codes = []; + for (let i = 0; i < 1000; i++) { + codes = [ + codeOf(() => buf.writeBigUInt64LE(-1n, 0)), + codeOf(() => buf.writeBigInt64LE(2n ** 63n, 0)), + codeOf(() => buf.writeBigInt64LE(-(2n ** 63n) - 1n, 0)), + codeOf(() => buf.writeBigInt64LE(5, 0)), + codeOf(() => buf.writeBigInt64LE(0n, 57)), + ]; + } + expect(codes).toEqual([ + "ERR_OUT_OF_RANGE", + "ERR_OUT_OF_RANGE", + "ERR_OUT_OF_RANGE", + "ERR_INVALID_ARG_TYPE", + "ERR_OUT_OF_RANGE", + ]); + }); + + it("works on many distinct buffers (no hidden per-buffer state)", () => { + const bufs = Array.from({ length: 512 }, (_, i) => { + const b = Buffer.alloc(16); + b.writeInt32LE(i * 7, 4); + return b; + }); + let sum = 0; + for (let round = 0; round < 50; round++) { + for (let i = 0; i < bufs.length; i++) sum += bufs[i].readInt32LE(4); + } + expect(sum).toBe(50 * 7 * ((511 * 512) / 2)); + // Reading/writing added no non-index own properties to the buffers. + expect(Object.getOwnPropertyNames(bufs[0]).filter(k => !/^\d+$/.test(k))).toEqual([]); + expect(Object.getOwnPropertySymbols(bufs[0])).toEqual([]); + }); + + it("keeps working with other ArrayBufferView receivers via .call", () => { + const u8 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + const dv8 = new DataView(u8.buffer); + for (let i = 0; i < 1500; i++) { + expect(Buffer.prototype.readInt32LE.call(u8, 0)).toBe(dv8.getInt32(0, true)); + expect(Buffer.prototype.readUInt16BE.call(u8, 6)).toBe(dv8.getUint16(6, false)); + expect(Buffer.prototype.writeUInt8.call(u8, i & 0xff, 7)).toBe(8); + expect(u8[7]).toBe(i & 0xff); + expect(() => Buffer.prototype.readInt32LE.call({}, 0)).toThrow(TypeError); + } + // The bound is the receiver's element count (this.length), as in lib/internal/buffer.js, not + // its byteLength: on a wider-element view these throw even though the bytes would fit. + const u16 = new Uint16Array(4); + expect(codeOf(() => Buffer.prototype.writeUInt32BE.call(u16, 1, 3))).toBe("ERR_OUT_OF_RANGE"); + // The BigInt writers share the receiver / bound handling with the rest of the family. + expect(codeOf(() => Buffer.prototype.writeBigInt64LE.call(u16, 5n, 0))).toBe("ERR_BUFFER_OUT_OF_BOUNDS"); + expect(codeOf(() => Buffer.prototype.readBigInt64LE.call(u16, 0))).toBe("ERR_BUFFER_OUT_OF_BOUNDS"); + expect(codeOf(() => Buffer.prototype.writeBigInt64LE.call({}, 0n))).toBe("ERR_INVALID_ARG_TYPE"); + // For the BigInt writers checkInt() validates the value's range before checkBounds() reaches the + // receiver, so a bad value wins over the DataView receiver. + expect(codeOf(() => Buffer.prototype.writeBigInt64LE.call(new DataView(new ArrayBuffer(16)), 2n ** 64n, 0))).toBe( + "ERR_OUT_OF_RANGE", + ); + expect(() => Buffer.prototype.writeBigInt64LE.call(new DataView(new ArrayBuffer(16)), 2n ** 64n, 0)).toThrow( + 'The value of "value" is out of range', + ); + // A garbage receiver never masks the value's own error: the BigInt writers, like their + // number siblings, validate the value first. + expect(codeOf(() => Buffer.prototype.writeBigInt64LE.call({}, 2n ** 64n, 0))).toBe("ERR_OUT_OF_RANGE"); + expect(() => Buffer.prototype.writeBigInt64LE.call({}, 2n ** 64n, 0)).toThrow('The value of "value"'); + // An omitted offset defaults to 0 in the error text, on writes as on reads. + expect(() => Buffer.prototype.writeBigInt64LE.call(new DataView(new ArrayBuffer(16)), 5n)).toThrow("Received 0"); + // The offset's type is validated before the receiver's length is consulted, matching the + // fixed-width writers, so a non-number offset still wins over the DataView receiver. + expect(codeOf(() => Buffer.prototype.writeBigInt64LE.call(new DataView(new ArrayBuffer(16)), 5n, "bad"))).toBe( + "ERR_INVALID_ARG_TYPE", + ); + expect(codeOf(() => Buffer.prototype.writeBigInt64LE.call(new DataView(new ArrayBuffer(16)), 5n, 1.5))).toBe( + "ERR_OUT_OF_RANGE", + ); + // A DataView has no `length`, so as in lib/internal/buffer.js every accessor reports `<= NaN`. + const dv = new DataView(new ArrayBuffer(8)); + for (const f of [ + () => Buffer.prototype.readIntLE.call(dv, 0, 3), + () => Buffer.prototype.writeIntLE.call(dv, 1, 0, 3), + () => Buffer.prototype.readInt32LE.call(dv, 0), + () => Buffer.prototype.writeInt32LE.call(dv, 1, 0), + () => Buffer.prototype.readBigInt64LE.call(dv, 0), + () => Buffer.prototype.writeBigInt64LE.call(dv, 5n, 0), + () => Buffer.prototype.writeBigUInt64BE.call(dv, 5n, 0), + ]) { + expect(codeOf(f)).toBe("ERR_OUT_OF_RANGE"); + expect(() => f()).toThrow("<= NaN"); + } + expect(codeOf(() => Buffer.prototype.readInt32LE.call(u16, 3))).toBe("ERR_OUT_OF_RANGE"); + expect(codeOf(() => Buffer.prototype.readUIntLE.call(u16, 2, 3))).toBe("ERR_OUT_OF_RANGE"); + u16[0] = 0x1234; + expect(Buffer.prototype.readInt32LE.call(u16, 0)).toBe(new DataView(u16.buffer).getInt32(0, true)); + }); + + it("variable-width readers/writers match across widths after tier-up", () => { + const uint = (o, l, le) => { + let value = 0; + for (let i = 0; i < l; ++i) value = le ? value + buf[o + i] * 2 ** (8 * i) : value * 256 + buf[o + i]; + return value; + }; + const sint = (o, l, le) => { + const value = uint(o, l, le); + return value >= 2 ** (8 * l - 1) ? value - 2 ** (8 * l) : value; + }; + // A constant byteLength (JIT-inlined for 1/2/4) and a varying one (host path). + const readConst3 = (b, o) => b.readUIntBE(o, 3); + const readConst4 = (b, o) => b.readIntLE(o, 4); + let mismatches = 0; + for (let i = 0; i < 1500; i++) { + const o = i & 15; + const l = 1 + (i % 6); + if (buf.readIntLE(o, l) !== sint(o, l, true)) mismatches++; + if (buf.readIntBE(o, l) !== sint(o, l, false)) mismatches++; + if (buf.readUIntLE(o, l) !== uint(o, l, true)) mismatches++; + if (buf.readUIntBE(o, l) !== uint(o, l, false)) mismatches++; + if (readConst3(buf, o) !== uint(o, 3, false)) mismatches++; + if (readConst4(buf, o) !== dv.getInt32(o, true)) mismatches++; + } + expect(mismatches).toBe(0); + const scratch = Buffer.alloc(16); + for (let i = 0; i < 1500; i++) { + const l = 1 + (i % 6); + const v = i % 100; + expect(scratch.writeUIntLE(v, 0, l)).toBe(l); + expect(scratch.readUIntLE(0, l)).toBe(v); + expect(scratch.writeIntBE(-v, 8, l)).toBe(8 + l); + expect(scratch.readIntBE(8, l)).toBe(-v | 0); + } + let codes = []; + for (let i = 0; i < 1000; i++) { + codes = [ + codeOf(() => buf.readIntLE(0, 7)), + codeOf(() => buf.readIntLE(undefined, 4)), + codeOf(() => readConst4(buf, 61)), + codeOf(() => scratch.writeIntLE(2 ** 24, 0, 3)), + ]; + } + expect(codes).toEqual(["ERR_OUT_OF_RANGE", "ERR_INVALID_ARG_TYPE", "ERR_OUT_OF_RANGE", "ERR_OUT_OF_RANGE"]); + // The value is only coerced after the byteLength dispatch, as in lib/internal/buffer.js. + let valueOfCalls = 0; + expect( + codeOf(() => + scratch.writeIntLE( + { + valueOf() { + valueOfCalls++; + return 1; + }, + }, + 0, + 7, + ), + ), + ).toBe("ERR_OUT_OF_RANGE"); + expect(valueOfCalls).toBe(0); + // With both a bad value and a bad offset: the one-byte writers (writeU_Int8) report the offset + // first, the wider ones (checkInt) the value first. + expect(codeOf(() => scratch.writeUIntLE(300, "bad", 1))).toBe("ERR_INVALID_ARG_TYPE"); + expect(codeOf(() => scratch.writeIntLE(200, "bad", 1))).toBe("ERR_INVALID_ARG_TYPE"); + expect(codeOf(() => scratch.writeUIntLE(2 ** 24, "bad", 2))).toBe("ERR_OUT_OF_RANGE"); + expect(codeOf(() => scratch.writeIntBE(2 ** 24, "bad", 3))).toBe("ERR_OUT_OF_RANGE"); + // Every accessor validates the offset's type before consulting the receiver, so a bad offset + // wins over a garbage receiver across the whole family. + for (const f of [ + () => Buffer.prototype.readIntLE.call({}, "bad", 3), + () => Buffer.prototype.readInt32LE.call({}, "bad"), + () => Buffer.prototype.writeIntLE.call({}, 1, "bad", 3), + () => Buffer.prototype.writeBigInt64LE.call({}, 5n, "bad"), + ]) { + expect(codeOf(f)).toBe("ERR_INVALID_ARG_TYPE"); + expect(() => f()).toThrow('The "offset" argument must be of type number'); + } + // Out-of-range integral offsets, including |offset| > 2**53, get the bounds message. + expect(() => scratch.readIntLE(2 ** 53, 2)).toThrow(">= 0 and <= 14"); + expect(() => scratch.readIntLE(1.5, 2)).toThrow("an integer"); + }); +});