node/test/parallel/test-internal-webidl-buffer-source.js
Filip Skokan ff101999ed
lib: refactor internal webidl converters
Rework lib/internal/webidl.js into a documented shared converter module
that follows the Web IDL conversion algorithms more closely.

Improvements:
- Add documented converters and helper factories for primitive values,
  dictionaries, enums, sequences, interfaces, required arguments,
  integers, `Uint8Array`, and `BufferSource`.
- Move WebCrypto onto the shared converters, while keeping compatibility
  wrappers for its existing `BufferSource` and `BigInteger` behavior.
- Use shared converters from Blob, Performance, Web Locks, and
  structured clone option handling.
- Add benchmarks for `ConvertToInt` and WebCrypto Web IDL converter hot
  paths.
- Add focused tests for core converters, WebCrypto converters, integer
  conversion, and buffer source behavior.

Fixes:
- Make the shared `BufferSource` and `Uint8Array` converters reject
  resizable `ArrayBuffer` and growable `SharedArrayBuffer` backing
  stores unless explicitly allowed. WebCrypto preserves its legacy
  resizable backing-store behavior through compatibility wrappers until
  a semver-major follow-up can opt in to the stricter behavior.
- Use Web IDL `ToNumber` and `ToString` behavior for BigInt, Symbol, and
  object primitive conversion.
- Use exact BigInt modulo for 64-bit `ConvertToInt` wrapping and
  document the final Number approximation behavior.
- Normalize mathematical modulo results to `+0` where Web IDL requires
  it.
- Process inherited dictionaries in least-derived to most-derived order,
  sorting members only within each dictionary level.
- Use `IteratorComplete` truthiness for sequence conversion.
- Cover detached buffers, resizable-backed views, growable-backed views,
  cross-realm buffer sources, mutation-after-call behavior, inherited
  dictionary member order, and sequence iterator completion behavior.

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/62979
Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
2026-05-04 15:21:45 +00:00

298 lines
8.8 KiB
JavaScript

// Flags: --expose-internals
'use strict';
require('../common');
const assert = require('assert');
const { test } = require('node:test');
const vm = require('vm');
const { converters } = require('internal/webidl');
const TYPED_ARRAY_CTORS = [
Uint8Array, Int8Array, Uint8ClampedArray,
Uint16Array, Int16Array,
Uint32Array, Int32Array,
Float16Array, Float32Array, Float64Array,
BigInt64Array, BigUint64Array,
];
function createGrowableSharedArrayBufferView(Ctor) {
const buffer = createGrowableSharedArrayBuffer();
const view = new Ctor(buffer);
return view;
}
function createGrowableSharedArrayBuffer() {
const buffer = new SharedArrayBuffer(0, { maxByteLength: 1 });
assert.strictEqual(buffer.growable, true);
return buffer;
}
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 cross-realm buffer sources', () => {
const context = vm.createContext();
{
const ab = vm.runInContext('new ArrayBuffer(0)', context);
assert.strictEqual(converters.BufferSource(ab), ab);
}
{
const dv = vm.runInContext('new DataView(new ArrayBuffer(0))', context);
assert.strictEqual(converters.BufferSource(dv), dv);
}
for (const Ctor of TYPED_ARRAY_CTORS) {
const ta = vm.runInContext(
`new ${Ctor.name}(new ArrayBuffer(0))`,
context,
);
assert.strictEqual(converters.BufferSource(ta), ta);
}
});
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' },
);
assert.throws(
() => converters.BufferSource(view, { allowShared: true }),
{ 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' },
);
assert.throws(
() => converters.BufferSource(dv, { allowShared: true }),
{ 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 resizable ArrayBuffer by default', () => {
const ab = new ArrayBuffer(0, { maxByteLength: 1 });
assert.throws(
() => converters.BufferSource(ab),
{ code: 'ERR_INVALID_ARG_TYPE' },
);
});
test('BufferSource handles resizable-backed views with explicit options', () => {
for (const Ctor of [DataView, ...TYPED_ARRAY_CTORS]) {
{
const view = new Ctor(new ArrayBuffer(0, { maxByteLength: 1 }));
assert.throws(
() => converters.BufferSource(view),
{ code: 'ERR_INVALID_ARG_TYPE' },
);
}
{
const view = new Ctor(new ArrayBuffer(0, { maxByteLength: 1 }));
assert.throws(
() => converters.BufferSource(view, { allowResizable: false }),
{ code: 'ERR_INVALID_ARG_TYPE' },
);
}
{
const view = new Ctor(new ArrayBuffer(0, { maxByteLength: 1 }));
assert.strictEqual(converters.BufferSource(view, {
allowResizable: true,
}), view);
}
}
});
test('BufferSource rejects SAB-backed views with explicit options', () => {
for (const Ctor of [DataView, ...TYPED_ARRAY_CTORS]) {
const view = createGrowableSharedArrayBufferView(Ctor);
assert.throws(
() => converters.BufferSource(view, {
allowShared: true,
allowResizable: true,
}),
{ code: 'ERR_INVALID_ARG_TYPE' },
);
}
});
test('AllowSharedBufferSource accepts ArrayBuffer and SharedArrayBuffer', () => {
const ab = new ArrayBuffer(8);
const sab = new SharedArrayBuffer(8);
assert.strictEqual(converters.AllowSharedBufferSource(ab), ab);
assert.strictEqual(converters.AllowSharedBufferSource(sab), sab);
});
test('AllowSharedBufferSource accepts cross-realm buffers', () => {
const context = vm.createContext();
const ab = vm.runInContext('new ArrayBuffer(0)', context);
const sab = vm.runInContext('new SharedArrayBuffer(0)', context);
assert.strictEqual(converters.AllowSharedBufferSource(ab), ab);
assert.strictEqual(converters.AllowSharedBufferSource(sab), sab);
});
test('AllowSharedBufferSource accepts ArrayBuffer and SharedArrayBuffer views', () => {
const abView = new Uint8Array(new ArrayBuffer(8));
const sabView = new Uint8Array(new SharedArrayBuffer(8));
const abDataView = new DataView(new ArrayBuffer(8));
const sabDataView = new DataView(new SharedArrayBuffer(8));
assert.strictEqual(converters.AllowSharedBufferSource(abView), abView);
assert.strictEqual(converters.AllowSharedBufferSource(sabView), sabView);
assert.strictEqual(
converters.AllowSharedBufferSource(abDataView), abDataView);
assert.strictEqual(
converters.AllowSharedBufferSource(sabDataView), sabDataView);
});
test('AllowSharedBufferSource handles resizable buffers with explicit options', () => {
const ab = new ArrayBuffer(0, { maxByteLength: 1 });
const views = [new Uint8Array(ab), new DataView(ab)];
assert.throws(
() => converters.AllowSharedBufferSource(ab),
{ code: 'ERR_INVALID_ARG_TYPE' },
);
for (const view of views) {
assert.throws(
() => converters.AllowSharedBufferSource(view),
{ code: 'ERR_INVALID_ARG_TYPE' },
);
}
assert.strictEqual(converters.AllowSharedBufferSource(ab, {
allowResizable: true,
}), ab);
for (const view of views) {
assert.strictEqual(converters.AllowSharedBufferSource(view, {
allowResizable: true,
}), view);
}
});
test('AllowSharedBufferSource handles growable shared buffers with explicit ' +
'options', () => {
const sab = createGrowableSharedArrayBuffer();
const views = [new Uint8Array(sab), new DataView(sab)];
assert.throws(
() => converters.AllowSharedBufferSource(sab),
{ code: 'ERR_INVALID_ARG_TYPE' },
);
for (const view of views) {
assert.throws(
() => converters.AllowSharedBufferSource(view),
{ code: 'ERR_INVALID_ARG_TYPE' },
);
}
assert.strictEqual(converters.AllowSharedBufferSource(sab, {
allowResizable: true,
}), sab);
for (const view of views) {
assert.strictEqual(converters.AllowSharedBufferSource(view, {
allowResizable: true,
}), view);
}
});
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(`AllowSharedBufferSource rejects ${typeof value} ${String(value)}`, () => {
assert.throws(
() => converters.AllowSharedBufferSource(value),
{ code: 'ERR_INVALID_ARG_TYPE' },
);
});
}