Skip to content

Commit 3618c25

Browse files
committed
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 dictionaries, enums, sequences, interfaces, required arguments, and integers. - Move WebCrypto to the shared converters, keeping compatibility wrappers for existing BufferSource and BigInteger behavior. - Use shared converters from Blob, Performance, Web Locks, and structured clone option handling. - Add focused tests for core converters, WebCrypto converters, integer conversion, and buffer source behavior. Fixes: - Make core BufferSource and Uint8Array reject resizable ArrayBuffer and growable SharedArrayBuffer backing stores unless allowed. This is non-breaking because the core behavior has no runtime consumers other than WebCrypto. WebCrypto keeps its legacy behavior until a semver-major follow-up opts in to the correct 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 Number approximation behavior. - Normalize mathematical modulo results to +0 where Web IDL requires it. - Cover detached buffers, resizable-backed views, growable-backed views, cross-realm buffer sources, and mutation-after-call behavior. Signed-off-by: Filip Skokan <panva.ip@gmail.com>
1 parent 21436f0 commit 3618c25

12 files changed

Lines changed: 1569 additions & 646 deletions

lib/internal/blob.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ const {
5454
const { inspect } = require('internal/util/inspect');
5555
const {
5656
converters,
57-
convertToInt,
5857
createSequenceConverter,
5958
} = require('internal/webidl');
6059

@@ -245,10 +244,14 @@ class Blob {
245244
if (!isBlob(this))
246245
throw new ERR_INVALID_THIS('Blob');
247246

248-
// Coerce values to int
249-
const opts = { __proto__: null, signed: true };
250-
start = convertToInt('start', start, 64, opts);
251-
end = convertToInt('end', end, 64, opts);
247+
start = converters['long long'](
248+
start,
249+
{ __proto__: null, context: 'start' },
250+
);
251+
end = converters['long long'](
252+
end,
253+
{ __proto__: null, context: 'end' },
254+
);
252255

253256
if (start < 0) {
254257
start = MathMax(this[kLength] + start, 0);

lib/internal/crypto/webidl.js

Lines changed: 31 additions & 262 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,33 @@
11
'use strict';
22

3-
// Adapted from the following sources
4-
// - https://github.com/jsdom/webidl-conversions
5-
// Copyright Domenic Denicola. Licensed under BSD-2-Clause License.
6-
// Original license at https://github.com/jsdom/webidl-conversions/blob/master/LICENSE.md.
7-
// - https://github.com/denoland/deno
8-
// Copyright Deno authors. Licensed under MIT License.
9-
// Original license at https://github.com/denoland/deno/blob/main/LICENSE.md.
10-
// Changes include using primordials and stripping the code down to only what
11-
// WebCryptoAPI needs.
12-
133
const {
14-
ArrayBufferIsView,
154
ArrayPrototypeIncludes,
16-
ArrayPrototypePush,
17-
ArrayPrototypeSort,
185
MathPow,
19-
MathTrunc,
20-
Number,
21-
NumberIsFinite,
226
NumberParseInt,
237
ObjectPrototypeHasOwnProperty,
24-
ObjectPrototypeIsPrototypeOf,
258
SafeArrayIterator,
26-
String,
279
StringPrototypeStartsWith,
2810
StringPrototypeToLowerCase,
29-
TypedArrayPrototypeGetBuffer,
30-
TypedArrayPrototypeGetSymbolToStringTag,
3111
} = primordials;
3212

33-
const {
34-
converters: sharedConverters,
35-
makeException,
36-
createEnumConverter,
37-
createSequenceConverter,
38-
} = require('internal/webidl');
39-
4013
const {
4114
lazyDOMException,
4215
kEmptyObject,
43-
setOwnProperty,
4416
} = require('internal/util');
4517
const { CryptoKey } = require('internal/crypto/webcrypto');
4618
const {
4719
validateMaxBufferLength,
4820
kNamedCurveAliases,
4921
} = require('internal/crypto/util');
50-
const { isSharedArrayBuffer } = require('internal/util/types');
51-
52-
// https://tc39.es/ecma262/#sec-tonumber
53-
function toNumber(value, opts = kEmptyObject) {
54-
switch (typeof value) {
55-
case 'number':
56-
return value;
57-
case 'bigint':
58-
throw makeException(
59-
'is a BigInt and cannot be converted to a number.',
60-
opts);
61-
case 'symbol':
62-
throw makeException(
63-
'is a Symbol and cannot be converted to a number.',
64-
opts);
65-
default:
66-
return Number(value);
67-
}
68-
}
69-
70-
function type(V) {
71-
if (V === null)
72-
return 'Null';
73-
74-
switch (typeof V) {
75-
case 'undefined':
76-
return 'Undefined';
77-
case 'boolean':
78-
return 'Boolean';
79-
case 'number':
80-
return 'Number';
81-
case 'string':
82-
return 'String';
83-
case 'symbol':
84-
return 'Symbol';
85-
case 'bigint':
86-
return 'BigInt';
87-
case 'object': // Fall through
88-
case 'function': // Fall through
89-
default:
90-
// Per ES spec, typeof returns an implementation-defined value that is not
91-
// any of the existing ones for uncallable non-standard exotic objects.
92-
// Yet Type() which the Web IDL spec depends on returns Object for such
93-
// cases. So treat the default case as an object.
94-
return 'Object';
95-
}
96-
}
97-
98-
const integerPart = MathTrunc;
22+
const {
23+
converters: webidl,
24+
createDictionaryConverter,
25+
createEnumConverter,
26+
createInterfaceConverter,
27+
createSequenceConverter,
28+
requiredArguments,
29+
type,
30+
} = require('internal/webidl');
9931

10032
function validateByteLength(buf, name, target) {
10133
if (buf.byteLength !== target) {
@@ -119,79 +51,7 @@ function namedCurveValidator(V, dict) {
11951
'NotSupportedError');
12052
}
12153

122-
// This was updated to only consider bitlength up to 32 used by WebCryptoAPI
123-
function createIntegerConversion(bitLength) {
124-
const lowerBound = 0;
125-
const upperBound = MathPow(2, bitLength) - 1;
126-
127-
const twoToTheBitLength = MathPow(2, bitLength);
128-
129-
return (V, opts = kEmptyObject) => {
130-
let x = toNumber(V, opts);
131-
132-
if (opts.enforceRange) {
133-
if (!NumberIsFinite(x)) {
134-
throw makeException(
135-
'is not a finite number.',
136-
opts);
137-
}
138-
139-
x = integerPart(x);
140-
141-
if (x < lowerBound || x > upperBound) {
142-
throw makeException(
143-
`is outside the expected range of ${lowerBound} to ${upperBound}.`,
144-
{ __proto__: null, ...opts, code: 'ERR_OUT_OF_RANGE' },
145-
);
146-
}
147-
148-
return x;
149-
}
150-
151-
if (!NumberIsFinite(x) || x === 0) {
152-
return 0;
153-
}
154-
155-
x = integerPart(x);
156-
157-
if (x >= lowerBound && x <= upperBound) {
158-
return x;
159-
}
160-
161-
x = x % twoToTheBitLength;
162-
163-
return x;
164-
};
165-
}
166-
167-
const converters = {};
168-
169-
converters.boolean = (val) => !!val;
170-
converters.octet = createIntegerConversion(8);
171-
converters['unsigned short'] = createIntegerConversion(16);
172-
converters['unsigned long'] = createIntegerConversion(32);
173-
174-
converters.DOMString = function(V, opts = kEmptyObject) {
175-
if (typeof V === 'string') {
176-
return V;
177-
} else if (typeof V === 'symbol') {
178-
throw makeException(
179-
'is a Symbol and cannot be converted to a string.',
180-
opts);
181-
}
182-
183-
return String(V);
184-
};
185-
186-
converters.object = (V, opts) => {
187-
if (type(V) !== 'Object') {
188-
throw makeException(
189-
'is not an object.',
190-
opts);
191-
}
192-
193-
return V;
194-
};
54+
const converters = { ...webidl };
19555

19656
/**
19757
* @param {string | object} V - The hash algorithm identifier (string or object).
@@ -205,117 +65,6 @@ function ensureSHA(V, label) {
20565
`Only SHA hashes are supported in ${label}`, 'NotSupportedError');
20666
}
20767

208-
converters.Uint8Array = (V, opts = kEmptyObject) => {
209-
if (!ArrayBufferIsView(V) ||
210-
TypedArrayPrototypeGetSymbolToStringTag(V) !== 'Uint8Array') {
211-
throw makeException(
212-
'is not an Uint8Array object.',
213-
opts);
214-
}
215-
if (isSharedArrayBuffer(TypedArrayPrototypeGetBuffer(V))) {
216-
throw makeException(
217-
'is a view on a SharedArrayBuffer, which is not allowed.',
218-
opts);
219-
}
220-
221-
return V;
222-
};
223-
224-
converters.BufferSource = sharedConverters.BufferSource;
225-
226-
converters['sequence<DOMString>'] = createSequenceConverter(
227-
converters.DOMString);
228-
229-
function requiredArguments(length, required, opts = kEmptyObject) {
230-
if (length < required) {
231-
throw makeException(
232-
`${required} argument${
233-
required === 1 ? '' : 's'
234-
} required, but only ${length} present.`,
235-
{ __proto__: null, ...opts, context: '', code: 'ERR_MISSING_ARGS' });
236-
}
237-
}
238-
239-
function createDictionaryConverter(name, dictionaries) {
240-
let hasRequiredKey = false;
241-
const allMembers = [];
242-
for (let i = 0; i < dictionaries.length; i++) {
243-
const member = dictionaries[i];
244-
if (member.required) {
245-
hasRequiredKey = true;
246-
}
247-
ArrayPrototypePush(allMembers, member);
248-
}
249-
ArrayPrototypeSort(allMembers, (a, b) => {
250-
if (a.key === b.key) {
251-
return 0;
252-
}
253-
return a.key < b.key ? -1 : 1;
254-
});
255-
256-
return function(V, opts = kEmptyObject) {
257-
const typeV = type(V);
258-
switch (typeV) {
259-
case 'Undefined':
260-
case 'Null':
261-
case 'Object':
262-
break;
263-
default:
264-
throw makeException(
265-
'can not be converted to a dictionary',
266-
opts);
267-
}
268-
const esDict = V;
269-
const idlDict = {};
270-
271-
// Fast path null and undefined.
272-
if (V == null && !hasRequiredKey) {
273-
return idlDict;
274-
}
275-
276-
for (const member of new SafeArrayIterator(allMembers)) {
277-
const key = member.key;
278-
279-
let esMemberValue;
280-
if (typeV === 'Undefined' || typeV === 'Null') {
281-
esMemberValue = undefined;
282-
} else {
283-
esMemberValue = esDict[key];
284-
}
285-
286-
if (esMemberValue !== undefined) {
287-
const context = `'${key}' of '${name}'${
288-
opts.context ? ` (${opts.context})` : ''
289-
}`;
290-
const idlMemberValue = member.converter(esMemberValue, {
291-
__proto__: null,
292-
...opts,
293-
context,
294-
});
295-
member.validator?.(idlMemberValue, esDict);
296-
setOwnProperty(idlDict, key, idlMemberValue);
297-
} else if (member.required) {
298-
throw makeException(
299-
`can not be converted to '${name}' because '${key}' is required in '${name}'.`,
300-
{ __proto__: null, ...opts, code: 'ERR_MISSING_OPTION' });
301-
}
302-
}
303-
304-
return idlDict;
305-
};
306-
}
307-
308-
function createInterfaceConverter(name, prototype) {
309-
return (V, opts) => {
310-
if (!ObjectPrototypeIsPrototypeOf(prototype, V)) {
311-
throw makeException(
312-
`is not of type ${name}.`,
313-
opts);
314-
}
315-
return V;
316-
};
317-
}
318-
31968
converters.AlgorithmIdentifier = (V, opts) => {
32069
// Union for (object or DOMString)
32170
if (type(V) === 'Object') {
@@ -365,7 +114,27 @@ const dictAlgorithm = [
365114
converters.Algorithm = createDictionaryConverter(
366115
'Algorithm', dictAlgorithm);
367116

368-
converters.BigInteger = converters.Uint8Array;
117+
// TODO(panva): Reject resizable backing stores in a semver-major with:
118+
// converters.BigInteger = webidl.Uint8Array;
119+
converters.BigInteger = (V, opts = kEmptyObject) => {
120+
return webidl.Uint8Array(V, {
121+
__proto__: null,
122+
...opts,
123+
allowResizable: true,
124+
allowShared: false,
125+
});
126+
};
127+
128+
// TODO(panva): Reject resizable backing stores in a semver-major by
129+
// removing this altogether.
130+
converters.BufferSource = (V, opts = kEmptyObject) => {
131+
return webidl.BufferSource(V, {
132+
__proto__: null,
133+
...opts,
134+
allowResizable: opts.allowResizable === undefined ?
135+
true : opts.allowResizable,
136+
});
137+
};
369138

370139
const dictRsaKeyGenParams = [
371140
...new SafeArrayIterator(dictAlgorithm),

lib/internal/locks.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const kConstructLock = Symbol('kConstructLock');
4343
const kConstructLockManager = Symbol('kConstructLockManager');
4444

4545
// WebIDL dictionary LockOptions
46-
const convertLockOptions = createDictionaryConverter([
46+
const convertLockOptions = createDictionaryConverter('LockOptions', [
4747
{
4848
key: 'mode',
4949
converter: createEnumConverter('LockMode', [

lib/internal/perf/performance.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const timerify = require('internal/perf/timerify');
4444
const { customInspectSymbol: kInspect, kEnumerableProperty, kEmptyObject } = require('internal/util');
4545
const { inspect } = require('util');
4646
const { validateThisInternalField } = require('internal/validators');
47-
const { convertToInt } = require('internal/webidl');
47+
const { converters } = require('internal/webidl');
4848

4949
const kPerformanceBrand = Symbol('performance');
5050

@@ -144,8 +144,10 @@ class Performance extends EventTarget {
144144
if (arguments.length === 0) {
145145
throw new ERR_MISSING_ARGS('maxSize');
146146
}
147-
// unsigned long
148-
maxSize = convertToInt('maxSize', maxSize, 32);
147+
maxSize = converters['unsigned long'](
148+
maxSize,
149+
{ __proto__: null, context: 'maxSize' },
150+
);
149151
return setResourceTimingBufferSize(maxSize);
150152
}
151153

0 commit comments

Comments
 (0)