diff --git a/benchmark/misc/webidl-buffer-source.js b/benchmark/misc/webidl-buffer-source.js new file mode 100644 index 00000000000000..56a3b628b98011 --- /dev/null +++ b/benchmark/misc/webidl-buffer-source.js @@ -0,0 +1,54 @@ +'use strict'; + +const common = require('../common.js'); + +const bench = common.createBenchmark(main, { + input: [ + 'arraybuffer', + 'buffer', + 'dataview', + 'uint8array', + 'uint8clampedarray', + 'int8array', + 'uint16array', + 'int16array', + 'uint32array', + 'int32array', + 'float16array', + 'float32array', + 'float64array', + 'bigint64array', + 'biguint64array', + ], + n: [1e6], +}, { flags: ['--expose-internals'] }); + +function main({ n, input }) { + const { converters } = require('internal/webidl'); + + let value; + switch (input) { + case 'arraybuffer': value = new ArrayBuffer(32); break; + case 'buffer': value = Buffer.alloc(32); break; + case 'dataview': value = new DataView(new ArrayBuffer(32)); break; + case 'uint8array': value = new Uint8Array(32); break; + case 'uint8clampedarray': value = new Uint8ClampedArray(32); break; + case 'int8array': value = new Int8Array(32); break; + case 'uint16array': value = new Uint16Array(16); break; + case 'int16array': value = new Int16Array(16); break; + case 'uint32array': value = new Uint32Array(8); break; + case 'int32array': value = new Int32Array(8); break; + case 'float16array': value = new Float16Array(16); break; + case 'float32array': value = new Float32Array(8); break; + case 'float64array': value = new Float64Array(4); break; + case 'bigint64array': value = new BigInt64Array(4); break; + case 'biguint64array': value = new BigUint64Array(4); break; + } + + const opts = { prefix: 'test', context: 'test' }; + + bench.start(); + for (let i = 0; i < n; i++) + converters.BufferSource(value, opts); + bench.end(n); +} diff --git a/lib/internal/webidl.js b/lib/internal/webidl.js index d6bfea8e8479fa..36bde94013d1a8 100644 --- a/lib/internal/webidl.js +++ b/lib/internal/webidl.js @@ -2,6 +2,7 @@ const { ArrayBufferIsView, + ArrayBufferPrototypeGetByteLength, ArrayPrototypePush, ArrayPrototypeToSorted, DataViewPrototypeGetBuffer, @@ -33,8 +34,7 @@ const { const { kEmptyObject } = require('internal/util'); const { isArrayBuffer, - isDataView, - isSharedArrayBuffer, + isTypedArray, } = require('internal/util/types'); const converters = { __proto__: null }; @@ -390,9 +390,23 @@ function createInterfaceConverter(name, I) { }; } -function getDataViewOrTypedArrayBuffer(V) { - return isDataView(V) ? - DataViewPrototypeGetBuffer(V) : TypedArrayPrototypeGetBuffer(V); +// Returns the [[ViewedArrayBuffer]] of an ArrayBufferView without leaving JS. +function getViewedArrayBuffer(V) { + return isTypedArray(V) ? + TypedArrayPrototypeGetBuffer(V) : DataViewPrototypeGetBuffer(V); +} + +// Returns `true` if `buffer` is a `SharedArrayBuffer`. Uses a brand check via +// the `ArrayBuffer.prototype.byteLength` getter, which succeeds only on real +// (non-shared) ArrayBuffers and throws on SharedArrayBuffers — independent +// of the receiver's prototype chain. +function isSharedArrayBufferBacking(buffer) { + try { + ArrayBufferPrototypeGetByteLength(buffer); + return false; + } catch { + return true; + } } // https://webidl.spec.whatwg.org/#ArrayBufferView @@ -402,7 +416,7 @@ converters.ArrayBufferView = (V, opts = kEmptyObject) => { 'is not an ArrayBufferView.', opts); } - if (isSharedArrayBuffer(getDataViewOrTypedArrayBuffer(V))) { + if (isSharedArrayBufferBacking(getViewedArrayBuffer(V))) { throw makeException( 'is a view on a SharedArrayBuffer, which is not allowed.', opts); @@ -414,7 +428,7 @@ converters.ArrayBufferView = (V, opts = kEmptyObject) => { // https://webidl.spec.whatwg.org/#BufferSource converters.BufferSource = (V, opts = kEmptyObject) => { if (ArrayBufferIsView(V)) { - if (isSharedArrayBuffer(getDataViewOrTypedArrayBuffer(V))) { + if (isSharedArrayBufferBacking(getViewedArrayBuffer(V))) { throw makeException( 'is a view on a SharedArrayBuffer, which is not allowed.', opts); diff --git a/test/parallel/test-internal-webidl-buffer-source.js b/test/parallel/test-internal-webidl-buffer-source.js new file mode 100644 index 00000000000000..2fb529edcde1b7 --- /dev/null +++ b/test/parallel/test-internal-webidl-buffer-source.js @@ -0,0 +1,208 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); +const assert = require('assert'); +const { test } = require('node:test'); + +const { converters } = require('internal/webidl'); + +const TYPED_ARRAY_CTORS = [ + Uint8Array, Int8Array, Uint8ClampedArray, + Uint16Array, Int16Array, + Uint32Array, Int32Array, + Float16Array, Float32Array, Float64Array, + BigInt64Array, BigUint64Array, +]; + +test('BufferSource accepts ArrayBuffer', () => { + const ab = new ArrayBuffer(8); + assert.strictEqual(converters.BufferSource(ab), ab); +}); + +test('BufferSource accepts all TypedArray kinds', () => { + for (const Ctor of TYPED_ARRAY_CTORS) { + const ta = new Ctor(4); + assert.strictEqual(converters.BufferSource(ta), ta); + } +}); + +test('BufferSource accepts Buffer', () => { + const buf = Buffer.alloc(8); + assert.strictEqual(converters.BufferSource(buf), buf); +}); + +test('BufferSource accepts DataView', () => { + const dv = new DataView(new ArrayBuffer(8)); + assert.strictEqual(converters.BufferSource(dv), dv); +}); + +test('BufferSource accepts ArrayBuffer subclass instance', () => { + class MyAB extends ArrayBuffer {} + const sub = new MyAB(8); + assert.strictEqual(converters.BufferSource(sub), sub); +}); + +test('BufferSource accepts TypedArray with null prototype', () => { + const ta = new Uint8Array(4); + Object.setPrototypeOf(ta, null); + assert.strictEqual(converters.BufferSource(ta), ta); +}); + +test('BufferSource accepts DataView with null prototype', () => { + const dv = new DataView(new ArrayBuffer(4)); + Object.setPrototypeOf(dv, null); + assert.strictEqual(converters.BufferSource(dv), dv); +}); + +test('BufferSource accepts ArrayBuffer with null prototype', () => { + const ab = new ArrayBuffer(4); + Object.setPrototypeOf(ab, null); + assert.strictEqual(converters.BufferSource(ab), ab); +}); + +test('BufferSource rejects SharedArrayBuffer', () => { + assert.throws( + () => converters.BufferSource(new SharedArrayBuffer(4)), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +test('BufferSource rejects SAB-backed TypedArray', () => { + const view = new Uint8Array(new SharedArrayBuffer(4)); + assert.throws( + () => converters.BufferSource(view), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +test('BufferSource rejects SAB-backed DataView', () => { + const dv = new DataView(new SharedArrayBuffer(4)); + assert.throws( + () => converters.BufferSource(dv), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +test('BufferSource rejects SAB view whose buffer prototype was reassigned', () => { + const sab = new SharedArrayBuffer(4); + Object.setPrototypeOf(sab, ArrayBuffer.prototype); + const view = new Uint8Array(sab); + assert.throws( + () => converters.BufferSource(view), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +test('BufferSource accepts a detached ArrayBuffer', () => { + const ab = new ArrayBuffer(8); + structuredClone(ab, { transfer: [ab] }); + assert.strictEqual(ab.byteLength, 0); + assert.strictEqual(converters.BufferSource(ab), ab); +}); + +test('BufferSource rejects objects with a forged @@toStringTag', () => { + const fake = { [Symbol.toStringTag]: 'Uint8Array' }; + assert.throws( + () => converters.BufferSource(fake), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +for (const value of [null, undefined, 0, 1, 1n, '', 'x', true, Symbol('s'), [], + {}, () => {}]) { + test(`BufferSource rejects ${typeof value} ${String(value)}`, () => { + assert.throws( + () => converters.BufferSource(value), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + }); +} + +test('ArrayBufferView accepts all TypedArray kinds', () => { + for (const Ctor of TYPED_ARRAY_CTORS) { + const ta = new Ctor(4); + assert.strictEqual(converters.ArrayBufferView(ta), ta); + } +}); + +test('ArrayBufferView accepts DataView', () => { + const dv = new DataView(new ArrayBuffer(8)); + assert.strictEqual(converters.ArrayBufferView(dv), dv); +}); + +test('ArrayBufferView accepts TypedArray subclass instance', () => { + class MyU8 extends Uint8Array {} + const sub = new MyU8(4); + assert.strictEqual(converters.ArrayBufferView(sub), sub); +}); + +test('ArrayBufferView accepts TypedArray with null prototype', () => { + const ta = new Uint8Array(4); + Object.setPrototypeOf(ta, null); + assert.strictEqual(converters.ArrayBufferView(ta), ta); +}); + +test('ArrayBufferView accepts DataView with null prototype', () => { + const dv = new DataView(new ArrayBuffer(4)); + Object.setPrototypeOf(dv, null); + assert.strictEqual(converters.ArrayBufferView(dv), dv); +}); + +test('ArrayBufferView rejects raw ArrayBuffer', () => { + assert.throws( + () => converters.ArrayBufferView(new ArrayBuffer(4)), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +test('ArrayBufferView rejects raw SharedArrayBuffer', () => { + assert.throws( + () => converters.ArrayBufferView(new SharedArrayBuffer(4)), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +test('ArrayBufferView rejects SAB-backed TypedArray', () => { + const view = new Uint8Array(new SharedArrayBuffer(4)); + assert.throws( + () => converters.ArrayBufferView(view), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +test('ArrayBufferView rejects SAB-backed DataView', () => { + const dv = new DataView(new SharedArrayBuffer(4)); + assert.throws( + () => converters.ArrayBufferView(dv), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +test('ArrayBufferView rejects SAB view whose buffer prototype was reassigned', () => { + const sab = new SharedArrayBuffer(4); + Object.setPrototypeOf(sab, ArrayBuffer.prototype); + const view = new Uint8Array(sab); + assert.throws( + () => converters.ArrayBufferView(view), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +test('ArrayBufferView rejects objects with a forged @@toStringTag', () => { + const fake = { [Symbol.toStringTag]: 'Uint8Array' }; + assert.throws( + () => converters.ArrayBufferView(fake), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); +}); + +for (const value of [null, undefined, 0, 1, 1n, '', 'x', true, Symbol('s'), [], + {}, () => {}]) { + test(`ArrayBufferView rejects ${typeof value} ${String(value)}`, () => { + assert.throws( + () => converters.ArrayBufferView(value), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + }); +}