Skip to content
Open
2 changes: 1 addition & 1 deletion src/codegen/builtins/data/own-properties.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Blob.prototype : @@toStringTag/c arrayBuffer/w bytes/w constructor/wc delete/w e
Boolean : length/? name/? prototype/-
Boolean.prototype : constructor/wc toString/wc valueOf/wc
BroadcastChannel.prototype open : @@@nodejs.util.inspect.custom/wc
Buffer : alloc/wc allocUnsafe/wc allocUnsafeSlow/wc byteLength/wc compare/wc concat/wc copyBytesFrom/wc from/wc isBuffer/wc isEncoding/wc length/? name/? poolSize/wc prototype/-
Buffer : alloc/wc allocUnsafe/wc allocUnsafeSlow/wc byteLength/wc compare/wc concat/wc copyBytesFrom/wc from/wc isBuffer/wc isEncoding/wc length/? name/? of/wc poolSize/wc prototype/-
Buffer.prototype : @@@nodejs.util.inspect.custom/wc @@species/- @@toStringTag/c asciiSlice/wc asciiWrite/wc base64Slice/wc base64Write/wc base64urlSlice/wc base64urlWrite/wc compare/wc constructor/wc copy/wc equals/wc fill/wc hexSlice/wc hexWrite/wc includes/wc indexOf/wc inspect/wc lastIndexOf/wc latin1Slice/wc latin1Write/wc offset/ac parent/ac readBigInt64/wc readBigInt64BE/wc readBigInt64LE/wc readBigUInt64/wc readBigUInt64BE/wc readBigUInt64LE/wc readBigUint64BE/wc readBigUint64LE/wc readDouble/wc readDoubleBE/wc readDoubleLE/wc readFloat/wc readFloatBE/wc readFloatLE/wc readInt16/wc readInt16BE/wc readInt16LE/wc readInt32/wc readInt32BE/wc readInt32LE/wc readInt8/wc readIntBE/wc readIntLE/wc readUInt16BE/wc readUInt16LE/wc readUInt32BE/wc readUInt32LE/wc readUInt8/wc readUIntBE/wc readUIntLE/wc readUint16BE/wc readUint16LE/wc readUint32BE/wc readUint32LE/wc readUint8/wc readUintBE/wc readUintLE/wc slice/wc subarray/wc swap16/wc swap32/wc swap64/wc toJSON/wc toLocaleString/wc toString/wc ucs2Slice/wc ucs2Write/wc utf16leSlice/wc utf16leWrite/wc utf8Slice/wc utf8Write/wc write/wc writeBigInt64BE/wc writeBigInt64LE/wc writeBigUInt64BE/wc writeBigUInt64LE/wc writeBigUint64BE/wc writeBigUint64LE/wc writeDouble/wc writeDoubleBE/wc writeDoubleLE/wc writeFloat/wc writeFloatBE/wc writeFloatLE/wc writeInt16BE/wc writeInt16LE/wc writeInt32BE/wc writeInt32LE/wc writeInt8/wc writeIntBE/wc writeIntLE/wc writeUInt16/wc writeUInt16BE/wc writeUInt16LE/wc writeUInt32/wc writeUInt32BE/wc writeUInt32LE/wc writeUInt8/wc writeUIntBE/wc writeUIntLE/wc writeUint16/wc writeUint16BE/wc writeUint16LE/wc writeUint32/wc writeUint32BE/wc writeUint32LE/wc writeUint8/wc writeUintBE/wc writeUintLE/wc
Bun.Cookie : from/w length/c name/c parse/w prototype/-
Bun.CookieMap : length/c name/c prototype/-
Expand Down
9 changes: 9 additions & 0 deletions src/codegen/builtins/host/buffer.builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,15 @@ export default b.declarations(
uses: { encoding: b.inspected },
implementation: b.hostCall,
},
of: {
args: { items: b.any.rest },
returns: newBuffer,
capability: "js",
throws: true,
runsOnlyOperandCode: true,
uses: { items: converted },
implementation: b.hostCall,
},
},
proto: {
parent: propertyOfBuffer("parent", "buffer", b.union(b.instance("ArrayBuffer"), b.instance("SharedArrayBuffer"))),
Expand Down
21 changes: 21 additions & 0 deletions src/jsc/bindings/JSBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ JSC_DECLARE_HOST_FUNCTION(jsBufferConstructorFunction_compare);
JSC_DECLARE_HOST_FUNCTION(jsBufferConstructorFunction_concat);
JSC_DECLARE_HOST_FUNCTION(jsBufferConstructorFunction_copyBytesFrom);
JSC_DECLARE_HOST_FUNCTION(jsBufferConstructorFunction_isEncoding);
JSC_DECLARE_HOST_FUNCTION(jsBufferConstructorFunction_of);

JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_compare);
JSC_DECLARE_HOST_FUNCTION(jsBufferPrototypeFunction_copy);
Expand Down Expand Up @@ -2749,6 +2750,25 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferConstructorFunction_copyBytesFrom, (JSGlobalObj
return jsBufferConstructorFunction_copyBytesFromBody(lexicalGlobalObject, callFrame);
}

// Node's `of` is an arrow function, so it never reads `this`: https://github.com/nodejs/node/blob/v26.3.0/lib/buffer.js#L420-L427
JSC_DEFINE_HOST_FUNCTION(jsBufferConstructorFunction_of, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);

size_t length = callFrame->argumentCount();
// JS cannot reach the buffer before the return, so the loop writes every byte or a throw drops the buffer.
auto* buffer = createUninitializedBuffer(lexicalGlobalObject, length);
RETURN_IF_EXCEPTION(scope, {});

for (size_t i = 0; i < length; i++) {
buffer->setIndex(lexicalGlobalObject, i, callFrame->uncheckedArgument(i));
RETURN_IF_EXCEPTION(scope, {});
}

return JSValue::encode(buffer);
}

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
Expand Down Expand Up @@ -3780,6 +3800,7 @@ const ClassInfo JSBufferPrototype::s_info = {
from JSBuiltin Builtin|Function 1
isBuffer JSBuiltin Builtin|Function 1
isEncoding jsBufferConstructorFunction_isEncoding Function 1
of jsBufferConstructorFunction_of Function 0
Comment thread
robobun marked this conversation as resolved.
Resolved
@end
*/
#include "JSBuffer.lut.h"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const objects = [
["%TypedArray%", TypedArray, ["from", "of", "prototype", Symbol.species], ofFunctionPrototype],
["Uint8Array", Uint8Array, ["BYTES_PER_ELEMENT", "prototype"], ["from", "of", ...ofFunctionPrototype]],
["BigInt64Array", BigInt64Array, ["BYTES_PER_ELEMENT", "prototype"], ["from", "of", ...ofFunctionPrototype]],
["Buffer", Buffer, ["from", "alloc", "concat", "isBuffer", "byteLength", "prototype"], ["of", "fromHex", "BYTES_PER_ELEMENT", ...ofFunctionPrototype]],
["Buffer", Buffer, ["from", "of", "alloc", "concat", "isBuffer", "byteLength", "prototype"], ["fromHex", "BYTES_PER_ELEMENT", ...ofFunctionPrototype]],
];
for (const [name, object, members, inherited] of objects) {
tryToChange(name + ": a member", object, members);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Buffer.of makes a Buffer of its operands whatever its receiver is: it converts each operand in order, ends at the first conversion that throws, and constructs nothing through the receiver.
const show = made => `${made instanceof Buffer} ${Object.getPrototypeOf(made) === Buffer.prototype} ${made.length} ${made.toString("hex")}`;
const ran = [];
const counted = (name, value) => ({ valueOf() { ran.push(name); return value; } });
console.log(show(Buffer.of()), show(Buffer.of(1, 2, 3)), show(Buffer.of(1.9, -1, 300, NaN, "16", "0x10", true, null, undefined)));
const hinted = { [Symbol.toPrimitive](hint) { ran.push(hint); return 3; } };
const spelled = { valueOf: undefined, toString() { ran.push("toString"); return "4"; } };
console.log(show(Buffer.of(counted("a", 1), counted("b", 2), hinted, spelled)), ran.join());
const { of } = Buffer;
console.log(show(of(5, 6)), show(Buffer.of.call(Uint8Array, 7)), show(Buffer.of.call(null, 8)), show(Reflect.apply(Buffer.of, undefined, [9])));
console.log([65, 66].map(Buffer.of).map(show).join(", "), Object.hasOwn(Buffer, "of"), Buffer.of === Uint8Array.of, Buffer.of.length, Buffer.of.name);
let constructed = 0;
class Packet extends Buffer { constructor(...operands) { constructed++; super(...operands); } }
console.log(show(Packet.of(1, 2)), Packet.of === Buffer.of, constructed);
for (const refused of [Symbol("s"), 1n, { valueOf() { throw new RangeError("thrown by valueOf"); } }]) {
ran.length = 0;
try { console.log(show(Buffer.of(counted("before", 1), refused, counted("after", 2)))); } catch (error) { console.log(error.name, ran.join()); }
}
function total() {
let sum = 0;
for (let i = 0; i < 300; i++) { const made = Buffer.of(i, i >> 1, counted("in the loop", i)); sum += made[0] + made[1] + made[2] + made.length; }
return sum;
}
ran.length = 0;
console.log(total(), ran.length, total(), ran.length);
147 changes: 147 additions & 0 deletions test/js/node/buffer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5030,6 +5030,153 @@ describe("Buffer.copyBytesFrom", () => {
});
});

// Node has its own Buffer.of that never reads `this`. The expected values are those of Node v26.3.0.
// https://github.com/nodejs/node/blob/v26.3.0/lib/buffer.js#L420-L427
describe("Buffer.of", () => {
const summarize = buffer => ({
isBuffer: Buffer.isBuffer(buffer),
prototype: Object.getPrototypeOf(buffer) === Buffer.prototype,
bytes: [...buffer],
byteOffset: buffer.byteOffset,
arrayBufferByteLength: buffer.buffer.byteLength,
});
// A Buffer of this realm that does not share its ArrayBuffer.
const plainBuffer = bytes => ({
isBuffer: true,
prototype: true,
bytes,
byteOffset: 0,
arrayBufferByteLength: bytes.length,
});

it("is an own method of Buffer, not %TypedArray%.of", () => {
const descriptor = Object.getOwnPropertyDescriptor(Buffer, "of");
expect(descriptor).toEqual({ value: expect.any(Function), writable: true, enumerable: true, configurable: true });
expect(descriptor.value).not.toBe(Uint8Array.of);
expect(Object.keys(Buffer)).toContain("of");
expect({
length: Buffer.of.length,
name: Buffer.of.name,
ownPrototype: Object.hasOwn(Buffer.of, "prototype"),
}).toEqual({ length: 0, name: "of", ownPrototype: false });
expect(() => new Buffer.of(1)).toThrow(TypeError);
});

it.each([
["undefined", undefined],
["null", null],
["Uint8Array", Uint8Array],
["Float64Array", Float64Array],
["Array", Array],
["a function that is not a constructor", () => {}],
])("returns a Buffer when `this` is %s", (_, receiver) => {
expect(summarize(Buffer.of.call(receiver, 1.5, 258))).toEqual(plainBuffer([1, 2]));
});

it("works as a detached function and as a callback", async () => {
const { of } = Buffer;
expect(summarize(of(1, 2))).toEqual(plainBuffer([1, 2]));
expect(summarize(Reflect.apply(Buffer.of, undefined, [1, 2, 3]))).toEqual(plainBuffer([1, 2, 3]));
// Array.prototype.map calls it with (value, index, array).
expect([65, 66].map(Buffer.of).map(summarize)).toEqual([plainBuffer([65, 0, 0]), plainBuffer([66, 1, 0])]);
expect(summarize(await Promise.resolve(65).then(Buffer.of))).toEqual(plainBuffer([65]));
expect(summarize(vm.runInNewContext("const f = B.of; f(3, 4)", { B: Buffer }))).toEqual(plainBuffer([3, 4]));
});

it("works on a plain object that copies the statics of Buffer, as safer-buffer does", () => {
const copy = {};
for (const key in Buffer) {
if (Object.hasOwn(Buffer, key)) copy[key] = Buffer[key];
}
expect(summarize(copy.of(1, 2))).toEqual(plainBuffer([1, 2]));
});

it("does not construct through a subclass or a Proxy", () => {
let constructed = 0;
class Sub extends Buffer {
constructor(...args) {
constructed++;
super(...args);
}
}
const proxy = new Proxy(Buffer, {
construct(target, args, newTarget) {
constructed++;
return Reflect.construct(target, args, newTarget);
},
});
expect(Sub.of).toBe(Buffer.of);
expect(summarize(Sub.of(1, 2))).toEqual(plainBuffer([1, 2]));
expect(summarize(proxy.of(3, 4))).toEqual(plainBuffer([3, 4]));
expect(constructed).toBe(0);
});

it("converts each argument to a byte, in order", () => {
const log = [];
const object = (name, value) => ({
valueOf() {
log.push(name);
return value;
},
});
const reentrant = {
valueOf() {
log.push("reentrant");
return Buffer.of(9, 8, 7)[1];
},
};
const [first, last] = [object("a", 1), object("b", 7)];
const args = [first, "3", 300, -1, 1.9, NaN, Infinity, undefined, null, true, "0x10", reentrant, last];
expect(summarize(Buffer.of(...args))).toEqual(plainBuffer([1, 3, 44, 255, 1, 0, 0, 0, 0, 1, 16, 8, 7]));
expect(log).toEqual(["a", "reentrant", "b"]);
});

it("stops at the first argument that throws", () => {
const log = [];
const error = new RangeError("boom");
const object = (name, fn) => ({
valueOf() {
log.push(name);
return fn();
},
});
const fail = () => {
throw error;
};
const args = [object("a", () => 1), object("b", fail), object("c", () => 3)];
let thrown;
try {
Buffer.of(...args);
} catch (e) {
thrown = e;
}
expect(thrown).toBe(error);
expect(log).toEqual(["a", "b"]);
expect(() => Buffer.of(1, Symbol("s"))).toThrow(TypeError);
expect(() => Buffer.of(1, 2n)).toThrow(TypeError);
});

// A typed array of up to 1000 bytes keeps its bytes in the GC heap. A longer one allocates them apart.
it.each([0, 1, 1000, 1001, 5000])("takes %d arguments", length => {
const bytes = Array.from({ length }, (_, i) => (i * 7) & 255);
expect(summarize(Buffer.of(...bytes))).toEqual(plainBuffer(bytes));
});

it.each([3, 2000])("keeps the new Buffer of %d bytes alive when valueOf() collects garbage", length => {
const bytes = Array.from({ length }, (_, i) => (i * 7) & 255);
const collect = {
valueOf() {
Bun.gc(true);
return 99;
},
};
const args = bytes.with(length - 2, collect);
for (let i = 0; i < 5; i++) {
expect(summarize(Buffer.of(...args))).toEqual(plainBuffer(bytes.with(length - 2, 99)));
}
});
});

describe("Buffer.prototype.toString binary-to-text encodings", () => {
// Reference implementations (scalar, independent of Bun's native encoders) so
// the SIMD/bulk paths for hex and base64 are checked byte-for-byte, including
Expand Down
Loading