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>
616 lines
16 KiB
JavaScript
616 lines
16 KiB
JavaScript
// Flags: --expose-internals
|
|
'use strict';
|
|
|
|
require('../common');
|
|
const assert = require('assert');
|
|
const vm = require('vm');
|
|
const webidl = require('internal/webidl');
|
|
|
|
const { converters } = webidl;
|
|
const opts = {
|
|
__proto__: null,
|
|
prefix: 'Prefix',
|
|
context: 'Context',
|
|
};
|
|
function createGrowableSharedArrayBufferView() {
|
|
const buffer = new SharedArrayBuffer(4, { maxByteLength: 8 });
|
|
const view = new Uint8Array(buffer);
|
|
assert.strictEqual(view.buffer.growable, true);
|
|
return view;
|
|
}
|
|
|
|
function assertInvalidArgType(fn) {
|
|
assert.throws(fn, {
|
|
name: 'TypeError',
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
});
|
|
}
|
|
|
|
function assertPlainTypeError(fn) {
|
|
assert.throws(fn, (err) => {
|
|
assert(err instanceof TypeError);
|
|
assert.strictEqual(err.name, 'TypeError');
|
|
assert.strictEqual(err.code, undefined);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function assertSameError(fn, expected) {
|
|
assert.throws(fn, (err) => {
|
|
assert.strictEqual(err, expected);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
assert.strictEqual(webidl.type(undefined), 'Undefined');
|
|
assert.strictEqual(webidl.type(null), 'Null');
|
|
assert.strictEqual(webidl.type(false), 'Boolean');
|
|
assert.strictEqual(webidl.type(''), 'String');
|
|
assert.strictEqual(webidl.type(Symbol()), 'Symbol');
|
|
assert.strictEqual(webidl.type(0), 'Number');
|
|
assert.strictEqual(webidl.type(0n), 'BigInt');
|
|
assert.strictEqual(webidl.type({}), 'Object');
|
|
assert.strictEqual(webidl.type(function fn() {}), 'Object');
|
|
|
|
assert.strictEqual(converters.boolean(0), false);
|
|
assert.strictEqual(converters.boolean('false'), true);
|
|
{
|
|
function fn() {}
|
|
assert.strictEqual(converters.object(fn), fn);
|
|
}
|
|
assert.throws(() => converters.object(null, opts), {
|
|
name: 'TypeError',
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: 'Prefix: Context is not an object.',
|
|
});
|
|
|
|
assert.strictEqual(converters.DOMString(null), 'null');
|
|
assert.throws(() => converters.DOMString(Symbol(), opts), {
|
|
name: 'TypeError',
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: 'Prefix: Context is a Symbol and cannot be converted to a string.',
|
|
});
|
|
|
|
for (const value of [
|
|
Object(Symbol()),
|
|
{ toString() { return Symbol(); } },
|
|
{ toString() { return {}; }, valueOf() { return Symbol(); } },
|
|
{ [Symbol.toPrimitive]() { return Symbol(); } },
|
|
]) {
|
|
assertPlainTypeError(() => converters.DOMString(value, opts));
|
|
}
|
|
assert.strictEqual(converters.DOMString(1n), '1');
|
|
assert.strictEqual(converters.DOMString(Object(1n)), '1');
|
|
assert.strictEqual(converters.DOMString({
|
|
toString() { return {}; },
|
|
valueOf() { return 1n; },
|
|
}), '1');
|
|
assert.strictEqual(converters.DOMString({
|
|
[Symbol.toPrimitive]() { return 1n; },
|
|
}), '1');
|
|
{
|
|
const value = Object(Symbol());
|
|
Object.defineProperty(value, Symbol.toPrimitive, {
|
|
__proto__: null,
|
|
value() { return 'symbol object'; },
|
|
});
|
|
assert.strictEqual(converters.DOMString(value), 'symbol object');
|
|
}
|
|
|
|
{
|
|
const calls = [];
|
|
const value = {
|
|
[Symbol.toPrimitive](hint) {
|
|
calls.push(hint);
|
|
return 7;
|
|
},
|
|
toString() {
|
|
calls.push('toString');
|
|
return '1';
|
|
},
|
|
valueOf() {
|
|
calls.push('valueOf');
|
|
return 1;
|
|
},
|
|
};
|
|
|
|
assert.strictEqual(converters.DOMString(value), '7');
|
|
assert.deepStrictEqual(calls, ['string']);
|
|
}
|
|
|
|
for (const { value, expected } of [
|
|
{
|
|
value: {
|
|
[Symbol.toPrimitive]: undefined,
|
|
toString() { return 'eight'; },
|
|
},
|
|
expected: 'eight',
|
|
},
|
|
{
|
|
value: {
|
|
[Symbol.toPrimitive]: null,
|
|
toString() { return 'nine'; },
|
|
},
|
|
expected: 'nine',
|
|
},
|
|
]) {
|
|
assert.strictEqual(converters.DOMString(value), expected);
|
|
}
|
|
|
|
{
|
|
const calls = [];
|
|
const value = {
|
|
toString: 1,
|
|
valueOf() {
|
|
calls.push('valueOf');
|
|
return 10;
|
|
},
|
|
};
|
|
|
|
assert.strictEqual(converters.DOMString(value), '10');
|
|
assert.deepStrictEqual(calls, ['valueOf']);
|
|
}
|
|
|
|
{
|
|
const calls = [];
|
|
const value = {
|
|
toString() {
|
|
calls.push('toString');
|
|
return {};
|
|
},
|
|
valueOf() {
|
|
calls.push('valueOf');
|
|
return 11;
|
|
},
|
|
};
|
|
|
|
assert.strictEqual(converters.DOMString(value), '11');
|
|
assert.deepStrictEqual(calls, ['toString', 'valueOf']);
|
|
}
|
|
|
|
for (const value of [
|
|
{ [Symbol.toPrimitive]: 1 },
|
|
{ [Symbol.toPrimitive]() { return {}; } },
|
|
{
|
|
toString() { return {}; },
|
|
valueOf() { return {}; },
|
|
},
|
|
]) {
|
|
assertPlainTypeError(() => converters.DOMString(value, opts));
|
|
}
|
|
|
|
{
|
|
const sentinel = new TypeError('sentinel');
|
|
assertSameError(() => converters.DOMString({
|
|
get [Symbol.toPrimitive]() {
|
|
throw sentinel;
|
|
},
|
|
}), sentinel);
|
|
}
|
|
|
|
{
|
|
const sentinel = new TypeError('sentinel');
|
|
assertSameError(() => converters.DOMString({
|
|
get toString() {
|
|
throw sentinel;
|
|
},
|
|
valueOf() {
|
|
return 1;
|
|
},
|
|
}), sentinel);
|
|
}
|
|
|
|
{
|
|
const sentinel = new TypeError('sentinel');
|
|
assertSameError(() => converters.DOMString({
|
|
toString() {
|
|
throw sentinel;
|
|
},
|
|
valueOf() {
|
|
return 1;
|
|
},
|
|
}), sentinel);
|
|
}
|
|
|
|
{
|
|
assert.strictEqual(converters.octet(-1), 255);
|
|
assert.strictEqual(converters['unsigned short'](-1), 0xFFFF);
|
|
assert.strictEqual(converters['unsigned long'](-1), 0xFFFF_FFFF);
|
|
assert.strictEqual(converters['long long'](-1), -1);
|
|
assert.strictEqual(converters.octet(2.5, {
|
|
__proto__: null,
|
|
clamp: true,
|
|
}), 2);
|
|
assert.throws(() => converters.octet(256, {
|
|
__proto__: null,
|
|
...opts,
|
|
enforceRange: true,
|
|
}), {
|
|
name: 'TypeError',
|
|
code: 'ERR_OUT_OF_RANGE',
|
|
message: 'Prefix: Context is outside the expected range of 0 to 255.',
|
|
});
|
|
}
|
|
|
|
{
|
|
const converter = webidl.createEnumConverter('Example', ['one', 'two']);
|
|
|
|
assert.strictEqual(converter('one'), 'one');
|
|
assert.throws(() => converter(Symbol(), opts), {
|
|
name: 'TypeError',
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: 'Prefix: Context is a Symbol and cannot be converted to a string.',
|
|
});
|
|
assert.throws(() => converter('three', opts), {
|
|
name: 'TypeError',
|
|
code: 'ERR_INVALID_ARG_VALUE',
|
|
message: "Prefix: Context 'three' is not a valid enum value " +
|
|
'of type Example.',
|
|
});
|
|
}
|
|
|
|
assert.throws(() => webidl.requiredArguments(1, 2, opts), {
|
|
name: 'TypeError',
|
|
code: 'ERR_MISSING_ARGS',
|
|
message: 'Prefix: 2 arguments required, but only 1 present.',
|
|
});
|
|
|
|
{
|
|
const ab = new ArrayBuffer(8, { maxByteLength: 16 });
|
|
const view = new Uint8Array(ab);
|
|
const dataView = new DataView(ab);
|
|
|
|
assert.strictEqual(ab.resizable, true);
|
|
assertInvalidArgType(() => converters.BufferSource(ab));
|
|
assertInvalidArgType(() => converters.BufferSource(view));
|
|
assertInvalidArgType(() => converters.BufferSource(dataView));
|
|
assertInvalidArgType(() => converters.Uint8Array(view));
|
|
|
|
const disallowResizable = {
|
|
__proto__: null,
|
|
allowResizable: false,
|
|
};
|
|
assertInvalidArgType(() => converters.BufferSource(ab, disallowResizable));
|
|
assertInvalidArgType(() => converters.BufferSource(view, disallowResizable));
|
|
assertInvalidArgType(() => converters.BufferSource(dataView,
|
|
disallowResizable));
|
|
|
|
const allowResizable = {
|
|
__proto__: null,
|
|
allowResizable: true,
|
|
};
|
|
assert.strictEqual(converters.BufferSource(ab, allowResizable), ab);
|
|
assert.strictEqual(converters.BufferSource(view, allowResizable), view);
|
|
assert.strictEqual(converters.BufferSource(dataView, allowResizable),
|
|
dataView);
|
|
assert.strictEqual(converters.Uint8Array(view, allowResizable), view);
|
|
}
|
|
|
|
{
|
|
const ab = new ArrayBuffer(8);
|
|
const view = new Uint8Array(ab);
|
|
const dataView = new DataView(ab);
|
|
|
|
structuredClone(ab, { transfer: [ab] });
|
|
assert.strictEqual(ab.detached, true);
|
|
assert.strictEqual(converters.BufferSource(ab), ab);
|
|
assert.strictEqual(converters.BufferSource(view), view);
|
|
assert.strictEqual(converters.BufferSource(dataView), dataView);
|
|
}
|
|
|
|
{
|
|
const ab = new ArrayBuffer(8, { maxByteLength: 16 });
|
|
const view = new Uint8Array(ab);
|
|
const dataView = new DataView(ab);
|
|
const allowResizable = {
|
|
__proto__: null,
|
|
allowResizable: true,
|
|
};
|
|
|
|
structuredClone(ab, { transfer: [ab] });
|
|
assert.strictEqual(ab.detached, true);
|
|
assert.strictEqual(ab.resizable, true);
|
|
assertInvalidArgType(() => converters.BufferSource(ab));
|
|
assertInvalidArgType(() => converters.BufferSource(view));
|
|
assertInvalidArgType(() => converters.BufferSource(dataView));
|
|
assertInvalidArgType(() => converters.Uint8Array(view));
|
|
|
|
const disallowResizable = {
|
|
__proto__: null,
|
|
allowResizable: false,
|
|
};
|
|
assertInvalidArgType(() => converters.BufferSource(ab, disallowResizable));
|
|
assertInvalidArgType(() => converters.BufferSource(view, disallowResizable));
|
|
assertInvalidArgType(() => converters.BufferSource(dataView,
|
|
disallowResizable));
|
|
|
|
assert.strictEqual(converters.BufferSource(ab, allowResizable), ab);
|
|
assert.strictEqual(converters.BufferSource(view, allowResizable), view);
|
|
assert.strictEqual(converters.BufferSource(dataView, allowResizable),
|
|
dataView);
|
|
assert.strictEqual(converters.Uint8Array(view, allowResizable), view);
|
|
}
|
|
|
|
{
|
|
const ab = new ArrayBuffer(8, { maxByteLength: 16 });
|
|
const view = new Uint8Array(ab);
|
|
|
|
assert.strictEqual(converters.BufferSource(ab, {
|
|
__proto__: null,
|
|
allowResizable: true,
|
|
}), ab);
|
|
assert.strictEqual(converters.BufferSource(view, {
|
|
__proto__: null,
|
|
allowResizable: true,
|
|
}), view);
|
|
|
|
ab.resize(4);
|
|
assert.strictEqual(ab.byteLength, 4);
|
|
assert.strictEqual(view.byteLength, 4);
|
|
|
|
ab.resize(12);
|
|
assert.strictEqual(ab.byteLength, 12);
|
|
assert.strictEqual(view.byteLength, 12);
|
|
}
|
|
|
|
{
|
|
const converter = webidl.createDictionaryConverter('Example', [
|
|
{
|
|
key: 'value',
|
|
converter: converters.DOMString,
|
|
required: true,
|
|
},
|
|
]);
|
|
|
|
const idlDict = converter({ value: 1 });
|
|
assert.deepStrictEqual(idlDict, { __proto__: null, value: '1' });
|
|
|
|
assert.throws(() => converter({}, opts), {
|
|
name: 'TypeError',
|
|
code: 'ERR_MISSING_OPTION',
|
|
message: "Prefix: Context cannot be converted to 'Example' because " +
|
|
"'value' is required in 'Example'.",
|
|
});
|
|
}
|
|
|
|
{
|
|
const calls = [];
|
|
const converter = webidl.createDictionaryConverter('Derived', [
|
|
[
|
|
{
|
|
key: 'zBase',
|
|
converter(value) {
|
|
calls.push(`base z:${value}`);
|
|
return value;
|
|
},
|
|
},
|
|
{
|
|
key: 'aBase',
|
|
converter(value) {
|
|
calls.push(`base a:${value}`);
|
|
return value;
|
|
},
|
|
},
|
|
],
|
|
[
|
|
{
|
|
key: 'zDerived',
|
|
converter(value) {
|
|
calls.push(`derived z:${value}`);
|
|
return value;
|
|
},
|
|
},
|
|
{
|
|
key: 'aDerived',
|
|
converter(value) {
|
|
calls.push(`derived a:${value}`);
|
|
return value;
|
|
},
|
|
},
|
|
],
|
|
]);
|
|
|
|
assert.deepStrictEqual(converter({
|
|
zBase: 1,
|
|
aBase: 2,
|
|
zDerived: 3,
|
|
aDerived: 4,
|
|
}), {
|
|
__proto__: null,
|
|
aBase: 2,
|
|
zBase: 1,
|
|
aDerived: 4,
|
|
zDerived: 3,
|
|
});
|
|
assert.deepStrictEqual(calls, [
|
|
'base a:2',
|
|
'base z:1',
|
|
'derived a:4',
|
|
'derived z:3',
|
|
]);
|
|
}
|
|
|
|
{
|
|
const converter = webidl.createDictionaryConverter('Example', [
|
|
{
|
|
key: 'same',
|
|
converter(value) {
|
|
return `first:${value}`;
|
|
},
|
|
},
|
|
{
|
|
key: 'same',
|
|
converter(value) {
|
|
return `second:${value}`;
|
|
},
|
|
},
|
|
]);
|
|
|
|
assert.deepStrictEqual(converter({ same: 1 }), {
|
|
__proto__: null,
|
|
same: 'second:1',
|
|
});
|
|
}
|
|
|
|
{
|
|
const converter = converters['sequence<DOMString>'];
|
|
assert.deepStrictEqual(converter([1, 2]), ['1', '2']);
|
|
|
|
assert.throws(() => converter([Symbol()]), {
|
|
name: 'TypeError',
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: 'Value[0] is a Symbol and cannot be converted to a string.',
|
|
});
|
|
|
|
assert.throws(() => converter({ [Symbol.iterator]: 1 }, opts), {
|
|
name: 'TypeError',
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: 'Prefix: Context cannot be converted to sequence.',
|
|
});
|
|
|
|
assert.throws(() => converter({
|
|
[Symbol.iterator]() {
|
|
return {};
|
|
},
|
|
}, opts), {
|
|
name: 'TypeError',
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: 'Prefix: Context cannot be converted to sequence.',
|
|
});
|
|
|
|
assert.throws(() => converter({
|
|
[Symbol.iterator]() {
|
|
return {
|
|
next() {
|
|
return null;
|
|
},
|
|
};
|
|
},
|
|
}, opts), {
|
|
name: 'TypeError',
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: 'Prefix: Context cannot be converted to sequence.',
|
|
});
|
|
|
|
assert.throws(() => converter([Symbol()], opts), {
|
|
name: 'TypeError',
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: 'Prefix: Context[0] is a Symbol and cannot be converted ' +
|
|
'to a string.',
|
|
});
|
|
|
|
assert.deepStrictEqual(converter({
|
|
[Symbol.iterator]() {
|
|
return {
|
|
next() {
|
|
return { done: 1, value: Symbol() };
|
|
},
|
|
};
|
|
},
|
|
}), []);
|
|
}
|
|
|
|
{
|
|
class Example {}
|
|
const converter = webidl.createInterfaceConverter(
|
|
'Example',
|
|
Example.prototype);
|
|
const example = new Example();
|
|
|
|
assert.strictEqual(converter(example), example);
|
|
assert.throws(() => converter({}, opts), {
|
|
name: 'TypeError',
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: 'Prefix: Context is not of type Example.',
|
|
});
|
|
}
|
|
|
|
{
|
|
const context = vm.createContext();
|
|
const view = vm.runInContext(
|
|
'new Uint8Array(new ArrayBuffer(0))',
|
|
context,
|
|
);
|
|
assert.strictEqual(converters.Uint8Array(view), view);
|
|
|
|
for (const value of [
|
|
new ArrayBuffer(0),
|
|
new SharedArrayBuffer(0),
|
|
new DataView(new ArrayBuffer(0)),
|
|
new Int8Array(0),
|
|
Object.create(Uint8Array.prototype, {
|
|
[Symbol.toStringTag]: { value: 'Uint8Array' },
|
|
}),
|
|
]) {
|
|
assertInvalidArgType(() => converters.Uint8Array(value));
|
|
}
|
|
}
|
|
|
|
{
|
|
const sab = new SharedArrayBuffer(4);
|
|
const view = new Uint8Array(sab);
|
|
|
|
assertInvalidArgType(() => converters.BufferSource(sab));
|
|
assertInvalidArgType(() => converters.BufferSource(view));
|
|
assertInvalidArgType(() => converters.Uint8Array(view));
|
|
|
|
const allowShared = {
|
|
__proto__: null,
|
|
allowShared: true,
|
|
};
|
|
assertInvalidArgType(() => converters.BufferSource(sab, allowShared));
|
|
assertInvalidArgType(() => converters.BufferSource(view, allowShared));
|
|
assert.strictEqual(converters.AllowSharedBufferSource(sab), sab);
|
|
assert.strictEqual(converters.AllowSharedBufferSource(view), view);
|
|
assert.strictEqual(converters.Uint8Array(view, allowShared), view);
|
|
|
|
const growableView = createGrowableSharedArrayBufferView();
|
|
|
|
assertInvalidArgType(() => converters.BufferSource(growableView, {
|
|
__proto__: null,
|
|
allowShared: true,
|
|
allowResizable: true,
|
|
}));
|
|
assert.throws(() => converters.AllowSharedBufferSource(growableView.buffer, {
|
|
__proto__: null,
|
|
...opts,
|
|
}), {
|
|
name: 'TypeError',
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: 'Prefix: Context is backed by a growable ' +
|
|
'SharedArrayBuffer, which is not allowed.',
|
|
});
|
|
assert.throws(() => converters.AllowSharedBufferSource(growableView, {
|
|
__proto__: null,
|
|
...opts,
|
|
allowResizable: false,
|
|
}), {
|
|
name: 'TypeError',
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: 'Prefix: Context is backed by a growable ' +
|
|
'SharedArrayBuffer, which is not allowed.',
|
|
});
|
|
assert.throws(() => converters.Uint8Array(growableView, {
|
|
__proto__: null,
|
|
...opts,
|
|
allowShared: true,
|
|
}), {
|
|
name: 'TypeError',
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
message: 'Prefix: Context is backed by a growable ' +
|
|
'SharedArrayBuffer, which is not allowed.',
|
|
});
|
|
assert.strictEqual(converters.AllowSharedBufferSource(growableView.buffer, {
|
|
__proto__: null,
|
|
allowResizable: true,
|
|
}), growableView.buffer);
|
|
assert.strictEqual(converters.AllowSharedBufferSource(growableView, {
|
|
__proto__: null,
|
|
allowResizable: true,
|
|
}), growableView);
|
|
assert.strictEqual(converters.Uint8Array(growableView, {
|
|
__proto__: null,
|
|
allowShared: true,
|
|
allowResizable: true,
|
|
}), growableView);
|
|
}
|