diff --git a/benchmark/misc/webcrypto-webidl.js b/benchmark/misc/webcrypto-webidl.js new file mode 100644 index 00000000000000..0f6275ed09517c --- /dev/null +++ b/benchmark/misc/webcrypto-webidl.js @@ -0,0 +1,109 @@ +'use strict'; + +const common = require('../common.js'); + +const bench = common.createBenchmark(main, { + op: [ + 'normalizeAlgorithm-string', + 'normalizeAlgorithm-dict', + 'webidl-dict', + 'webidl-algorithm-identifier-string', + 'webidl-algorithm-identifier-object', + 'webidl-dict-enforce-range', + 'webidl-dict-ensure-sha', + 'webidl-dict-null', + ], + n: [1e6], +}, { flags: ['--expose-internals'] }); + +function main({ n, op }) { + const { normalizeAlgorithm } = require('internal/crypto/util'); + + switch (op) { + case 'normalizeAlgorithm-string': { + // String shortcut + null dictionary (cheapest path). + bench.start(); + for (let i = 0; i < n; i++) + normalizeAlgorithm('SHA-256', 'digest'); + bench.end(n); + break; + } + case 'normalizeAlgorithm-dict': { + // Object input with a dictionary type and no BufferSource members. + const alg = { name: 'ECDSA', hash: 'SHA-256' }; + bench.start(); + for (let i = 0; i < n; i++) + normalizeAlgorithm(alg, 'sign'); + bench.end(n); + break; + } + case 'webidl-dict': { + // WebIDL dictionary converter in isolation. + const webidl = require('internal/crypto/webidl'); + const input = { name: 'AES-GCM', iv: new Uint8Array(12) }; + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.AeadParams(input, opts); + bench.end(n); + break; + } + case 'webidl-algorithm-identifier-string': { + // Exercises converters.AlgorithmIdentifier string path. + const webidl = require('internal/crypto/webidl'); + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.AlgorithmIdentifier('SHA-256', opts); + bench.end(n); + break; + } + case 'webidl-algorithm-identifier-object': { + // Exercises converters.AlgorithmIdentifier object path. + const webidl = require('internal/crypto/webidl'); + const input = { name: 'SHA-256' }; + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.AlgorithmIdentifier(input, opts); + bench.end(n); + break; + } + case 'webidl-dict-enforce-range': { + // Exercises [EnforceRange] integer dictionary members. + const webidl = require('internal/crypto/webidl'); + const input = { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + }; + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.RsaKeyGenParams(input, opts); + bench.end(n); + break; + } + case 'webidl-dict-ensure-sha': { + // Exercises ensureSHA on a hash member. + const webidl = require('internal/crypto/webidl'); + const input = { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }; + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.RsaHashedImportParams(input, opts); + bench.end(n); + break; + } + case 'webidl-dict-null': { + // Exercises the null/undefined path in createDictionaryConverter(). + const webidl = require('internal/crypto/webidl'); + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.JsonWebKey(undefined, opts); + bench.end(n); + break; + } + } +} diff --git a/benchmark/misc/webidl-convert-to-int.js b/benchmark/misc/webidl-convert-to-int.js new file mode 100644 index 00000000000000..52245ee3b6a1a5 --- /dev/null +++ b/benchmark/misc/webidl-convert-to-int.js @@ -0,0 +1,82 @@ +'use strict'; + +const assert = require('assert'); +const common = require('../common.js'); + +const bench = common.createBenchmark(main, { + converter: [ + 'byte', + 'octet', + 'unsigned short', + 'unsigned long', + 'long long', + ], + input: [ + 'integer', + 'fractional', + 'wrap', + 'clamp', + 'enforce-range', + 'object', + ], + n: [1e6], +}, { flags: ['--expose-internals'] }); + +function getConverter(converter) { + switch (converter) { + case 'byte': + return { bitLength: 8, signedness: 'signed' }; + case 'octet': + return { bitLength: 8 }; + case 'unsigned short': + return { bitLength: 16 }; + case 'unsigned long': + return { bitLength: 32 }; + case 'long long': + return { bitLength: 64, signedness: 'signed' }; + default: + throw new Error(`Unsupported converter: ${converter}`); + } +} + +function getInput(input) { + switch (input) { + case 'integer': + return { value: 7 }; + case 'fractional': + return { value: 7.9 }; + case 'wrap': + return { value: 2 ** 63 + 2 ** 11 }; + case 'clamp': + return { value: 300.8, options: { clamp: true } }; + case 'enforce-range': + return { value: 7.9, options: { enforceRange: true } }; + case 'object': + return { + value: { + valueOf() { return 7; }, + }, + }; + default: + throw new Error(`Unsupported input: ${input}`); + } +} + +function main({ n, converter, input }) { + const { convertToInt } = require('internal/webidl'); + const { bitLength, signedness } = getConverter(converter); + const { value, options } = getInput(input); + + let noDead; + bench.start(); + if (options === undefined) { + for (let i = 0; i < n; i++) + noDead = convertToInt(value, bitLength, signedness); + } else { + for (let i = 0; i < n; i++) + noDead = convertToInt(value, bitLength, signedness, options); + } + bench.end(n); + + assert.strictEqual(typeof noDead, 'number'); +} diff --git a/lib/internal/blob.js b/lib/internal/blob.js index 5059b651f467ca..ab01484f1f7313 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -54,7 +54,6 @@ const { const { inspect } = require('internal/util/inspect'); const { converters, - convertToInt, createSequenceConverter, } = require('internal/webidl'); @@ -245,10 +244,14 @@ class Blob { if (!isBlob(this)) throw new ERR_INVALID_THIS('Blob'); - // Coerce values to int - const opts = { __proto__: null, signed: true }; - start = convertToInt('start', start, 64, opts); - end = convertToInt('end', end, 64, opts); + start = converters['long long']( + start, + { __proto__: null, context: 'start' }, + ); + end = converters['long long']( + end, + { __proto__: null, context: 'end' }, + ); if (start < 0) { start = MathMax(this[kLength] + start, 0); diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 0c14bd22d6a9ad..9e87a77c08a2dc 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -1,101 +1,32 @@ 'use strict'; -// Adapted from the following sources -// - https://github.com/jsdom/webidl-conversions -// Copyright Domenic Denicola. Licensed under BSD-2-Clause License. -// Original license at https://github.com/jsdom/webidl-conversions/blob/master/LICENSE.md. -// - https://github.com/denoland/deno -// Copyright Deno authors. Licensed under MIT License. -// Original license at https://github.com/denoland/deno/blob/main/LICENSE.md. -// Changes include using primordials and stripping the code down to only what -// WebCryptoAPI needs. - const { - ArrayBufferIsView, ArrayPrototypeIncludes, - ArrayPrototypePush, - ArrayPrototypeSort, MathPow, - MathTrunc, - Number, - NumberIsFinite, NumberParseInt, ObjectPrototypeHasOwnProperty, - ObjectPrototypeIsPrototypeOf, - SafeArrayIterator, - String, StringPrototypeStartsWith, StringPrototypeToLowerCase, - TypedArrayPrototypeGetBuffer, - TypedArrayPrototypeGetSymbolToStringTag, } = primordials; -const { - converters: sharedConverters, - makeException, - createEnumConverter, - createSequenceConverter, -} = require('internal/webidl'); - const { lazyDOMException, kEmptyObject, - setOwnProperty, } = require('internal/util'); const { CryptoKey } = require('internal/crypto/webcrypto'); const { validateMaxBufferLength, kNamedCurveAliases, } = require('internal/crypto/util'); -const { isSharedArrayBuffer } = require('internal/util/types'); - -// https://tc39.es/ecma262/#sec-tonumber -function toNumber(value, opts = kEmptyObject) { - switch (typeof value) { - case 'number': - return value; - case 'bigint': - throw makeException( - 'is a BigInt and cannot be converted to a number.', - opts); - case 'symbol': - throw makeException( - 'is a Symbol and cannot be converted to a number.', - opts); - default: - return Number(value); - } -} - -function type(V) { - if (V === null) - return 'Null'; - - switch (typeof V) { - case 'undefined': - return 'Undefined'; - case 'boolean': - return 'Boolean'; - case 'number': - return 'Number'; - case 'string': - return 'String'; - case 'symbol': - return 'Symbol'; - case 'bigint': - return 'BigInt'; - case 'object': // Fall through - case 'function': // Fall through - default: - // Per ES spec, typeof returns an implementation-defined value that is not - // any of the existing ones for uncallable non-standard exotic objects. - // Yet Type() which the Web IDL spec depends on returns Object for such - // cases. So treat the default case as an object. - return 'Object'; - } -} - -const integerPart = MathTrunc; +const { + converters: webidl, + createDictionaryConverter, + createEnumConverter, + createInterfaceConverter, + createSequenceConverter, + requiredArguments, + type, +} = require('internal/webidl'); function validateByteLength(buf, name, target) { if (buf.byteLength !== target) { @@ -119,79 +50,7 @@ function namedCurveValidator(V, dict) { 'NotSupportedError'); } -// This was updated to only consider bitlength up to 32 used by WebCryptoAPI -function createIntegerConversion(bitLength) { - const lowerBound = 0; - const upperBound = MathPow(2, bitLength) - 1; - - const twoToTheBitLength = MathPow(2, bitLength); - - return (V, opts = kEmptyObject) => { - let x = toNumber(V, opts); - - if (opts.enforceRange) { - if (!NumberIsFinite(x)) { - throw makeException( - 'is not a finite number.', - opts); - } - - x = integerPart(x); - - if (x < lowerBound || x > upperBound) { - throw makeException( - `is outside the expected range of ${lowerBound} to ${upperBound}.`, - { __proto__: null, ...opts, code: 'ERR_OUT_OF_RANGE' }, - ); - } - - return x; - } - - if (!NumberIsFinite(x) || x === 0) { - return 0; - } - - x = integerPart(x); - - if (x >= lowerBound && x <= upperBound) { - return x; - } - - x = x % twoToTheBitLength; - - return x; - }; -} - -const converters = {}; - -converters.boolean = (val) => !!val; -converters.octet = createIntegerConversion(8); -converters['unsigned short'] = createIntegerConversion(16); -converters['unsigned long'] = createIntegerConversion(32); - -converters.DOMString = function(V, opts = kEmptyObject) { - if (typeof V === 'string') { - return V; - } else if (typeof V === 'symbol') { - throw makeException( - 'is a Symbol and cannot be converted to a string.', - opts); - } - - return String(V); -}; - -converters.object = (V, opts) => { - if (type(V) !== 'Object') { - throw makeException( - 'is not an object.', - opts); - } - - return V; -}; +const converters = { __proto__: null, ...webidl }; /** * @param {string | object} V - The hash algorithm identifier (string or object). @@ -205,117 +64,6 @@ function ensureSHA(V, label) { `Only SHA hashes are supported in ${label}`, 'NotSupportedError'); } -converters.Uint8Array = (V, opts = kEmptyObject) => { - if (!ArrayBufferIsView(V) || - TypedArrayPrototypeGetSymbolToStringTag(V) !== 'Uint8Array') { - throw makeException( - 'is not an Uint8Array object.', - opts); - } - if (isSharedArrayBuffer(TypedArrayPrototypeGetBuffer(V))) { - throw makeException( - 'is a view on a SharedArrayBuffer, which is not allowed.', - opts); - } - - return V; -}; - -converters.BufferSource = sharedConverters.BufferSource; - -converters['sequence'] = createSequenceConverter( - converters.DOMString); - -function requiredArguments(length, required, opts = kEmptyObject) { - if (length < required) { - throw makeException( - `${required} argument${ - required === 1 ? '' : 's' - } required, but only ${length} present.`, - { __proto__: null, ...opts, context: '', code: 'ERR_MISSING_ARGS' }); - } -} - -function createDictionaryConverter(name, dictionaries) { - let hasRequiredKey = false; - const allMembers = []; - for (let i = 0; i < dictionaries.length; i++) { - const member = dictionaries[i]; - if (member.required) { - hasRequiredKey = true; - } - ArrayPrototypePush(allMembers, member); - } - ArrayPrototypeSort(allMembers, (a, b) => { - if (a.key === b.key) { - return 0; - } - return a.key < b.key ? -1 : 1; - }); - - return function(V, opts = kEmptyObject) { - const typeV = type(V); - switch (typeV) { - case 'Undefined': - case 'Null': - case 'Object': - break; - default: - throw makeException( - 'can not be converted to a dictionary', - opts); - } - const esDict = V; - const idlDict = {}; - - // Fast path null and undefined. - if (V == null && !hasRequiredKey) { - return idlDict; - } - - for (const member of new SafeArrayIterator(allMembers)) { - const key = member.key; - - let esMemberValue; - if (typeV === 'Undefined' || typeV === 'Null') { - esMemberValue = undefined; - } else { - esMemberValue = esDict[key]; - } - - if (esMemberValue !== undefined) { - const context = `'${key}' of '${name}'${ - opts.context ? ` (${opts.context})` : '' - }`; - const idlMemberValue = member.converter(esMemberValue, { - __proto__: null, - ...opts, - context, - }); - member.validator?.(idlMemberValue, esDict); - setOwnProperty(idlDict, key, idlMemberValue); - } else if (member.required) { - throw makeException( - `can not be converted to '${name}' because '${key}' is required in '${name}'.`, - { __proto__: null, ...opts, code: 'ERR_MISSING_OPTION' }); - } - } - - return idlDict; - }; -} - -function createInterfaceConverter(name, prototype) { - return (V, opts) => { - if (!ObjectPrototypeIsPrototypeOf(prototype, V)) { - throw makeException( - `is not of type ${name}.`, - opts); - } - return V; - }; -} - converters.AlgorithmIdentifier = (V, opts) => { // Union for (object or DOMString) if (type(V) === 'Object') { @@ -365,10 +113,29 @@ const dictAlgorithm = [ converters.Algorithm = createDictionaryConverter( 'Algorithm', dictAlgorithm); -converters.BigInteger = converters.Uint8Array; +// TODO(panva): Reject resizable backing stores in a semver-major with: +// converters.BigInteger = webidl.Uint8Array; +converters.BigInteger = (V, opts = kEmptyObject) => { + return webidl.Uint8Array(V, { + __proto__: null, + ...opts, + allowResizable: true, + allowShared: false, + }); +}; + +// TODO(panva): Reject resizable backing stores in a semver-major by +// removing this altogether. +converters.BufferSource = (V, opts = kEmptyObject) => { + return webidl.BufferSource(V, { + __proto__: null, + ...opts, + allowResizable: opts.allowResizable === undefined ? + true : opts.allowResizable, + }); +}; const dictRsaKeyGenParams = [ - ...new SafeArrayIterator(dictAlgorithm), { key: 'modulusLength', converter: (V, opts) => @@ -383,64 +150,78 @@ const dictRsaKeyGenParams = [ ]; converters.RsaKeyGenParams = createDictionaryConverter( - 'RsaKeyGenParams', dictRsaKeyGenParams); + 'RsaKeyGenParams', [ + dictAlgorithm, + dictRsaKeyGenParams, + ]); converters.RsaHashedKeyGenParams = createDictionaryConverter( 'RsaHashedKeyGenParams', [ - ...new SafeArrayIterator(dictRsaKeyGenParams), - { - key: 'hash', - converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'RsaHashedKeyGenParams'), - required: true, - }, + dictAlgorithm, + dictRsaKeyGenParams, + [ + { + key: 'hash', + converter: converters.HashAlgorithmIdentifier, + validator: (V, dict) => ensureSHA(V, 'RsaHashedKeyGenParams'), + required: true, + }, + ], ]); converters.RsaHashedImportParams = createDictionaryConverter( 'RsaHashedImportParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'hash', - converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'RsaHashedImportParams'), - required: true, - }, + dictAlgorithm, + [ + { + key: 'hash', + converter: converters.HashAlgorithmIdentifier, + validator: (V, dict) => ensureSHA(V, 'RsaHashedImportParams'), + required: true, + }, + ], ]); converters.NamedCurve = converters.DOMString; converters.EcKeyImportParams = createDictionaryConverter( 'EcKeyImportParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'namedCurve', - converter: converters.NamedCurve, - validator: namedCurveValidator, - required: true, - }, + dictAlgorithm, + [ + { + key: 'namedCurve', + converter: converters.NamedCurve, + validator: namedCurveValidator, + required: true, + }, + ], ]); converters.EcKeyGenParams = createDictionaryConverter( 'EcKeyGenParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'namedCurve', - converter: converters.NamedCurve, - validator: namedCurveValidator, - required: true, - }, + dictAlgorithm, + [ + { + key: 'namedCurve', + converter: converters.NamedCurve, + validator: namedCurveValidator, + required: true, + }, + ], ]); converters.AesKeyGenParams = createDictionaryConverter( 'AesKeyGenParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'length', - converter: (V, opts) => - converters['unsigned short'](V, { ...opts, enforceRange: true }), - validator: AESLengthValidator, - required: true, - }, + dictAlgorithm, + [ + { + key: 'length', + converter: (V, opts) => + converters['unsigned short'](V, { ...opts, enforceRange: true }), + validator: AESLengthValidator, + required: true, + }, + ], ]); function validateZeroLength(parameterName) { @@ -454,51 +235,59 @@ function validateZeroLength(parameterName) { converters.RsaPssParams = createDictionaryConverter( 'RsaPssParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'saltLength', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - required: true, - }, + dictAlgorithm, + [ + { + key: 'saltLength', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + required: true, + }, + ], ]); converters.RsaOaepParams = createDictionaryConverter( 'RsaOaepParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'label', - converter: converters.BufferSource, - }, + dictAlgorithm, + [ + { + key: 'label', + converter: converters.BufferSource, + }, + ], ]); converters.EcdsaParams = createDictionaryConverter( 'EcdsaParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'hash', - converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'EcdsaParams'), - required: true, - }, - ]); - -for (const { 0: name, 1: zeroError } of [['HmacKeyGenParams', 'OperationError'], ['HmacImportParams', 'DataError']]) { - converters[name] = createDictionaryConverter( - name, [ - ...new SafeArrayIterator(dictAlgorithm), + dictAlgorithm, + [ { key: 'hash', converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, name), + validator: (V, dict) => ensureSHA(V, 'EcdsaParams'), required: true, }, - { - key: 'length', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - validator: validateMacKeyLength(`${name}.length`, zeroError), - }, + ], + ]); + +for (const { 0: name, 1: zeroError } of [['HmacKeyGenParams', 'OperationError'], ['HmacImportParams', 'DataError']]) { + converters[name] = createDictionaryConverter( + name, [ + dictAlgorithm, + [ + { + key: 'hash', + converter: converters.HashAlgorithmIdentifier, + validator: (V, dict) => ensureSHA(V, name), + required: true, + }, + { + key: 'length', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: validateMacKeyLength(`${name}.length`, zeroError), + }, + ], ]); } @@ -537,195 +326,209 @@ converters.JsonWebKey = createDictionaryConverter( simpleDomStringKey('dp'), simpleDomStringKey('dq'), simpleDomStringKey('qi'), - simpleDomStringKey('pub'), - simpleDomStringKey('priv'), { key: 'oth', converter: converters['sequence'], }, simpleDomStringKey('k'), + simpleDomStringKey('pub'), + simpleDomStringKey('priv'), ]); converters.HkdfParams = createDictionaryConverter( 'HkdfParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'hash', - converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'HkdfParams'), - required: true, - }, - { - key: 'salt', - converter: converters.BufferSource, - required: true, - }, - { - key: 'info', - converter: converters.BufferSource, - required: true, - }, + dictAlgorithm, + [ + { + key: 'hash', + converter: converters.HashAlgorithmIdentifier, + validator: (V, dict) => ensureSHA(V, 'HkdfParams'), + required: true, + }, + { + key: 'salt', + converter: converters.BufferSource, + required: true, + }, + { + key: 'info', + converter: converters.BufferSource, + required: true, + }, + ], ]); converters.CShakeParams = createDictionaryConverter( 'CShakeParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'outputLength', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - validator: (V, opts) => { - // The Web Crypto spec allows for SHAKE output length that are not multiples of - // 8. We don't. - if (V % 8) - throw lazyDOMException('Unsupported CShakeParams outputLength', 'NotSupportedError'); - }, - required: true, - }, - { - key: 'functionName', - converter: converters.BufferSource, - validator: validateZeroLength('CShakeParams.functionName'), - }, - { - key: 'customization', - converter: converters.BufferSource, - validator: validateZeroLength('CShakeParams.customization'), - }, + dictAlgorithm, + [ + { + key: 'outputLength', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V, opts) => { + // The Web Crypto spec allows for SHAKE output length that are not multiples of + // 8. We don't. + if (V % 8) + throw lazyDOMException('Unsupported CShakeParams outputLength', 'NotSupportedError'); + }, + required: true, + }, + { + key: 'functionName', + converter: converters.BufferSource, + validator: validateZeroLength('CShakeParams.functionName'), + }, + { + key: 'customization', + converter: converters.BufferSource, + validator: validateZeroLength('CShakeParams.customization'), + }, + ], ]); converters.Pbkdf2Params = createDictionaryConverter( 'Pbkdf2Params', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'hash', - converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'Pbkdf2Params'), - required: true, - }, - { - key: 'iterations', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - validator: (V, dict) => { - if (V === 0) - throw lazyDOMException('iterations cannot be zero', 'OperationError'); - }, - required: true, - }, - { - key: 'salt', - converter: converters.BufferSource, - required: true, - }, + dictAlgorithm, + [ + { + key: 'salt', + converter: converters.BufferSource, + required: true, + }, + { + key: 'iterations', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V, dict) => { + if (V === 0) + throw lazyDOMException('iterations cannot be zero', 'OperationError'); + }, + required: true, + }, + { + key: 'hash', + converter: converters.HashAlgorithmIdentifier, + validator: (V, dict) => ensureSHA(V, 'Pbkdf2Params'), + required: true, + }, + ], ]); converters.AesDerivedKeyParams = createDictionaryConverter( 'AesDerivedKeyParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'length', - converter: (V, opts) => - converters['unsigned short'](V, { ...opts, enforceRange: true }), - validator: AESLengthValidator, - required: true, - }, + dictAlgorithm, + [ + { + key: 'length', + converter: (V, opts) => + converters['unsigned short'](V, { ...opts, enforceRange: true }), + validator: AESLengthValidator, + required: true, + }, + ], ]); converters.AesCbcParams = createDictionaryConverter( 'AesCbcParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'iv', - converter: converters.BufferSource, - validator: (V, dict) => validateByteLength(V, 'algorithm.iv', 16), - required: true, - }, + dictAlgorithm, + [ + { + key: 'iv', + converter: converters.BufferSource, + validator: (V, dict) => validateByteLength(V, 'algorithm.iv', 16), + required: true, + }, + ], ]); converters.AeadParams = createDictionaryConverter( 'AeadParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'iv', - converter: converters.BufferSource, - validator: (V, dict) => { - switch (StringPrototypeToLowerCase(dict.name)) { - case 'chacha20-poly1305': - validateByteLength(V, 'algorithm.iv', 12); - break; - case 'aes-gcm': - validateMaxBufferLength(V, 'algorithm.iv'); - break; - case 'aes-ocb': - if (V.byteLength > 15) { - throw lazyDOMException( - 'AES-OCB algorithm.iv must be no more than 15 bytes', - 'OperationError'); - } - break; - } - }, - required: true, - }, - { - key: 'tagLength', - converter: (V, opts) => - converters.octet(V, { ...opts, enforceRange: true }), - validator: (V, dict) => { - switch (StringPrototypeToLowerCase(dict.name)) { - case 'chacha20-poly1305': - if (V !== 128) { - throw lazyDOMException( - `${V} is not a valid ChaCha20-Poly1305 tag length`, - 'OperationError'); - } - break; - case 'aes-gcm': - if (!ArrayPrototypeIncludes([32, 64, 96, 104, 112, 120, 128], V)) { - throw lazyDOMException( - `${V} is not a valid AES-GCM tag length`, - 'OperationError'); - } - break; - case 'aes-ocb': - if (!ArrayPrototypeIncludes([64, 96, 128], V)) { - throw lazyDOMException( - `${V} is not a valid AES-OCB tag length`, - 'OperationError'); - } - break; - } + dictAlgorithm, + [ + { + key: 'iv', + converter: converters.BufferSource, + validator: (V, dict) => { + switch (StringPrototypeToLowerCase(dict.name)) { + case 'chacha20-poly1305': + validateByteLength(V, 'algorithm.iv', 12); + break; + case 'aes-gcm': + validateMaxBufferLength(V, 'algorithm.iv'); + break; + case 'aes-ocb': + if (V.byteLength > 15) { + throw lazyDOMException( + 'AES-OCB algorithm.iv must be no more than 15 bytes', + 'OperationError'); + } + break; + } + }, + required: true, }, - }, - { - key: 'additionalData', - converter: converters.BufferSource, - validator: (V, dict) => validateMaxBufferLength(V, 'algorithm.additionalData'), - }, + { + key: 'additionalData', + converter: converters.BufferSource, + validator: (V, dict) => validateMaxBufferLength(V, 'algorithm.additionalData'), + }, + { + key: 'tagLength', + converter: (V, opts) => + converters.octet(V, { ...opts, enforceRange: true }), + validator: (V, dict) => { + switch (StringPrototypeToLowerCase(dict.name)) { + case 'chacha20-poly1305': + if (V !== 128) { + throw lazyDOMException( + `${V} is not a valid ChaCha20-Poly1305 tag length`, + 'OperationError'); + } + break; + case 'aes-gcm': + if (!ArrayPrototypeIncludes([32, 64, 96, 104, 112, 120, 128], V)) { + throw lazyDOMException( + `${V} is not a valid AES-GCM tag length`, + 'OperationError'); + } + break; + case 'aes-ocb': + if (!ArrayPrototypeIncludes([64, 96, 128], V)) { + throw lazyDOMException( + `${V} is not a valid AES-OCB tag length`, + 'OperationError'); + } + break; + } + }, + }, + ], ]); converters.AesCtrParams = createDictionaryConverter( 'AesCtrParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'counter', - converter: converters.BufferSource, - validator: (V, dict) => validateByteLength(V, 'algorithm.counter', 16), - required: true, - }, - { - key: 'length', - converter: (V, opts) => - converters.octet(V, { ...opts, enforceRange: true }), - validator: (V, dict) => { - if (V === 0 || V > 128) - throw lazyDOMException( - 'AES-CTR algorithm.length must be between 1 and 128', - 'OperationError'); - }, - required: true, - }, + dictAlgorithm, + [ + { + key: 'counter', + converter: converters.BufferSource, + validator: (V, dict) => validateByteLength(V, 'algorithm.counter', 16), + required: true, + }, + { + key: 'length', + converter: (V, opts) => + converters.octet(V, { ...opts, enforceRange: true }), + validator: (V, dict) => { + if (V === 0 || V > 128) + throw lazyDOMException( + 'AES-CTR algorithm.length must be between 1 and 128', + 'OperationError'); + }, + required: true, + }, + ], ]); converters.CryptoKey = createInterfaceConverter( @@ -733,104 +536,120 @@ converters.CryptoKey = createInterfaceConverter( converters.EcdhKeyDeriveParams = createDictionaryConverter( 'EcdhKeyDeriveParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'public', - converter: converters.CryptoKey, - validator: (V, dict) => { - if (V.type !== 'public') - throw lazyDOMException( - 'algorithm.public must be a public key', 'InvalidAccessError'); - - if (StringPrototypeToLowerCase(V.algorithm.name) !== StringPrototypeToLowerCase(dict.name)) - throw lazyDOMException( - 'key algorithm mismatch', - 'InvalidAccessError'); - }, - required: true, - }, + dictAlgorithm, + [ + { + key: 'public', + converter: converters.CryptoKey, + validator: (V, dict) => { + if (V.type !== 'public') + throw lazyDOMException( + 'algorithm.public must be a public key', 'InvalidAccessError'); + + if (StringPrototypeToLowerCase(V.algorithm.name) !== StringPrototypeToLowerCase(dict.name)) + throw lazyDOMException( + 'key algorithm mismatch', + 'InvalidAccessError'); + }, + required: true, + }, + ], ]); converters.ContextParams = createDictionaryConverter( 'ContextParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'context', - converter: converters.BufferSource, - validator(V, dict) { - let { 0: major, 1: minor } = process.versions.openssl.split('.'); - major = NumberParseInt(major, 10); - minor = NumberParseInt(minor, 10); - if (major > 3 || (major === 3 && minor >= 2)) { - this.validator = undefined; - } else { - this.validator = validateZeroLength('ContextParams.context'); - this.validator(V, dict); - } + dictAlgorithm, + [ + { + key: 'context', + converter: converters.BufferSource, + validator(V, dict) { + let { 0: major, 1: minor } = process.versions.openssl.split('.'); + major = NumberParseInt(major, 10); + minor = NumberParseInt(minor, 10); + if (major > 3 || (major === 3 && minor >= 2)) { + this.validator = undefined; + } else { + this.validator = validateZeroLength('ContextParams.context'); + this.validator(V, dict); + } + }, }, - }, + ], ]); converters.Argon2Params = createDictionaryConverter( 'Argon2Params', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'nonce', - converter: converters.BufferSource, - required: true, - }, - { - key: 'parallelism', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - validator: (V, dict) => { - if (V === 0 || V > MathPow(2, 24) - 1) { - throw lazyDOMException( - 'parallelism must be > 0 and < 16777215', - 'OperationError'); - } - }, - required: true, - }, - { - key: 'memory', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - validator: (V, dict) => { - if (V < 8 * dict.parallelism) { - throw lazyDOMException( - 'memory must be at least 8 times the degree of parallelism', - 'OperationError'); - } - }, - required: true, - }, - { - key: 'passes', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - required: true, - }, - { - key: 'version', - converter: (V, opts) => - converters.octet(V, { ...opts, enforceRange: true }), - validator: (V, dict) => { - if (V !== 0x13) { - throw lazyDOMException( - `${V} is not a valid Argon2 version`, - 'OperationError'); - } + dictAlgorithm, + [ + { + key: 'nonce', + converter: converters.BufferSource, + validator: (V) => { + if (V.byteLength < 8) { + throw lazyDOMException('nonce must be at least 8 bytes', 'OperationError'); + } + }, + required: true, }, - }, - { - key: 'secretValue', - converter: converters.BufferSource, - }, - { - key: 'associatedData', - converter: converters.BufferSource, - }, + { + key: 'parallelism', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V, dict) => { + if (V === 0 || V > MathPow(2, 24) - 1) { + throw lazyDOMException( + 'parallelism must be > 0 and <= 16777215', + 'OperationError'); + } + }, + required: true, + }, + { + key: 'memory', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V, dict) => { + if (V < 8 * dict.parallelism) { + throw lazyDOMException( + 'memory must be at least 8 times the degree of parallelism', + 'OperationError'); + } + }, + required: true, + }, + { + key: 'passes', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V) => { + if (V === 0) { + throw lazyDOMException('passes must be > 0', 'OperationError'); + } + }, + required: true, + }, + { + key: 'version', + converter: (V, opts) => + converters.octet(V, { ...opts, enforceRange: true }), + validator: (V, dict) => { + if (V !== 0x13) { + throw lazyDOMException( + `${V} is not a valid Argon2 version`, + 'OperationError'); + } + }, + }, + { + key: 'secretValue', + converter: converters.BufferSource, + }, + { + key: 'associatedData', + converter: converters.BufferSource, + }, + ], ]); function validateMacKeyLength(parameterName, zeroError) { @@ -847,80 +666,88 @@ function validateMacKeyLength(parameterName, zeroError) { for (const { 0: name, 1: zeroError } of [['KmacKeyGenParams', 'OperationError'], ['KmacImportParams', 'DataError']]) { converters[name] = createDictionaryConverter( name, [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'length', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - validator: validateMacKeyLength(`${name}.length`, zeroError), - }, + dictAlgorithm, + [ + { + key: 'length', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: validateMacKeyLength(`${name}.length`, zeroError), + }, + ], ]); } converters.KmacParams = createDictionaryConverter( 'KmacParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'outputLength', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - validator: (V, opts) => { - // The Web Crypto spec allows for KMAC output length that are not multiples of 8. We don't. - if (V % 8) - throw lazyDOMException('Unsupported KmacParams outputLength', 'NotSupportedError'); - }, - required: true, - }, - { - key: 'customization', - converter: converters.BufferSource, - }, + dictAlgorithm, + [ + { + key: 'outputLength', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V, opts) => { + // The Web Crypto spec allows for KMAC output length that are not multiples of 8. We don't. + if (V % 8) + throw lazyDOMException('Unsupported KmacParams outputLength', 'NotSupportedError'); + }, + required: true, + }, + { + key: 'customization', + converter: converters.BufferSource, + }, + ], ]); converters.KangarooTwelveParams = createDictionaryConverter( 'KangarooTwelveParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'outputLength', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - validator: (V, opts) => { - if (V === 0 || V % 8) - throw lazyDOMException('Invalid KangarooTwelveParams outputLength', 'OperationError'); - }, - required: true, - }, - { - key: 'customization', - converter: converters.BufferSource, - }, + dictAlgorithm, + [ + { + key: 'outputLength', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V, opts) => { + if (V === 0 || V % 8) + throw lazyDOMException('Invalid KangarooTwelveParams outputLength', 'OperationError'); + }, + required: true, + }, + { + key: 'customization', + converter: converters.BufferSource, + }, + ], ]); converters.TurboShakeParams = createDictionaryConverter( 'TurboShakeParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'outputLength', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - validator: (V, opts) => { - if (V === 0 || V % 8) - throw lazyDOMException('Invalid TurboShakeParams outputLength', 'OperationError'); - }, - required: true, - }, - { - key: 'domainSeparation', - converter: (V, opts) => - converters.octet(V, { ...opts, enforceRange: true }), - validator: (V) => { - if (V < 0x01 || V > 0x7F) { - throw lazyDOMException( - 'TurboShakeParams.domainSeparation must be in range 0x01-0x7f', - 'OperationError'); - } + dictAlgorithm, + [ + { + key: 'outputLength', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V, opts) => { + if (V === 0 || V % 8) + throw lazyDOMException('Invalid TurboShakeParams outputLength', 'OperationError'); + }, + required: true, }, - }, + { + key: 'domainSeparation', + converter: (V, opts) => + converters.octet(V, { ...opts, enforceRange: true }), + validator: (V) => { + if (V < 0x01 || V > 0x7F) { + throw lazyDOMException( + 'TurboShakeParams.domainSeparation must be in range 0x01-0x7f', + 'OperationError'); + } + }, + }, + ], ]); module.exports = { diff --git a/lib/internal/locks.js b/lib/internal/locks.js index 05000e933f0b55..817159c06e281a 100644 --- a/lib/internal/locks.js +++ b/lib/internal/locks.js @@ -43,7 +43,7 @@ const kConstructLock = Symbol('kConstructLock'); const kConstructLockManager = Symbol('kConstructLockManager'); // WebIDL dictionary LockOptions -const convertLockOptions = createDictionaryConverter([ +const convertLockOptions = createDictionaryConverter('LockOptions', [ { key: 'mode', converter: createEnumConverter('LockMode', [ diff --git a/lib/internal/perf/performance.js b/lib/internal/perf/performance.js index ef2b5a55dd7c44..a12be66333d4f9 100644 --- a/lib/internal/perf/performance.js +++ b/lib/internal/perf/performance.js @@ -44,7 +44,7 @@ const timerify = require('internal/perf/timerify'); const { customInspectSymbol: kInspect, kEnumerableProperty, kEmptyObject } = require('internal/util'); const { inspect } = require('util'); const { validateThisInternalField } = require('internal/validators'); -const { convertToInt } = require('internal/webidl'); +const { converters } = require('internal/webidl'); const kPerformanceBrand = Symbol('performance'); @@ -144,8 +144,10 @@ class Performance extends EventTarget { if (arguments.length === 0) { throw new ERR_MISSING_ARGS('maxSize'); } - // unsigned long - maxSize = convertToInt('maxSize', maxSize, 32); + maxSize = converters['unsigned long']( + maxSize, + { __proto__: null, context: 'maxSize' }, + ); return setResourceTimingBufferSize(maxSize); } diff --git a/lib/internal/webidl.js b/lib/internal/webidl.js index 36bde94013d1a8..9d177fb6bafc52 100644 --- a/lib/internal/webidl.js +++ b/lib/internal/webidl.js @@ -2,98 +2,185 @@ const { ArrayBufferIsView, - ArrayBufferPrototypeGetByteLength, + ArrayBufferPrototypeGetResizable, + ArrayIsArray, ArrayPrototypePush, ArrayPrototypeToSorted, + BigInt, DataViewPrototypeGetBuffer, + FunctionPrototypeCall, MathAbs, MathMax, MathMin, MathPow, MathSign, MathTrunc, + Number, + NumberIsFinite, NumberIsNaN, NumberMAX_SAFE_INTEGER, NumberMIN_SAFE_INTEGER, - ObjectAssign, ObjectPrototypeIsPrototypeOf, SafeSet, String, - Symbol, SymbolIterator, TypeError, TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetSymbolToStringTag, } = primordials; +const { kEmptyObject, setOwnProperty } = require('internal/util'); const { - codes: { - ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE, - }, -} = require('internal/errors'); -const { kEmptyObject } = require('internal/util'); -const { - isArrayBuffer, + isSharedArrayBuffer, isTypedArray, } = require('internal/util/types'); +const BIGINT_2_63 = 1n << 63n; +const BIGINT_2_64 = 1n << 64n; + const converters = { __proto__: null }; -const UNDEFINED = Symbol('undefined'); -const BOOLEAN = Symbol('boolean'); -const STRING = Symbol('string'); -const SYMBOL = Symbol('symbol'); -const NUMBER = Symbol('number'); -const BIGINT = Symbol('bigint'); -const NULL = Symbol('null'); -const OBJECT = Symbol('object'); +/** + * @typedef {object} ConversionOptions + * @property {string} [prefix] Message prefix for operation failures. + * @property {string} [context] Message context for the converted value. + * @property {string} [code] Node.js error code to assign to TypeError. + * @property {boolean} [enforceRange] Web IDL [EnforceRange] attribute. + * @property {boolean} [clamp] Web IDL [Clamp] attribute. + * @property {boolean} [allowShared] Web IDL [AllowShared] attribute for + * buffer view types. + * @property {boolean} [allowResizable] Web IDL [AllowResizable] attribute. + */ /** - * @see https://webidl.spec.whatwg.org/#es-any - * @param {any} V + * @callback Converter + * @param {any} V JavaScript value to convert to an IDL value. + * @param {ConversionOptions} [options] Conversion options. * @returns {any} */ -converters.any = (V) => { - return V; -}; -converters.object = (V, opts = kEmptyObject) => { - if (type(V) !== OBJECT) { - throw makeException( - 'is not an object', - kEmptyObject, - ); +/** + * @callback DictionaryMemberValidator + * @param {any} idlMemberValue Converted IDL member value. + * @param {object} jsDict Original JavaScript dictionary object. + * @returns {void} + */ + +/** + * @typedef {object} DictionaryMember + * @property {string} key Dictionary member identifier. + * @property {Converter} converter Converter for the member type. + * @property {boolean} [required] Whether the member is required. + * @property {() => any} [defaultValue] Function returning the default value. + * @property {DictionaryMemberValidator} [validator] Optional Node.js + * extension point invoked after conversion and before storing the member. + * This is for early semantic validation of known unsupported IDL values, + * especially in Web Crypto, where SubtleCrypto.supports() needs to answer + * from normalized dictionaries without running the requested operation. + */ + +/** + * Creates a TypeError with a Node.js error code. + * @param {string} message Error message. + * @param {string} code Node.js error code to assign. + * @returns {TypeError} + */ +function codedTypeError(message, code) { + // eslint-disable-next-line no-restricted-syntax + const err = new TypeError(message); + setOwnProperty(err, 'code', code); + return err; +} + +/** + * Creates the exception thrown by Web IDL converters. + * @param {string} message Unprefixed conversion failure message. + * @param {ConversionOptions} [options] Conversion options. + * @returns {TypeError} + */ +function makeException(message, options = kEmptyObject) { + const prefix = options.prefix ? options.prefix + ': ' : ''; + const context = options.context?.length === 0 ? + '' : (options.context ?? 'Value') + ' '; + return codedTypeError( + `${prefix}${context}${message}`, + options.code || 'ERR_INVALID_ARG_TYPE', + ); +} + +/** + * Returns the ECMAScript specification type of a JavaScript value. + * @see https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values + * @param {any} V JavaScript value. + * @returns {'Undefined'|'Null'|'Boolean'|'String'|'Symbol'|'Number'|'BigInt'|'Object'} + */ +function type(V) { + // ECMA-262 6.1: map JavaScript values to language type names. + switch (typeof V) { + case 'undefined': + return 'Undefined'; + case 'boolean': + return 'Boolean'; + case 'string': + return 'String'; + case 'symbol': + return 'Symbol'; + case 'number': + return 'Number'; + case 'bigint': + return 'BigInt'; + case 'object': + case 'function': + default: + // ECMA-262 6.1.2: null is its own language type. + // ECMA-262 6.1.7: functions are Object values. + if (V === null) { + return 'Null'; + } + return 'Object'; } - return V; -}; +} -// https://webidl.spec.whatwg.org/#abstract-opdef-integerpart -const integerPart = MathTrunc; +/** + * Returns IntegerPart(n). + * @see https://webidl.spec.whatwg.org/#abstract-opdef-integerpart + * @param {number} n Numeric value. + * @returns {number} + */ +function integerPart(n) { + // Web IDL IntegerPart steps 1-3: floor(abs(n)), restore the sign, + // and choose +0 rather than -0. + const integer = MathTrunc(n); + return integer === 0 ? 0 : integer; +} -/* eslint-disable node-core/non-ascii-character */ -// Round x to the nearest integer, choosing the even integer if it lies halfway -// between two, and choosing +0 rather than -0. -// This is different from Math.round, which rounds to the next integer in the -// direction of +∞ when the fraction portion is exactly 0.5. -/* eslint-enable node-core/non-ascii-character */ +/** + * Rounds to the nearest integer, choosing the even integer on ties. + * @param {number} x Numeric value. + * @returns {number} + */ function evenRound(x) { - // Convert -0 to +0. - const i = integerPart(x) + 0; - const reminder = MathAbs(x % 1); - const sign = MathSign(i); - if (reminder === 0.5) { + // Web IDL ConvertToInt step 7.2: round to the nearest integer, + // choosing the even integer on ties and +0 rather than -0. + const i = integerPart(x); + const remainder = MathAbs(x % 1); + const sign = MathSign(x); + if (remainder === 0.5) { return i % 2 === 0 ? i : i + sign; } - const r = reminder < 0.5 ? i : i + sign; - // Convert -0 to +0. + const r = remainder < 0.5 ? i : i + sign; if (r === 0) { return 0; } return r; } +/** + * Returns 2 to the power of the given exponent. + * @param {number} exponent Non-negative integer exponent. + * @returns {number} + */ function pow2(exponent) { - // << operates on 32 bit signed integers. if (exponent < 31) { return 1 << exponent; } @@ -106,354 +193,877 @@ function pow2(exponent) { return MathPow(2, exponent); } -// https://tc39.es/ecma262/#eqn-modulo -// The notation “x modulo y” computes a value k of the same sign as y. +/** + * Returns x modulo y for Web IDL ConvertToInt step 10. + * + * This is intentionally not a general modulo helper. ConvertToInt only calls + * it with a positive power-of-two modulus, and the implementation assumes + * that. It converts JavaScript remainder into mathematical modulo and + * normalizes -0 to +0. + * @param {number} x Dividend. + * @param {number} y Positive divisor. + * @returns {number} + */ function modulo(x, y) { + // Web IDL ConvertToInt step 10 uses mathematical modulo. const r = x % y; - // Convert -0 to +0. if (r === 0) { return 0; } - return r; + return r > 0 ? r : r + y; +} + +/** + * Returns x modulo y for Web IDL ConvertToInt step 10. + * + * This is intentionally not a general modulo helper. ConvertToInt only calls + * it with a positive power-of-two modulus, and the implementation assumes + * that. BigInt has no -0, but this mirrors modulo()'s mathematical modulo + * behavior for the 64-bit path. + * @param {bigint} x Dividend. + * @param {bigint} y Positive divisor. + * @returns {bigint} + */ +function bigIntModulo(x, y) { + // Web IDL ConvertToInt step 10 uses mathematical modulo. + const r = x % y; + return r >= 0n ? r : r + y; } -// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint -function convertToInt(name, value, bitLength, options = kEmptyObject) { - const { signed = false, enforceRange = false, clamp = false } = options; +/** + * Returns ToNumber(V). + * @see https://tc39.es/ecma262/#sec-tonumber + * @param {any} V JavaScript value. + * @param {ConversionOptions} [options] Conversion options. + * @returns {number} + */ +function toNumber(V, options = kEmptyObject) { + if (typeof V === 'bigint') { + // ECMA-262 ToNumber step 2: BigInt values throw. + throw makeException( + 'is a BigInt and cannot be converted to a number.', + options); + } + if (typeof V === 'symbol') { + // ECMA-262 ToNumber step 2: Symbol values throw. + throw makeException( + 'is a Symbol and cannot be converted to a number.', + options); + } + // Unary plus performs ToNumber, including ToPrimitive(V, number) for + // objects. Number(V) is not equivalent because it converts BigInt values, + // including BigInt values produced by ToPrimitive. + // Abrupt completions and native TypeErrors propagate unchanged. This is an + // intentional diagnostics tradeoff: decorating object conversion failures + // would require maintaining local ECMA-262 ToPrimitive and + // OrdinaryToPrimitive implementations. + return +V; +} + +/** + * Returns ToString(V). + * @see https://tc39.es/ecma262/#sec-tostring + * @param {any} V JavaScript value. + * @param {ConversionOptions} [options] Conversion options. + * @returns {string} + */ +function toString(V, options = kEmptyObject) { + if (typeof V === 'symbol') { + // ECMA-262 ToString step 2: Symbol values throw. + throw makeException( + 'is a Symbol and cannot be converted to a string.', + options); + } + + // The String function performs ToString for all non-Symbol primitives and + // objects, including ToPrimitive(V, string). String concatenation is not + // equivalent because it uses ToPrimitive(V, default). Abrupt completions + // and native TypeErrors propagate unchanged. This is an intentional + // diagnostics tradeoff: decorating object conversion failures would require + // maintaining local ECMA-262 ToPrimitive and OrdinaryToPrimitive + // implementations. + return String(V); +} + +/** + * Converts a JavaScript value to a Web IDL integer value. + * @see https://webidl.spec.whatwg.org/#abstract-opdef-converttoint + * @param {any} V JavaScript value. + * @param {number} bitLength Integer bit length. + * @param {'signed'|'unsigned'} [signedness] Integer signedness. + * @param {ConversionOptions} [options] Conversion options. + * @returns {number} + */ +function convertToInt( + V, + bitLength, + signedness = 'unsigned', + options = kEmptyObject, +) { + const signed = signedness === 'signed'; let upperBound; let lowerBound; - // 1. If bitLength is 64, then: + + // Web IDL ConvertToInt steps 1-3: determine lower/upper bounds. if (bitLength === 64) { - // 1.1. Let upperBound be 2^53 − 1. + // Steps 1.1-1.3 set upperBound to 2^53 - 1 and lowerBound to 0 + // for unsigned, or -2^53 + 1 for signed. This ensures 64-bit + // integer types associated with [EnforceRange] or [Clamp] are + // representable in JavaScript's Number type as unambiguous integers. upperBound = NumberMAX_SAFE_INTEGER; - // 1.2. If signedness is "unsigned", then let lowerBound be 0. - // 1.3. Otherwise let lowerBound be −2^53 + 1. - lowerBound = !signed ? 0 : NumberMIN_SAFE_INTEGER; + lowerBound = signed ? NumberMIN_SAFE_INTEGER : 0; } else if (!signed) { - // 2. Otherwise, if signedness is "unsigned", then: - // 2.1. Let lowerBound be 0. - // 2.2. Let upperBound be 2^bitLength − 1. + // Spell out the common Web IDL integer sizes so hot converters avoid + // recomputing powers of two on every call. lowerBound = 0; - upperBound = pow2(bitLength) - 1; + if (bitLength === 8) { + upperBound = 0xff; + } else if (bitLength === 16) { + upperBound = 0xffff; + } else if (bitLength === 32) { + upperBound = 0xffff_ffff; + } else { + upperBound = pow2(bitLength) - 1; + } + } else if (bitLength === 8) { + // Signed 8/16/32-bit conversions are mostly exercised through direct + // convertToInt() calls, but keep their common bounds cheap too. + lowerBound = -0x80; + upperBound = 0x7f; + } else if (bitLength === 16) { + lowerBound = -0x8000; + upperBound = 0x7fff; + } else if (bitLength === 32) { + lowerBound = -0x8000_0000; + upperBound = 0x7fff_ffff; } else { - // 3. Otherwise: - // 3.1. Let lowerBound be -2^(bitLength − 1). - // 3.2. Let upperBound be 2^(bitLength − 1) − 1. lowerBound = -pow2(bitLength - 1); upperBound = pow2(bitLength - 1) - 1; } - // 4. Let x be ? ToNumber(V). - let x = +value; - // 5. If x is −0, then set x to +0. + // Common case: primitive Number values that already fit the Web IDL + // range and have no fractional part are returned unchanged by every + // ConvertToInt path, except that -0 must become +0. This skips the + // generic ToNumber and option handling without skipping observable + // object coercion. + let x; + if (typeof V === 'number') { + // For primitive Numbers, in-range non-[Clamp] conversion is either + // identity or IntegerPart(V). This keeps the default and [EnforceRange] + // paths out of the generic ToNumber/options flow. + if (V >= lowerBound && V <= upperBound) { + const integer = MathTrunc(V); + if (integer === V) { + return V === 0 ? 0 : V; + } + if (options === kEmptyObject || options.enforceRange || !options.clamp) { + return integer === 0 ? 0 : integer; + } + return evenRound(V); + } + if (options !== kEmptyObject && options.enforceRange) { + // Keep [EnforceRange] ahead of [Clamp] without falling through to + // the shared check, which would observe options.enforceRange again. + if (!NumberIsFinite(V)) { + throw makeException( + 'is not a finite number.', + options); + } + + const integer = integerPart(V); + if (integer < lowerBound || integer > upperBound) { + throw makeException( + `is outside the expected range of ${lowerBound} to ${upperBound}.`, + { __proto__: null, ...options, code: 'ERR_OUT_OF_RANGE' }); + } + + return integer; + } + if (options !== kEmptyObject && options.clamp && !NumberIsNaN(V)) { + // Out-of-range [Clamp] returns one of the already-computed bounds. + if (V <= lowerBound) { + return lowerBound === 0 ? 0 : lowerBound; + } + if (V >= upperBound) { + return upperBound === 0 ? 0 : upperBound; + } + } + x = V; + } else { + // Step 4: convert V with ECMA-262 ToNumber. + x = toNumber(V, options); + } + // Step 5: normalize -0 to +0. if (x === 0) { x = 0; } - // 6. If the conversion is to an IDL type associated with the [EnforceRange] - // extended attribute, then: - if (enforceRange) { - // 6.1. If x is NaN, +∞, or −∞, then throw a TypeError. - if (NumberIsNaN(x) || x === Infinity || x === -Infinity) { - throw new ERR_INVALID_ARG_VALUE(name, x); + // Step 6: [EnforceRange] rejects non-finite and out-of-range values. + if (options.enforceRange) { + // Step 6.1: reject NaN and infinities. + if (!NumberIsFinite(x)) { + throw makeException( + 'is not a finite number.', + options); } - // 6.2. Set x to IntegerPart(x). + + // Step 6.2: truncate to IntegerPart(x). x = integerPart(x); - // 6.3. If x < lowerBound or x > upperBound, then throw a TypeError. + // Steps 6.3-6.4: reject out-of-range values, otherwise return. if (x < lowerBound || x > upperBound) { - throw new ERR_INVALID_ARG_VALUE(name, x); + throw makeException( + `is outside the expected range of ${lowerBound} to ${upperBound}.`, + { __proto__: null, ...options, code: 'ERR_OUT_OF_RANGE' }); } - // 6.4. Return x. return x; } - // 7. If x is not NaN and the conversion is to an IDL type associated with - // the [Clamp] extended attribute, then: - if (clamp && !NumberIsNaN(x)) { - // 7.1. Set x to min(max(x, lowerBound), upperBound). + // Step 7: [Clamp] clamps, rounds, and returns non-NaN values. + if (options.clamp && !NumberIsNaN(x)) { + // Step 7.1: clamp x into the supported bounds. x = MathMin(MathMax(x, lowerBound), upperBound); + // Steps 7.2-7.3: round ties to even and return. + return evenRound(x); + } - // 7.2. Round x to the nearest integer, choosing the even integer if it - // lies halfway between two, and choosing +0 rather than −0. - x = evenRound(x); + // Step 8: NaN, +0, -0, and infinities become +0. + if (!NumberIsFinite(x) || x === 0) { + return 0; + } + + // Step 9: truncate to IntegerPart(x). + x = integerPart(x); - // 7.3. Return x. + // Steps 10-12 are an identity for values already in the step 1-3 + // bounds. For 64-bit conversions this only skips the safe-integer + // subset; values outside it still need exact BigInt modulo and the + // final Number approximation. + if (x >= lowerBound && x <= upperBound) { return x; } - // 8. If x is NaN, +0, +∞, or −∞, then return +0. - if (NumberIsNaN(x) || x === 0 || x === Infinity || x === -Infinity) { - return 0; + if (bitLength === 64) { + // Steps 10-12 still wrap over the full 2^64 IDL integer range. + // BigInt keeps x modulo 2^64 and the signed high-bit adjustment exact + // before this helper returns the JavaScript binding result. + let xBigInt = BigInt(x); + xBigInt = bigIntModulo(xBigInt, BIGINT_2_64); + + // For long long and unsigned long long values outside the safe-integer + // range, Web IDL says the JS Number value represents the closest numeric + // value, choosing the value with an even significand if there are two + // equally close values. Number(BigInt) performs that final approximation. + + // Step 11: wrap into the signed range when the high bit is set. + if (signed && xBigInt >= BIGINT_2_63) { + return Number(xBigInt - BIGINT_2_64); + } + + // Step 12: return the unsigned value. + return Number(xBigInt); } - // 9. Set x to IntegerPart(x). - x = integerPart(x); + // For 8/16/32-bit conversions, bitwise operators perform the same + // power-of-two wrapping as Web IDL step 10 for finite integer Numbers. + // The shifts narrow the unsigned value into the signed range when needed. + if (bitLength === 8) { + return signed ? (x << 24) >> 24 : x & 0xff; + } + if (bitLength === 16) { + return signed ? (x << 16) >> 16 : x & 0xffff; + } + if (bitLength === 32) { + return signed ? x | 0 : x >>> 0; + } - // 10. Set x to x modulo 2^bitLength. - x = modulo(x, pow2(bitLength)); + // Step 10: reduce modulo 2^bitLength. + const twoToTheBitLength = pow2(bitLength); + x = modulo(x, twoToTheBitLength); - // 11. If signedness is "signed" and x ≥ 2^(bitLength − 1), then return x − - // 2^bitLength. + // Step 11: wrap into the signed range when the high bit is set. if (signed && x >= pow2(bitLength - 1)) { - return x - pow2(bitLength); + return x - twoToTheBitLength; } - // 12. Otherwise, return x. + // Step 12: return the unsigned value. return x; } /** - * @see https://webidl.spec.whatwg.org/#es-DOMString - * @param {any} V - * @returns {string} + * Creates a converter for a Web IDL integer type. + * @param {number} bitLength Integer bit length. + * @param {'signed'|'unsigned'} [signedness] Integer signedness. + * @returns {Converter} */ -converters.DOMString = function DOMString(V) { - if (typeof V === 'symbol') { - throw new ERR_INVALID_ARG_VALUE('value', V); - } +function createIntegerConverter(bitLength, signedness = 'unsigned') { + return (V, options = kEmptyObject) => { + // Integer conversion step 1 calls ConvertToInt; step 2 returns + // the IDL value with the same numeric value. + return convertToInt(V, bitLength, signedness, options); + }; +} - return String(V); +/** + * Converts a JavaScript value to the IDL boolean type. + * @see https://webidl.spec.whatwg.org/#es-boolean + * @param {any} V JavaScript value. + * @returns {boolean} + */ +converters.boolean = (V) => { + // Web IDL boolean steps 1-2: ToBoolean(V), then return the same + // truth value as an IDL boolean. + return !!V; }; -converters['sequence'] = createSequenceConverter(converters.object); +/** + * Converts a JavaScript value to the IDL object type. + * @see https://webidl.spec.whatwg.org/#es-object + * @param {any} V JavaScript value. + * @param {ConversionOptions} [options] Conversion options. + * @returns {object|Function} + */ +converters.object = (V, options = kEmptyObject) => { + // Web IDL object step 1: throw unless V is an ECMA-262 Object. + if (type(V) !== 'Object') { + throw makeException( + 'is not an object.', + options, + ); + } + // Step 2: return a reference to the same object. + return V; +}; -function codedTypeError(message, errorProperties = kEmptyObject) { - // eslint-disable-next-line no-restricted-syntax - const err = new TypeError(message); - ObjectAssign(err, errorProperties); - return err; -} +/** + * Converts a JavaScript value to the IDL octet type. + * @see https://webidl.spec.whatwg.org/#es-octet + * @type {Converter} + */ +converters.octet = createIntegerConverter(8); -function makeException(message, opts = kEmptyObject) { - const prefix = opts.prefix ? opts.prefix + ': ' : ''; - const context = opts.context?.length === 0 ? - '' : (opts.context ?? 'Value') + ' '; - return codedTypeError( - `${prefix}${context}${message}`, - { code: opts.code || 'ERR_INVALID_ARG_TYPE' }, - ); +/** + * Converts a JavaScript value to the IDL unsigned short type. + * @see https://webidl.spec.whatwg.org/#es-unsigned-short + * @type {Converter} + */ +converters['unsigned short'] = createIntegerConverter(16); + +/** + * Converts a JavaScript value to the IDL unsigned long type. + * @see https://webidl.spec.whatwg.org/#es-unsigned-long + * @type {Converter} + */ +converters['unsigned long'] = createIntegerConverter(32); + +/** + * Converts a JavaScript value to the IDL long long type. + * @see https://webidl.spec.whatwg.org/#es-long-long + * @type {Converter} + */ +converters['long long'] = createIntegerConverter(64, 'signed'); + +/** + * Converts a JavaScript value to the IDL DOMString type. + * @see https://webidl.spec.whatwg.org/#es-DOMString + * @param {any} V JavaScript value. + * @param {ConversionOptions} [options] Conversion options. + * @returns {string} + */ +converters.DOMString = function DOMString(V, options = kEmptyObject) { + // Step 1 only applies to [LegacyNullToEmptyString], which this core + // converter does not implement. Steps 2-3 apply ToString(V) and + // return a DOMString with the same code units. + return toString(V, options); +}; + +/** + * Throws when a Web IDL operation receives too few arguments. + * @param {number} length Actual argument count. + * @param {number} required Required argument count. + * @param {ConversionOptions} [options] Conversion options. + * @returns {void} + */ +function requiredArguments(length, required, options = kEmptyObject) { + if (length < required) { + throw makeException( + `${required} argument${ + required === 1 ? '' : 's' + } required, but only ${length} present.`, + { __proto__: null, ...options, context: '', code: 'ERR_MISSING_ARGS' }); + } } +/** + * Creates a converter for a Web IDL enum type. + * @see https://webidl.spec.whatwg.org/#es-enumeration + * @param {string} name Enum identifier. + * @param {string[]} values Enum values. + * @returns {Converter} + */ function createEnumConverter(name, values) { const E = new SafeSet(values); - return function(V, opts = kEmptyObject) { - const S = String(V); + return function(V, options = kEmptyObject) { + // Web IDL enumeration step 1: convert V with ToString. + const S = toString(V, options); + // Step 2: throw unless S is one of the enumeration values. if (!E.has(S)) { throw makeException( - `value '${S}' is not a valid enum value of type ${name}.`, - { __proto__: null, ...opts, code: 'ERR_INVALID_ARG_VALUE' }); + `'${S}' is not a valid enum value of type ${name}.`, + { __proto__: null, ...options, code: 'ERR_INVALID_ARG_VALUE' }); } + // Step 3: return the matching enumeration value. return S; }; } -// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values -function type(V) { - switch (typeof V) { - case 'undefined': - return UNDEFINED; - case 'boolean': - return BOOLEAN; - case 'number': - return NUMBER; - case 'string': - return STRING; - case 'symbol': - return SYMBOL; - case 'bigint': - return BIGINT; - case 'object': // Fall through - case 'function': // Fall through - default: - if (V === null) { - return NULL; - } - // Per ES spec, typeof returns an implementation-defined value that is not - // any of the existing ones for uncallable non-standard exotic objects. - // Yet Type() which the Web IDL spec depends on returns Object for such - // cases. So treat the default case as an object. - return OBJECT; - } +/** + * Returns the context used when converting a dictionary member. + * @param {string} key Dictionary member identifier. + * @param {ConversionOptions} options Conversion options. + * @returns {string} + */ +function dictionaryMemberContext(key, options) { + return options.context ? `${key} in ${options.context}` : key; } -// https://webidl.spec.whatwg.org/#js-dictionary -function createDictionaryConverter(members) { - // The spec requires us to operate the members of a dictionary in - // lexicographical order. We are doing this in the outer scope to - // reduce the overhead that could happen in the returned function. - const sortedMembers = ArrayPrototypeToSorted(members, (a, b) => { +/** + * Returns the context used when converting a sequence element. + * @param {number} index Sequence element index. + * @param {ConversionOptions} options Conversion options. + * @returns {string} + */ +function sequenceElementContext(index, options) { + return `${options.context ?? 'Value'}[${index}]`; +} + +/** + * Returns the message used for a missing required dictionary member. + * @param {string} dictionaryName Dictionary identifier. + * @param {string} key Dictionary member identifier. + * @returns {string} + */ +function missingDictionaryMemberMessage(dictionaryName, key) { + return `cannot be converted to '${dictionaryName}' because ` + + `'${key}' is required in '${dictionaryName}'.`; +} + +/** + * Creates a converter for a Web IDL dictionary type. + * @see https://webidl.spec.whatwg.org/#js-dictionary + * @param {string} dictionaryName Dictionary identifier. + * @param {DictionaryMember[]|DictionaryMember[][]} members Dictionary members, + * either for a single dictionary or grouped from least-derived to + * most-derived dictionary. + * @returns {Converter} + */ +function createDictionaryConverter( + dictionaryName, + members, +) { + const compareMembers = (a, b) => { if (a.key === b.key) { return 0; } return a.key < b.key ? -1 : 1; - }); + }; + + const dictionaries = ArrayIsArray(members[0]) ? members : [members]; + const sortedDictionaries = []; + + // Web IDL dictionary conversion steps 3-4 process inherited dictionaries + // from least-derived to most-derived and sort only within each dictionary. + // Callers with inheritance pass one member array per dictionary level. + for (let i = 0; i < dictionaries.length; i++) { + ArrayPrototypePush( + sortedDictionaries, + ArrayPrototypeToSorted(dictionaries[i], compareMembers), + ); + } - return function( - V, - opts = kEmptyObject, - ) { - if (V != null && type(V) !== OBJECT) { + return function(jsDict, options = kEmptyObject) { + // Step 1: reject non-object, non-null, non-undefined values. + if (jsDict != null && type(jsDict) !== 'Object') { throw makeException( 'cannot be converted to a dictionary', - opts, + options, ); } + // Step 2: create the IDL dictionary value. const idlDict = { __proto__: null }; - for (let i = 0; i < sortedMembers.length; i++) { - const member = sortedMembers[i]; - const key = member.key; - let jsMemberValue; - if (V == null) { - jsMemberValue = undefined; - } else { - jsMemberValue = V[key]; - } - if (jsMemberValue !== undefined) { - const memberContext = opts.context ? `${key} in ${opts.context}` : `${key}`; - const converter = member.converter; - const idlMemberValue = converter( - jsMemberValue, - { - __proto__: null, - prefix: opts.prefix, - context: memberContext, - }, - ); - idlDict[key] = idlMemberValue; - } else if (typeof member.defaultValue === 'function') { - const idlMemberValue = member.defaultValue(); - idlDict[key] = idlMemberValue; - } else if (member.required) { - throw makeException( - `cannot be converted because of the missing '${key}'`, - opts, - ); + // Steps 3-4: iterate each dictionary level, then its sorted members. + for (let i = 0; i < sortedDictionaries.length; i++) { + const sortedMembers = sortedDictionaries[i]; + for (let j = 0; j < sortedMembers.length; j++) { + const member = sortedMembers[j]; + // Step 4.1.1: get the dictionary member identifier. + const key = member.key; + // Steps 4.1.2-4.1.3: read the JavaScript member value. + const jsMemberValue = jsDict == null ? undefined : jsDict[key]; + + // Step 4.1.4: convert and store present member values. + if (jsMemberValue !== undefined) { + const converter = member.converter; + // Step 4.1.4.1: convert the JavaScript value to IDL. + const idlMemberValue = converter( + jsMemberValue, + { + __proto__: null, + ...options, + context: dictionaryMemberContext(key, options), + }, + ); + // Validators are a Node.js extension after conversion. They let + // consumers reject known unsupported values while dictionary + // conversion still has precise member context. Web Crypto uses this + // so SubtleCrypto.supports() can make accurate decisions from + // normalized dictionaries instead of probing by running operations. + member.validator?.(idlMemberValue, jsDict); + // Step 4.1.4.2: set idlDict[key] to the IDL value. + idlDict[key] = idlMemberValue; + } else if (typeof member.defaultValue === 'function') { + // Step 4.1.5: store the member default value. + idlDict[key] = member.defaultValue(); + } else if (member.required) { + // Step 4.1.6: required missing members throw. + throw makeException( + missingDictionaryMemberMessage(dictionaryName, key), + { __proto__: null, ...options, code: 'ERR_MISSING_OPTION' }); + } } } + // Step 5: return the IDL dictionary. return idlDict; }; } -// https://webidl.spec.whatwg.org/#es-sequence +/** + * Creates a converter for a Web IDL sequence type. + * @see https://webidl.spec.whatwg.org/#es-sequence + * @param {Converter} converter Element converter. + * @returns {Converter} + */ function createSequenceConverter(converter) { - return function(V, opts = kEmptyObject) { - if (type(V) !== OBJECT) { + return function(V, options = kEmptyObject) { + // Web IDL sequence conversion step 1: require an ECMA-262 Object. + if (type(V) !== 'Object') { throw makeException( - 'can not be converted to sequence.', - opts); + 'cannot be converted to sequence.', + options); } - const iter = V?.[SymbolIterator]?.(); - if (iter === undefined) { + + // Step 2: GetMethod(V, %Symbol.iterator%). + const method = V[SymbolIterator]; + // Step 3: throw if the iterator method is undefined, null, or not callable. + if (typeof method !== 'function') { throw makeException( - 'can not be converted to sequence.', - opts); + 'cannot be converted to sequence.', + options); } - const array = []; + + // Step 4 and create-sequence step 1: get the iterator record. + const iterator = FunctionPrototypeCall(method, V); + const nextMethod = iterator?.next; + if (typeof nextMethod !== 'function') { + throw makeException( + 'cannot be converted to sequence.', + options); + } + + // Create-sequence step 2: initialize i to 0. + const idlSequence = []; while (true) { - const res = iter?.next?.(); - if (res === undefined) { + // Step 3.1: IteratorStepValue(iteratorRecord). + const next = FunctionPrototypeCall(nextMethod, iterator); + if (type(next) !== 'Object') { throw makeException( - 'can not be converted to sequence.', - opts); + 'cannot be converted to sequence.', + options); } - if (res.done === true) break; - const val = converter(res.value, { + // Step 3.2: IteratorComplete applies ToBoolean(done). + if (next.done) { + break; + } + // Step 3.3: convert next to an IDL value of type T. + const idlValue = converter(next.value, { __proto__: null, - ...opts, - context: `${opts.context}[${array.length}]`, + ...options, + context: sequenceElementContext(idlSequence.length, options), }); - ArrayPrototypePush(array, val); - }; - return array; + // Step 3.4: store the value and advance i. + ArrayPrototypePush(idlSequence, idlValue); + } + return idlSequence; }; } -// https://webidl.spec.whatwg.org/#js-interface -function createInterfaceConverter(name, I) { - return (V, opts = kEmptyObject) => { - // 1. If V implements I, then return the IDL interface type value that - // represents a reference to that platform object. - if (ObjectPrototypeIsPrototypeOf(I, V)) return V; - // 2. Throw a TypeError. - throw new ERR_INVALID_ARG_TYPE( - typeof opts.context === 'string' ? opts.context : 'value', name, V, - ); +/** + * Creates a converter for a Web IDL interface type. + * @see https://webidl.spec.whatwg.org/#js-interface + * @param {string} name Interface identifier. + * @param {object} prototype Interface prototype object. + * @returns {Converter} + */ +function createInterfaceConverter(name, prototype) { + return (V, options = kEmptyObject) => { + // Web IDL interface conversion step 1: return V if it implements I. + if (ObjectPrototypeIsPrototypeOf(prototype, V)) { + return V; + } + // Step 2: otherwise throw. + throw makeException( + `is not of type ${name}.`, + options); }; } -// Returns the [[ViewedArrayBuffer]] of an ArrayBufferView without leaving JS. +/** + * Returns the ArrayBuffer or SharedArrayBuffer viewed by a typed array or + * DataView. + * @param {ArrayBufferView} V TypedArray or DataView. + * @returns {ArrayBuffer|SharedArrayBuffer} + */ function getViewedArrayBuffer(V) { + // Buffer view conversion steps read V.[[ViewedArrayBuffer]]. 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) { +/** + * Validates [AllowShared] and [AllowResizable] backing-store constraints. + * @param {ArrayBuffer|SharedArrayBuffer} buffer Backing buffer. + * @param {ConversionOptions} options Conversion options. + * @returns {void} + */ +function validateBufferSourceBacking(buffer, options) { + let resizable; try { - ArrayBufferPrototypeGetByteLength(buffer); - return false; + // ArrayBuffer.prototype.resizable is an ArrayBuffer brand check and the + // [AllowResizable] value we need. For SharedArrayBuffer-backed views it + // throws, which lets this path avoid a separate IsSharedArrayBuffer check. + // BufferSource has separate inline logic because [AllowShared] cannot be + // used with that typedef. + resizable = ArrayBufferPrototypeGetResizable(buffer); } catch { - return true; + // ArrayBufferView conversion step 2: reject SharedArrayBuffer + // backing stores unless [AllowShared] is present. + if (!options.allowShared) { + throw makeException( + 'is a view on a SharedArrayBuffer, which is not allowed.', + options); + } + // Step 3: reject non-fixed SharedArrayBuffer backing stores unless + // [AllowResizable] is present. + validateAllowGrowableSharedArrayBuffer(buffer, options); + return; + } + + // ArrayBuffer conversion step 3 and ArrayBufferView conversion step 3: + // reject non-fixed ArrayBuffer backing stores unless [AllowResizable] + // is present. + validateAllowResizableArrayBuffer(resizable, options); +} + +/** + * Validates the [AllowResizable] constraint for growable SharedArrayBuffer. + * @param {SharedArrayBuffer} buffer SharedArrayBuffer backing buffer. + * @param {ConversionOptions} options Conversion options. + * @returns {void} + */ +function validateAllowGrowableSharedArrayBuffer(buffer, options) { + // SharedArrayBuffer and ArrayBufferView conversion step 3: + // IsFixedLengthArrayBuffer(buffer) must be true without [AllowResizable]. + // Do not use a primordial getter here. When this module is included in the + // startup snapshot, an early-captured SharedArrayBuffer.prototype.growable + // getter does not detect growable buffers created after deserialization. + // Lazily capturing the getter would work, but it would observe the runtime + // prototype at first comparison, so it would not be an actual primordial. + if (!options.allowResizable && buffer.growable) { + throw makeException( + 'is backed by a growable SharedArrayBuffer, which is not allowed.', + options); } } -// https://webidl.spec.whatwg.org/#ArrayBufferView -converters.ArrayBufferView = (V, opts = kEmptyObject) => { - if (!ArrayBufferIsView(V)) { +/** + * Validates the [AllowResizable] constraint for resizable ArrayBuffer. + * @param {boolean} resizable ArrayBuffer [[ArrayBufferResizable]] value. + * @param {ConversionOptions} options Conversion options. + * @returns {void} + */ +function validateAllowResizableArrayBuffer(resizable, options) { + // ArrayBuffer and ArrayBufferView conversion step 3: + // IsFixedLengthArrayBuffer(buffer) must be true without [AllowResizable]. + // Read [[ArrayBufferResizable]] first so fixed buffers skip the options + // property lookup on this hot path. + if (resizable && !options.allowResizable) { throw makeException( - 'is not an ArrayBufferView.', - opts); + 'is backed by a resizable ArrayBuffer, which is not allowed.', + options); } - if (isSharedArrayBufferBacking(getViewedArrayBuffer(V))) { +} + +/** + * Converts a JavaScript value to the IDL Uint8Array type. + * @param {any} V JavaScript value. + * @param {ConversionOptions} [options] Conversion options. + * @returns {Uint8Array} + */ +converters.Uint8Array = (V, options = kEmptyObject) => { + // Typed array conversion steps 1-2: T is Uint8Array, and V must + // have [[TypedArrayName]] equal to "Uint8Array". + if (!ArrayBufferIsView(V) || + TypedArrayPrototypeGetSymbolToStringTag(V) !== 'Uint8Array') { throw makeException( - 'is a view on a SharedArrayBuffer, which is not allowed.', - opts); + 'is not an Uint8Array object.', + options); } + // Steps 3-4: validate [AllowShared] and [AllowResizable]. + validateBufferSourceBacking(TypedArrayPrototypeGetBuffer(V), options); + // Step 5: return a reference to the same object. return V; }; -// https://webidl.spec.whatwg.org/#BufferSource -converters.BufferSource = (V, opts = kEmptyObject) => { +/** + * Converts a JavaScript value to the IDL BufferSource typedef. + * @see https://webidl.spec.whatwg.org/#BufferSource + * @param {any} V JavaScript value. + * @param {ConversionOptions} [options] Conversion options. + * @returns {ArrayBuffer|ArrayBufferView} + */ +converters.BufferSource = (V, options = kEmptyObject) => { + // BufferSource is a typedef for (ArrayBufferView or ArrayBuffer). + // [AllowShared] cannot be used with BufferSource because the ArrayBuffer + // union branch does not support it. Use AllowSharedBufferSource instead. if (ArrayBufferIsView(V)) { - if (isSharedArrayBufferBacking(getViewedArrayBuffer(V))) { + const buffer = getViewedArrayBuffer(V); + // ArrayBufferView conversion steps 2-4: validate the viewed buffer + // and return a reference to the same view. + // Keep this logic inline instead of calling validateBufferSourceBacking(). + // BufferSource conversion is hot, and this avoids a helper call while still + // using a primordial getter for the backing-buffer internal-slot check. + // Unlike validateBufferSourceBacking(), this intentionally ignores + // options.allowShared because [AllowShared] cannot be used with + // BufferSource. + let resizable; + try { + // ArrayBuffer.prototype.resizable is both the ArrayBuffer brand check + // and the step 3 value. It throws for SharedArrayBuffer, which lets this + // path reject SAB-backed views without an extra byteLength getter call. + resizable = ArrayBufferPrototypeGetResizable(buffer); + } catch { throw makeException( 'is a view on a SharedArrayBuffer, which is not allowed.', - opts); + options); + } + if (resizable && !options.allowResizable) { + throw makeException( + 'is backed by a resizable ArrayBuffer, which is not allowed.', + options); } - return V; } - if (!isArrayBuffer(V)) { + // ArrayBuffer conversion steps 1-2: require a non-shared ArrayBuffer. + // Use the primordial resizable getter as both the ArrayBuffer brand check + // and the step 3 value. This avoids isArrayBuffer(V) followed by another + // getter call, and rejects SharedArrayBuffer on this union branch. + let resizable; + try { + resizable = ArrayBufferPrototypeGetResizable(V); + } catch { throw makeException( 'is not instance of ArrayBuffer, Buffer, TypedArray, or DataView.', - opts); + options); } + // ArrayBuffer conversion step 3: validate [AllowResizable]. + // ArrayBufferPrototypeGetResizable(V) already excluded SharedArrayBuffer, + // so no [AllowShared] validation is needed on this branch. + if (resizable && !options.allowResizable) { + throw makeException( + 'is backed by a resizable ArrayBuffer, which is not allowed.', + options); + } + + // Step 4: return a reference to the same ArrayBuffer. return V; }; +/** + * Converts a JavaScript value to the IDL AllowSharedBufferSource typedef. + * @see https://webidl.spec.whatwg.org/#AllowSharedBufferSource + * @param {any} V JavaScript value. + * @param {ConversionOptions} [options] Conversion options. + * @returns {ArrayBuffer|SharedArrayBuffer|ArrayBufferView} + */ +converters.AllowSharedBufferSource = (V, options = kEmptyObject) => { + // AllowSharedBufferSource is a typedef for + // (ArrayBuffer or SharedArrayBuffer or [AllowShared] ArrayBufferView). + // The union branches are disjoint, so this keeps the hot view path first. + if (ArrayBufferIsView(V)) { + const buffer = getViewedArrayBuffer(V); + let resizable; + try { + resizable = ArrayBufferPrototypeGetResizable(buffer); + } catch { + validateAllowGrowableSharedArrayBuffer(buffer, options); + return V; + } + validateAllowResizableArrayBuffer(resizable, options); + return V; + } + + let resizable; + try { + resizable = ArrayBufferPrototypeGetResizable(V); + } catch { + if (isSharedArrayBuffer(V)) { + validateAllowGrowableSharedArrayBuffer(V, options); + return V; + } + throw makeException( + 'is not instance of ArrayBuffer, SharedArrayBuffer, Buffer, ' + + 'TypedArray, or DataView.', + options); + } + + validateAllowResizableArrayBuffer(resizable, options); + return V; +}; + +/** + * Converts a JavaScript value to the IDL sequence type. + * @see https://webidl.spec.whatwg.org/#es-sequence + * @type {Converter} + */ +converters['sequence'] = + createSequenceConverter(converters.DOMString); + +/** + * Converts a JavaScript value to the IDL sequence type. + * @see https://webidl.spec.whatwg.org/#es-sequence + * @type {Converter} + */ +converters['sequence'] = + createSequenceConverter(converters.object); + module.exports = { - type, converters, convertToInt, + createDictionaryConverter, createEnumConverter, createInterfaceConverter, createSequenceConverter, - createDictionaryConverter, - evenRound, - makeException, + requiredArguments, + type, }; diff --git a/lib/internal/worker/js_transferable.js b/lib/internal/worker/js_transferable.js index 6acc0a3b19a3cc..0ae80d9a728725 100644 --- a/lib/internal/worker/js_transferable.js +++ b/lib/internal/worker/js_transferable.js @@ -100,6 +100,7 @@ function markTransferMode(obj, cloneable = false, transferable = false) { webidl.converters.StructuredSerializeOptions = webidl .createDictionaryConverter( + 'StructuredSerializeOptions', [ { key: 'transfer', diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index 62b90daf7b0463..0572107e9f492e 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -168,19 +168,22 @@ export const vectors = { 'Argon2id'], ], 'deriveBits': [ - [argon2, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 8, passes: 1 }, 32], - [argon2, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 2, memory: 16, passes: 1 }, 32], - [argon2, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 8, passes: 1, secretValue: Buffer.alloc(0) }, 32], - [argon2, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 8, passes: 1, associatedData: Buffer.alloc(0) }, 32], - [argon2, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 8, passes: 1, version: 0x13 }, 32], - [false, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 8, passes: 1, version: 0x14 }, 32], - [false, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 7, passes: 1 }, 32], - [false, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 2, memory: 15, passes: 1 }, 32], - [false, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 8, passes: 1 }, null], - [false, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 8, passes: 1 }, 24], - [false, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 8, passes: 1 }, 31], - [false, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 0, memory: 8, passes: 1 }, 32], - [false, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 16777215, memory: 8, passes: 1 }, 32], + [argon2, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1 }, 32], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 0 }, 32], + [false, { name: 'Argon2d', nonce: Buffer.alloc(7), parallelism: 1, memory: 8, passes: 1 }, 32], + [argon2, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1 }, 32], + [argon2, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 2, memory: 16, passes: 1 }, 32], + [argon2, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1, secretValue: Buffer.alloc(0) }, 32], + [argon2, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1, associatedData: Buffer.alloc(0) }, 32], + [argon2, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1, version: 0x13 }, 32], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1, version: 0x14 }, 32], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 7, passes: 1 }, 32], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 2, memory: 15, passes: 1 }, 32], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1 }, null], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1 }, 24], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1 }, 31], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 0, memory: 8, passes: 1 }, 32], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 16777215, memory: 8, passes: 1 }, 32], ], 'encrypt': [ [chacha, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12) }], diff --git a/test/parallel/test-abortsignal-any.mjs b/test/parallel/test-abortsignal-any.mjs index 19b5569c4779d1..ff5226055b866c 100644 --- a/test/parallel/test-abortsignal-any.mjs +++ b/test/parallel/test-abortsignal-any.mjs @@ -16,7 +16,7 @@ describe('AbortSignal.any()', { concurrency: !process.env.TEST_PARALLEL }, () => () => AbortSignal.any([AbortSignal.abort(), undefined]), { code: 'ERR_INVALID_ARG_TYPE', - message: 'The "signals[1]" argument must be an instance of AbortSignal. Received undefined' + message: 'signals[1] is not of type AbortSignal.', }, ); }); diff --git a/test/parallel/test-blob.js b/test/parallel/test-blob.js index 8ce1c53012ddcc..dd74b244420f2a 100644 --- a/test/parallel/test-blob.js +++ b/test/parallel/test-blob.js @@ -157,6 +157,22 @@ assert.throws(() => new Blob({}), { assert.strictEqual(b.type, ''); } +{ + const b = new Blob(['hello']); + + assert.throws(() => b.slice(1n), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'start is a BigInt and cannot be converted to a number.', + }); + + assert.throws(() => b.slice(0, Symbol()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'end is a Symbol and cannot be converted to a number.', + }); +} + { const b = new Blob([Buffer.from('hello'), Buffer.from('world')]); const mc = new MessageChannel(); diff --git a/test/parallel/test-internal-webidl-buffer-source.js b/test/parallel/test-internal-webidl-buffer-source.js index 2fb529edcde1b7..9e522d7d7b8a6e 100644 --- a/test/parallel/test-internal-webidl-buffer-source.js +++ b/test/parallel/test-internal-webidl-buffer-source.js @@ -4,6 +4,7 @@ require('../common'); const assert = require('assert'); const { test } = require('node:test'); +const vm = require('vm'); const { converters } = require('internal/webidl'); @@ -15,6 +16,18 @@ const TYPED_ARRAY_CTORS = [ 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); @@ -37,6 +50,28 @@ test('BufferSource accepts DataView', () => { 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); @@ -74,6 +109,10 @@ test('BufferSource rejects SAB-backed TypedArray', () => { () => 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', () => { @@ -82,6 +121,10 @@ test('BufferSource rejects SAB-backed DataView', () => { () => 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', () => { @@ -101,107 +144,154 @@ test('BufferSource accepts a detached ArrayBuffer', () => { assert.strictEqual(converters.BufferSource(ab), ab); }); -test('BufferSource rejects objects with a forged @@toStringTag', () => { - const fake = { [Symbol.toStringTag]: 'Uint8Array' }; +test('BufferSource rejects resizable ArrayBuffer by default', () => { + const ab = new ArrayBuffer(0, { maxByteLength: 1 }); assert.throws( - () => converters.BufferSource(fake), + () => converters.BufferSource(ab), { 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)}`, () => { +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(value), + () => converters.BufferSource(view, { + allowShared: true, + allowResizable: true, + }), { 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('AllowSharedBufferSource accepts ArrayBuffer and SharedArrayBuffer', () => { + const ab = new ArrayBuffer(8); + const sab = new SharedArrayBuffer(8); -test('ArrayBufferView accepts TypedArray subclass instance', () => { - class MyU8 extends Uint8Array {} - const sub = new MyU8(4); - assert.strictEqual(converters.ArrayBufferView(sub), sub); + assert.strictEqual(converters.AllowSharedBufferSource(ab), ab); + assert.strictEqual(converters.AllowSharedBufferSource(sab), sab); }); -test('ArrayBufferView accepts TypedArray with null prototype', () => { - const ta = new Uint8Array(4); - Object.setPrototypeOf(ta, null); - assert.strictEqual(converters.ArrayBufferView(ta), ta); -}); +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); -test('ArrayBufferView accepts DataView with null prototype', () => { - const dv = new DataView(new ArrayBuffer(4)); - Object.setPrototypeOf(dv, null); - assert.strictEqual(converters.ArrayBufferView(dv), dv); + assert.strictEqual(converters.AllowSharedBufferSource(ab), ab); + assert.strictEqual(converters.AllowSharedBufferSource(sab), sab); }); -test('ArrayBufferView rejects raw ArrayBuffer', () => { - assert.throws( - () => converters.ArrayBufferView(new ArrayBuffer(4)), - { code: 'ERR_INVALID_ARG_TYPE' }, - ); -}); +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)); -test('ArrayBufferView rejects raw SharedArrayBuffer', () => { - assert.throws( - () => converters.ArrayBufferView(new SharedArrayBuffer(4)), - { code: 'ERR_INVALID_ARG_TYPE' }, - ); + 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('ArrayBufferView rejects SAB-backed TypedArray', () => { - const view = new Uint8Array(new SharedArrayBuffer(4)); - assert.throws( - () => converters.ArrayBufferView(view), - { code: 'ERR_INVALID_ARG_TYPE' }, - ); -}); +test('AllowSharedBufferSource handles resizable buffers with explicit options', () => { + const ab = new ArrayBuffer(0, { maxByteLength: 1 }); + const views = [new Uint8Array(ab), new DataView(ab)]; -test('ArrayBufferView rejects SAB-backed DataView', () => { - const dv = new DataView(new SharedArrayBuffer(4)); assert.throws( - () => converters.ArrayBufferView(dv), + () => 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('ArrayBufferView rejects SAB view whose buffer prototype was reassigned', () => { - const sab = new SharedArrayBuffer(4); - Object.setPrototypeOf(sab, ArrayBuffer.prototype); - const view = new Uint8Array(sab); +test('AllowSharedBufferSource handles growable shared buffers with explicit ' + + 'options', () => { + const sab = createGrowableSharedArrayBuffer(); + const views = [new Uint8Array(sab), new DataView(sab)]; + assert.throws( - () => converters.ArrayBufferView(view), + () => 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('ArrayBufferView rejects objects with a forged @@toStringTag', () => { +test('BufferSource rejects objects with a forged @@toStringTag', () => { const fake = { [Symbol.toStringTag]: 'Uint8Array' }; assert.throws( - () => converters.ArrayBufferView(fake), + () => converters.BufferSource(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)}`, () => { + 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.ArrayBufferView(value), + () => converters.AllowSharedBufferSource(value), { code: 'ERR_INVALID_ARG_TYPE' }, ); }); diff --git a/test/parallel/test-internal-webidl-converttoint.js b/test/parallel/test-internal-webidl-converttoint.js index 7e7c024387a0ec..0713c7f817ea0e 100644 --- a/test/parallel/test-internal-webidl-converttoint.js +++ b/test/parallel/test-internal-webidl-converttoint.js @@ -3,56 +3,394 @@ require('../common'); const assert = require('assert'); -const { convertToInt, evenRound } = require('internal/webidl'); +const { convertToInt } = require('internal/webidl'); -assert.strictEqual(evenRound(-0.5), 0); -assert.strictEqual(evenRound(0.5), 0); -assert.strictEqual(evenRound(-1.5), -2); -assert.strictEqual(evenRound(1.5), 2); -assert.strictEqual(evenRound(3.4), 3); -assert.strictEqual(evenRound(4.6), 5); -assert.strictEqual(evenRound(5), 5); -assert.strictEqual(evenRound(6), 6); +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; + }); +} // https://webidl.spec.whatwg.org/#abstract-opdef-converttoint -assert.strictEqual(convertToInt('x', 0, 64), 0); -assert.strictEqual(convertToInt('x', 1, 64), 1); -assert.strictEqual(convertToInt('x', -0.5, 64), 0); -assert.strictEqual(convertToInt('x', -0.5, 64, { signed: true }), 0); -assert.strictEqual(convertToInt('x', -1.5, 64, { signed: true }), -1); +const two63 = 2 ** 63; +const two64 = 2 ** 64; + +assert.strictEqual(convertToInt(0, 64), 0); +assert.strictEqual(convertToInt(1, 64), 1); +assert.strictEqual(convertToInt(-0.5, 64), 0); +assert.strictEqual(convertToInt(-0.5, 64, 'signed'), 0); +assert.strictEqual(convertToInt(-1.5, 64, 'signed'), -1); +assert.strictEqual(convertToInt(2 ** 12 + 1, 12), 1); +assert.strictEqual(convertToInt(-1, 12), 2 ** 12 - 1); +assert.strictEqual(convertToInt(2 ** 11, 12, 'signed'), -(2 ** 11)); +assert.strictEqual(convertToInt(-1, 12, 'signed'), -1); + +{ + const options = { + get enforceRange() { + throw new Error('enforceRange should not be read'); + }, + get clamp() { + throw new Error('clamp should not be read'); + }, + }; + + assert.strictEqual(convertToInt(7, 8, 'unsigned', options), 7); +} + +{ + const opts = { + __proto__: null, + prefix: 'Prefix', + context: 'Context', + }; + + assert.throws(() => convertToInt(1n, 8, 'unsigned', opts), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'Prefix: Context is a BigInt and cannot be converted ' + + 'to a number.', + }); + + assert.throws(() => convertToInt(Symbol(), 8, 'unsigned', opts), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'Prefix: Context is a Symbol and cannot be converted ' + + 'to a number.', + }); + + for (const value of [ + Object(1n), + { valueOf() { return 1n; } }, + { valueOf() { return {}; }, toString() { return 1n; } }, + { [Symbol.toPrimitive]() { return 1n; } }, + Object(Symbol()), + { valueOf() { return Symbol(); } }, + { valueOf() { return {}; }, toString() { return Symbol(); } }, + { [Symbol.toPrimitive]() { return Symbol(); } }, + ]) { + assertPlainTypeError(() => convertToInt(value, 8, 'unsigned', opts)); + } + + assert.strictEqual(convertToInt({ + valueOf() { return {}; }, + toString() { return 7; }, + }, 8), 7); + + { + const value = Object(1n); + value.valueOf = () => 7; + assert.strictEqual(convertToInt(value, 8), 7); + } + + { + const calls = []; + const value = { + [Symbol.toPrimitive](hint) { + calls.push(hint); + return '7'; + }, + valueOf() { + calls.push('valueOf'); + return 1; + }, + toString() { + calls.push('toString'); + return '1'; + }, + }; + + assert.strictEqual(convertToInt(value, 8), 7); + assert.deepStrictEqual(calls, ['number']); + } + + for (const { value, expected } of [ + { + value: { + [Symbol.toPrimitive]: undefined, + valueOf() { return 8; }, + }, + expected: 8, + }, + { + value: { + [Symbol.toPrimitive]: null, + valueOf() { return 9; }, + }, + expected: 9, + }, + ]) { + assert.strictEqual(convertToInt(value, 8), expected); + } + + { + const calls = []; + const value = { + valueOf: 1, + toString() { + calls.push('toString'); + return '10'; + }, + }; + + assert.strictEqual(convertToInt(value, 8), 10); + assert.deepStrictEqual(calls, ['toString']); + } + + { + const calls = []; + const value = { + valueOf() { + calls.push('valueOf'); + return {}; + }, + toString() { + calls.push('toString'); + return '11'; + }, + }; + + assert.strictEqual(convertToInt(value, 8), 11); + assert.deepStrictEqual(calls, ['valueOf', 'toString']); + } + + for (const value of [ + { [Symbol.toPrimitive]: 1 }, + { [Symbol.toPrimitive]() { return {}; } }, + { + valueOf() { return {}; }, + toString() { return {}; }, + }, + ]) { + assertPlainTypeError(() => convertToInt(value, 8, 'unsigned', opts)); + } + + { + const sentinel = new TypeError('sentinel'); + assertSameError(() => convertToInt({ + get [Symbol.toPrimitive]() { + throw sentinel; + }, + }, 8), sentinel); + } + + { + const sentinel = new TypeError('sentinel'); + assertSameError(() => convertToInt({ + get valueOf() { + throw sentinel; + }, + toString() { + return 1; + }, + }, 8), sentinel); + } + + { + const sentinel = new TypeError('sentinel'); + assertSameError(() => convertToInt({ + valueOf() { + throw sentinel; + }, + toString() { + return 1; + }, + }, 8), sentinel); + } +} // EnforceRange -const OutOfRangeValues = [ NaN, Infinity, -Infinity, 2 ** 53, -(2 ** 53) ]; -for (const value of OutOfRangeValues) { - assert.throws(() => convertToInt('x', value, 64, { enforceRange: true }), { +const nonFiniteValues = [NaN, Infinity, -Infinity]; +for (const value of nonFiniteValues) { + assert.throws(() => convertToInt(value, 64, 'unsigned', { + enforceRange: true, + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +assert.strictEqual(convertToInt(-0.8, 64, 'unsigned', { + enforceRange: true, +}), 0); +assert.strictEqual(convertToInt(-0.8, 64, 'signed', { + enforceRange: true, +}), 0); +assert.strictEqual(convertToInt(Number.MAX_SAFE_INTEGER, 64, 'signed', { + enforceRange: true, +}), Number.MAX_SAFE_INTEGER); +assert.strictEqual(convertToInt(Number.MIN_SAFE_INTEGER, 64, 'signed', { + enforceRange: true, +}), Number.MIN_SAFE_INTEGER); +assert.strictEqual(convertToInt(-0.5, 8, 'unsigned', { + enforceRange: true, +}), 0); +assert.strictEqual(convertToInt(255.5, 8, 'unsigned', { + enforceRange: true, +}), 255); + +const outOfRangeValues = [2 ** 53, -(2 ** 53)]; +for (const value of outOfRangeValues) { + assert.throws(() => convertToInt(value, 64, 'unsigned', { + enforceRange: true, + }), { name: 'TypeError', - code: 'ERR_INVALID_ARG_VALUE', + code: 'ERR_OUT_OF_RANGE', }); } +assert.throws(() => convertToInt(Number.MAX_SAFE_INTEGER + 1, 64, 'signed', { + enforceRange: true, +}), { + name: 'TypeError', + code: 'ERR_OUT_OF_RANGE', +}); +assert.throws(() => convertToInt(Number.MIN_SAFE_INTEGER - 1, 64, 'signed', { + enforceRange: true, +}), { + name: 'TypeError', + code: 'ERR_OUT_OF_RANGE', +}); +assert.throws(() => convertToInt(256, 8, 'unsigned', { + enforceRange: true, +}), { + name: 'TypeError', + code: 'ERR_OUT_OF_RANGE', +}); + +{ + const calls = []; + const options = { + get enforceRange() { + calls.push('enforceRange'); + return true; + }, + get clamp() { + calls.push('clamp'); + return true; + }, + }; + + assert.strictEqual(convertToInt(1.5, 8, 'unsigned', options), 1); + assert.deepStrictEqual(calls, ['enforceRange']); +} + +{ + const calls = []; + const options = { + get enforceRange() { + calls.push('enforceRange'); + return true; + }, + get clamp() { + calls.push('clamp'); + return true; + }, + }; + + assert.strictEqual(convertToInt(255.5, 8, 'unsigned', options), 255); + assert.deepStrictEqual(calls, ['enforceRange']); +} + +{ + const calls = []; + const options = { + get enforceRange() { + calls.push('enforceRange'); + return false; + }, + get clamp() { + calls.push('clamp'); + return true; + }, + }; + + assert.strictEqual(convertToInt(256, 8, 'unsigned', options), 255); + assert.deepStrictEqual(calls, ['enforceRange', 'clamp']); +} + +{ + const calls = []; + const options = { + get enforceRange() { + calls.push('enforceRange'); + return false; + }, + get clamp() { + calls.push('clamp'); + return true; + }, + }; + + assert.strictEqual(convertToInt({ valueOf: () => 2.5 }, 8, 'unsigned', options), 2); + assert.deepStrictEqual(calls, ['enforceRange', 'clamp']); +} // Out of range: clamp -assert.strictEqual(convertToInt('x', NaN, 64, { clamp: true }), 0); -assert.strictEqual(convertToInt('x', Infinity, 64, { clamp: true }), Number.MAX_SAFE_INTEGER); -assert.strictEqual(convertToInt('x', -Infinity, 64, { clamp: true }), 0); -assert.strictEqual(convertToInt('x', -Infinity, 64, { signed: true, clamp: true }), Number.MIN_SAFE_INTEGER); -assert.strictEqual(convertToInt('x', 0x1_0000_0000, 32, { clamp: true }), 0xFFFF_FFFF); -assert.strictEqual(convertToInt('x', 0xFFFF_FFFF, 32, { clamp: true }), 0xFFFF_FFFF); -assert.strictEqual(convertToInt('x', 0x8000_0000, 32, { clamp: true, signed: true }), 0x7FFF_FFFF); -assert.strictEqual(convertToInt('x', 0xFFFF_FFFF, 32, { clamp: true, signed: true }), 0x7FFF_FFFF); -assert.strictEqual(convertToInt('x', 0.5, 64, { clamp: true }), 0); -assert.strictEqual(convertToInt('x', 1.5, 64, { clamp: true }), 2); -assert.strictEqual(convertToInt('x', -0.5, 64, { clamp: true }), 0); -assert.strictEqual(convertToInt('x', -0.5, 64, { signed: true, clamp: true }), 0); -assert.strictEqual(convertToInt('x', -1.5, 64, { signed: true, clamp: true }), -2); +assert.strictEqual(convertToInt(NaN, 64, 'unsigned', { clamp: true }), 0); +assert.strictEqual(convertToInt(Infinity, 64, 'unsigned', { clamp: true }), Number.MAX_SAFE_INTEGER); +assert.strictEqual(convertToInt(-Infinity, 64, 'unsigned', { clamp: true }), 0); +assert.strictEqual(convertToInt(-Infinity, 64, 'signed', { clamp: true }), Number.MIN_SAFE_INTEGER); +assert.strictEqual(convertToInt(0x1_0000, 16, 'unsigned', { clamp: true }), 0xFFFF); +assert.strictEqual(convertToInt(0x1_0000_0000, 32, 'unsigned', { clamp: true }), 0xFFFF_FFFF); +assert.strictEqual(convertToInt(0xFFFF_FFFF, 32, 'unsigned', { clamp: true }), 0xFFFF_FFFF); +assert.strictEqual(convertToInt(0x8000, 16, 'signed', { clamp: true }), 0x7FFF); +assert.strictEqual(convertToInt(-0x8001, 16, 'signed', { clamp: true }), -0x8000); +assert.strictEqual(convertToInt(0x8000_0000, 32, 'signed', { clamp: true }), 0x7FFF_FFFF); +assert.strictEqual(convertToInt(0xFFFF_FFFF, 32, 'signed', { clamp: true }), 0x7FFF_FFFF); +assert.strictEqual(convertToInt(0.5, 64, 'unsigned', { clamp: true }), 0); +assert.strictEqual(convertToInt(0.8, 64, 'unsigned', { clamp: true }), 1); +assert.strictEqual(convertToInt(1.5, 64, 'unsigned', { clamp: true }), 2); +assert.strictEqual(convertToInt(-0.5, 64, 'unsigned', { clamp: true }), 0); +assert.strictEqual(convertToInt(-0.5, 64, 'signed', { clamp: true }), 0); +assert.strictEqual(convertToInt(-0.8, 64, 'signed', { clamp: true }), -1); +assert.strictEqual(convertToInt(-1.5, 64, 'signed', { clamp: true }), -2); // Out of range, step 8. -assert.strictEqual(convertToInt('x', NaN, 64), 0); -assert.strictEqual(convertToInt('x', Infinity, 64), 0); -assert.strictEqual(convertToInt('x', -Infinity, 64), 0); -assert.strictEqual(convertToInt('x', 0x1_0000_0000, 32), 0); -assert.strictEqual(convertToInt('x', 0x1_0000_0001, 32), 1); -assert.strictEqual(convertToInt('x', 0xFFFF_FFFF, 32), 0xFFFF_FFFF); +assert.strictEqual(convertToInt(NaN, 64), 0); +assert.strictEqual(convertToInt(Infinity, 64), 0); +assert.strictEqual(convertToInt(-Infinity, 64), 0); +assert.strictEqual(convertToInt(-3, 8), 253); +assert.strictEqual(convertToInt(-3, 8), new Uint8Array([-3])[0]); +assert.strictEqual(convertToInt(0x1_0000, 16), 0); +assert.strictEqual(convertToInt(0x1_0001, 16), 1); +assert.strictEqual(convertToInt(-1, 16), 0xFFFF); +assert.strictEqual(convertToInt(-1, 16), new Uint16Array([-1])[0]); +assert.strictEqual(convertToInt(0x1_0000_0000, 32), 0); +assert.strictEqual(convertToInt(0x1_0000_0001, 32), 1); +assert.strictEqual(convertToInt(0xFFFF_FFFF, 32), 0xFFFF_FFFF); +assert.strictEqual(convertToInt(-1, 32), 0xFFFF_FFFF); +assert.strictEqual(convertToInt(-1, 32), new Uint32Array([-1])[0]); +assert.strictEqual(convertToInt(-8192, 64), 2 ** 64 - 8192); +assert.strictEqual(convertToInt(two63, 64), two63); +assert.strictEqual(convertToInt(two64, 64), 0); +assert.strictEqual(convertToInt(two64 + 8192, 64), 8192); +assert.strictEqual(convertToInt(-(two64 + 2 ** 12), 64), + two64 - 2 ** 12); // Out of range, step 11. -assert.strictEqual(convertToInt('x', 0x8000_0000, 32, { signed: true }), -0x8000_0000); -assert.strictEqual(convertToInt('x', 0xFFF_FFFF, 32, { signed: true }), 0xFFF_FFFF); +assert.strictEqual(convertToInt(0x8000, 16, 'signed'), -0x8000); +assert.strictEqual(convertToInt(0xFFFF, 16, 'signed'), -1); +assert.strictEqual(convertToInt(-0x8001, 16, 'signed'), 0x7FFF); +assert.strictEqual(convertToInt(-0x8001, 16, 'signed'), new Int16Array([-0x8001])[0]); +assert.strictEqual(convertToInt(0x8000_0000, 32, 'signed'), -0x8000_0000); +assert.strictEqual(convertToInt(0xFFF_FFFF, 32, 'signed'), 0xFFF_FFFF); +assert.strictEqual(convertToInt(-200, 8, 'signed'), 56); +assert.strictEqual(convertToInt(-200, 8, 'signed'), new Int8Array([-200])[0]); +assert.strictEqual(convertToInt(-8192, 64, 'signed'), -8192); +assert.strictEqual(convertToInt(-8193, 64, 'signed'), -8193); +assert.strictEqual(convertToInt(two63, 64, 'signed'), -two63); +assert.strictEqual(convertToInt(two63 + 2 ** 11, 64, 'signed'), + -two63 + 2 ** 11); +assert.strictEqual(convertToInt(two64 + 8192, 64, 'signed'), 8192); +assert.strictEqual(convertToInt(-(two64 + 2 ** 12), 64, 'signed'), + -(2 ** 12)); diff --git a/test/parallel/test-internal-webidl.js b/test/parallel/test-internal-webidl.js new file mode 100644 index 00000000000000..bd08648549be78 --- /dev/null +++ b/test/parallel/test-internal-webidl.js @@ -0,0 +1,616 @@ +// 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']; + 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); +} diff --git a/test/parallel/test-performance-resourcetimingbuffersize.js b/test/parallel/test-performance-resourcetimingbuffersize.js index c9c84dc9416f68..f01a31cc4403f6 100644 --- a/test/parallel/test-performance-resourcetimingbuffersize.js +++ b/test/parallel/test-performance-resourcetimingbuffersize.js @@ -37,6 +37,19 @@ async function main() { globalThis, cacheMode, ]; + + assert.throws(() => performance.setResourceTimingBufferSize(1n), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'maxSize is a BigInt and cannot be converted to a number.', + }); + + assert.throws(() => performance.setResourceTimingBufferSize(Symbol()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'maxSize is a Symbol and cannot be converted to a number.', + }); + // Invalid buffer size values are converted to 0. const invalidValues = [ null, undefined, true, false, -1, 0.5, Infinity, NaN, '', 'foo', {}, [], () => {} ]; for (const value of invalidValues) { diff --git a/test/parallel/test-structuredClone-global.js b/test/parallel/test-structuredClone-global.js index e6b63c382b39b1..09e36ad47940ea 100644 --- a/test/parallel/test-structuredClone-global.js +++ b/test/parallel/test-structuredClone-global.js @@ -6,7 +6,7 @@ const assert = require('assert'); const prefix = "Failed to execute 'structuredClone'"; const key = 'transfer'; const context = 'Options'; -const memberConverterError = `${prefix}: ${key} in ${context} can not be converted to sequence.`; +const memberConverterError = `${prefix}: ${key} in ${context} cannot be converted to sequence.`; const dictionaryConverterError = `${prefix}: ${context} cannot be converted to a dictionary`; assert.throws(() => structuredClone(), { code: 'ERR_MISSING_ARGS' }); diff --git a/test/parallel/test-webapi-sharedarraybuffer-rejection.js b/test/parallel/test-webapi-sharedarraybuffer-rejection.js index c5503dfc0a1b2d..c0450e8a6ce6e1 100644 --- a/test/parallel/test-webapi-sharedarraybuffer-rejection.js +++ b/test/parallel/test-webapi-sharedarraybuffer-rejection.js @@ -133,33 +133,28 @@ test('webidl converters.BufferSource accepts regular TypedArray', () => { assert.strictEqual(converters.BufferSource(ta), ta); }); -test('webidl converters.ArrayBufferView rejects SAB-backed Uint8Array', () => { +test('webidl converters.Uint8Array rejects SAB-backed Uint8Array', () => { assert.throws( - () => converters.ArrayBufferView(sabView), + () => converters.Uint8Array(sabView), { code: 'ERR_INVALID_ARG_TYPE' }, ); }); -test('webidl converters.ArrayBufferView rejects SAB-backed DataView', () => { +test('webidl converters.Uint8Array rejects DataView', () => { assert.throws( - () => converters.ArrayBufferView(sabDataView), + () => converters.Uint8Array(sabDataView), { code: 'ERR_INVALID_ARG_TYPE' }, ); }); -test('webidl converters.ArrayBufferView rejects non-view', () => { +test('webidl converters.Uint8Array rejects non-view', () => { assert.throws( - () => converters.ArrayBufferView('not a view'), + () => converters.Uint8Array('not a view'), { code: 'ERR_INVALID_ARG_TYPE' }, ); }); -test('webidl converters.ArrayBufferView accepts regular Uint8Array', () => { +test('webidl converters.Uint8Array accepts regular Uint8Array', () => { const ta = new Uint8Array(4); - assert.strictEqual(converters.ArrayBufferView(ta), ta); -}); - -test('webidl converters.ArrayBufferView accepts regular DataView', () => { - const dv = new DataView(new ArrayBuffer(4)); - assert.strictEqual(converters.ArrayBufferView(dv), dv); + assert.strictEqual(converters.Uint8Array(ta), ta); }); diff --git a/test/parallel/test-webcrypto-webidl.js b/test/parallel/test-webcrypto-webidl.js index 99386176c4d6ef..0f8f57ad6fb6a1 100644 --- a/test/parallel/test-webcrypto-webidl.js +++ b/test/parallel/test-webcrypto-webidl.js @@ -16,6 +16,21 @@ const prefix = "Failed to execute 'fn' on 'interface'"; const context = '1st argument'; const opts = { prefix, context }; +function asIdlDictionary(value) { + return { __proto__: null, ...value }; +} + +function assertIdlDictionary(actual, expected) { + assert.deepStrictEqual(actual, asIdlDictionary(expected)); +} + +function assertJsonWebKey(actual, expected) { + const idlDictionary = asIdlDictionary(expected); + if (idlDictionary.oth !== undefined) + idlDictionary.oth = idlDictionary.oth.map(asIdlDictionary); + assert.deepStrictEqual(actual, idlDictionary); +} + // Required arguments.length { assert.throws(() => webidl.requiredArguments(0, 3, { prefix }), { @@ -158,6 +173,14 @@ const opts = { prefix, context }; code: 'ERR_INVALID_ARG_TYPE', message: `${prefix}: ${context} is a view on a SharedArrayBuffer, which is not allowed.` }); + + { + const resizable = new ArrayBuffer(8, { maxByteLength: 16 }); + const view = new Uint8Array(resizable); + + // TODO(panva): Reject resizable backing stores in a semver-major + assert.deepStrictEqual(converters.BigInteger(view), view); + } } // BufferSource @@ -194,6 +217,39 @@ const opts = { prefix, context }; code: 'ERR_INVALID_ARG_TYPE', message: `${prefix}: ${context} is a view on a SharedArrayBuffer, which is not allowed.` }); + + { + const resizable = new ArrayBuffer(8, { maxByteLength: 16 }); + const view = new Uint8Array(resizable); + const dataView = new DataView(resizable); + + // TODO(panva): Reject resizable backing stores in a semver-major by + // removing the crypto/webidl BufferSource override. + assert.deepStrictEqual(converters.BufferSource(resizable), resizable); + assert.deepStrictEqual(converters.BufferSource(view), view); + assert.deepStrictEqual(converters.BufferSource(dataView), dataView); + const resizableError = { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: `${prefix}: ${context} is backed by a resizable ` + + 'ArrayBuffer, which is not allowed.', + }; + assert.throws(() => converters.BufferSource(resizable, { + __proto__: null, + ...opts, + allowResizable: false, + }), resizableError); + assert.throws(() => converters.BufferSource(view, { + __proto__: null, + ...opts, + allowResizable: false, + }), resizableError); + assert.throws(() => converters.BufferSource(dataView, { + __proto__: null, + ...opts, + allowResizable: false, + }), resizableError); + } } // CryptoKey @@ -231,8 +287,8 @@ const opts = { prefix, context }; { oth: [] }, { oth: [{ r: '', d: '', t: '' }] }, ]) { - assert.deepStrictEqual(converters.JsonWebKey(good), good); - assert.deepStrictEqual(converters.JsonWebKey({ ...good, filtered: 'out' }), good); + assertJsonWebKey(converters.JsonWebKey(good), good); + assertJsonWebKey(converters.JsonWebKey({ ...good, filtered: 'out' }), good); } } @@ -255,7 +311,7 @@ const opts = { prefix, context }; assert.throws(() => converters.KeyFormat(bad, opts), { name: 'TypeError', code: 'ERR_INVALID_ARG_VALUE', - message: `${prefix}: ${context} value '${bad}' is not a valid enum value of type KeyFormat.`, + message: `${prefix}: ${context} '${bad}' is not a valid enum value of type KeyFormat.`, }); } } @@ -283,7 +339,7 @@ const opts = { prefix, context }; assert.throws(() => converters.KeyUsage(bad, opts), { name: 'TypeError', code: 'ERR_INVALID_ARG_VALUE', - message: `${prefix}: ${context} value '${bad}' is not a valid enum value of type KeyUsage.`, + message: `${prefix}: ${context} '${bad}' is not a valid enum value of type KeyUsage.`, }); } } @@ -291,12 +347,12 @@ const opts = { prefix, context }; // Algorithm { const good = { name: 'RSA-PSS' }; - assert.deepStrictEqual(converters.Algorithm({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.Algorithm({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converters.Algorithm({}, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'Algorithm' because 'name' is required in 'Algorithm'.`, + message: `${prefix}: ${context} cannot be converted to 'Algorithm' because 'name' is required in 'Algorithm'.`, }); } @@ -316,12 +372,12 @@ const opts = { prefix, context }; publicExponent: new Uint8Array([1, 0, 1]), }, ]) { - assert.deepStrictEqual(converters.RsaHashedKeyGenParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.RsaHashedKeyGenParams({ ...good, filtered: 'out' }, opts), good); for (const required of ['hash', 'publicExponent', 'modulusLength']) { assert.throws(() => converters.RsaHashedKeyGenParams({ ...good, [required]: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'RsaHashedKeyGenParams' because '${required}' is required in 'RsaHashedKeyGenParams'.`, + message: `${prefix}: ${context} cannot be converted to 'RsaHashedKeyGenParams' because '${required}' is required in 'RsaHashedKeyGenParams'.`, }); } } @@ -333,11 +389,11 @@ const opts = { prefix, context }; { name: 'RSA-OAEP', hash: { name: 'SHA-1' } }, { name: 'RSA-OAEP', hash: 'SHA-1' }, ]) { - assert.deepStrictEqual(converters.RsaHashedImportParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.RsaHashedImportParams({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converters.RsaHashedImportParams({ ...good, hash: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'RsaHashedImportParams' because 'hash' is required in 'RsaHashedImportParams'.`, + message: `${prefix}: ${context} cannot be converted to 'RsaHashedImportParams' because 'hash' is required in 'RsaHashedImportParams'.`, }); } } @@ -345,19 +401,19 @@ const opts = { prefix, context }; // RsaPssParams { const good = { name: 'RSA-PSS', saltLength: 20 }; - assert.deepStrictEqual(converters.RsaPssParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.RsaPssParams({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converters.RsaPssParams({ ...good, saltLength: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'RsaPssParams' because 'saltLength' is required in 'RsaPssParams'.`, + message: `${prefix}: ${context} cannot be converted to 'RsaPssParams' because 'saltLength' is required in 'RsaPssParams'.`, }); } // RsaOaepParams { for (const good of [{ name: 'RSA-OAEP' }, { name: 'RSA-OAEP', label: Buffer.alloc(0) }]) { - assert.deepStrictEqual(converters.RsaOaepParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.RsaOaepParams({ ...good, filtered: 'out' }, opts), good); } } @@ -367,12 +423,12 @@ const opts = { prefix, context }; const { [name]: converter } = converters; const good = { name: 'ECDSA', namedCurve: 'P-256' }; - assert.deepStrictEqual(converter({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converter({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converter({ ...good, namedCurve: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to '${name}' because 'namedCurve' is required in '${name}'.`, + message: `${prefix}: ${context} cannot be converted to '${name}' because 'namedCurve' is required in '${name}'.`, }); } } @@ -383,11 +439,11 @@ const opts = { prefix, context }; { name: 'ECDSA', hash: { name: 'SHA-1' } }, { name: 'ECDSA', hash: 'SHA-1' }, ]) { - assert.deepStrictEqual(converters.EcdsaParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.EcdsaParams({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converters.EcdsaParams({ ...good, hash: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'EcdsaParams' because 'hash' is required in 'EcdsaParams'.`, + message: `${prefix}: ${context} cannot be converted to 'EcdsaParams' because 'hash' is required in 'EcdsaParams'.`, }); } } @@ -403,11 +459,11 @@ const opts = { prefix, context }; { name: 'HMAC', hash: 'SHA-1' }, { name: 'HMAC', hash: 'SHA-1', length: 32 }, ]) { - assert.deepStrictEqual(converter({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converter({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converter({ ...good, hash: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to '${name}' because 'hash' is required in '${name}'.`, + message: `${prefix}: ${context} cannot be converted to '${name}' because 'hash' is required in '${name}'.`, }); } } @@ -419,12 +475,12 @@ const opts = { prefix, context }; const { [name]: converter } = converters; const good = { name: 'AES-CBC', length: 128 }; - assert.deepStrictEqual(converter({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converter({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converter({ ...good, length: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to '${name}' because 'length' is required in '${name}'.`, + message: `${prefix}: ${context} cannot be converted to '${name}' because 'length' is required in '${name}'.`, }); } } @@ -435,12 +491,12 @@ const opts = { prefix, context }; { name: 'HKDF', hash: { name: 'SHA-1' }, salt: Buffer.alloc(0), info: Buffer.alloc(0) }, { name: 'HKDF', hash: 'SHA-1', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, ]) { - assert.deepStrictEqual(converters.HkdfParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.HkdfParams({ ...good, filtered: 'out' }, opts), good); for (const required of ['hash', 'salt', 'info']) { assert.throws(() => converters.HkdfParams({ ...good, [required]: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'HkdfParams' because '${required}' is required in 'HkdfParams'.`, + message: `${prefix}: ${context} cannot be converted to 'HkdfParams' because '${required}' is required in 'HkdfParams'.`, }); } } @@ -452,12 +508,12 @@ const opts = { prefix, context }; { name: 'PBKDF2', hash: { name: 'SHA-1' }, iterations: 5, salt: Buffer.alloc(0) }, { name: 'PBKDF2', hash: 'SHA-1', iterations: 5, salt: Buffer.alloc(0) }, ]) { - assert.deepStrictEqual(converters.Pbkdf2Params({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.Pbkdf2Params({ ...good, filtered: 'out' }, opts), good); for (const required of ['hash', 'iterations', 'salt']) { assert.throws(() => converters.Pbkdf2Params({ ...good, [required]: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'Pbkdf2Params' because '${required}' is required in 'Pbkdf2Params'.`, + message: `${prefix}: ${context} cannot be converted to 'Pbkdf2Params' because '${required}' is required in 'Pbkdf2Params'.`, }); } } @@ -466,12 +522,12 @@ const opts = { prefix, context }; // AesCbcParams { const good = { name: 'AES-CBC', iv: Buffer.alloc(16) }; - assert.deepStrictEqual(converters.AesCbcParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.AesCbcParams({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converters.AesCbcParams({ ...good, iv: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'AesCbcParams' because 'iv' is required in 'AesCbcParams'.`, + message: `${prefix}: ${context} cannot be converted to 'AesCbcParams' because 'iv' is required in 'AesCbcParams'.`, }); } @@ -482,12 +538,12 @@ const opts = { prefix, context }; { name: 'AES-GCM', iv: Buffer.alloc(0), tagLength: 64 }, { name: 'AES-GCM', iv: Buffer.alloc(0), tagLength: 64, additionalData: Buffer.alloc(0) }, ]) { - assert.deepStrictEqual(converters.AeadParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.AeadParams({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converters.AeadParams({ ...good, iv: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'AeadParams' because 'iv' is required in 'AeadParams'.`, + message: `${prefix}: ${context} cannot be converted to 'AeadParams' because 'iv' is required in 'AeadParams'.`, }); } } @@ -495,13 +551,13 @@ const opts = { prefix, context }; // AesCtrParams { const good = { name: 'AES-CTR', counter: Buffer.alloc(16), length: 20 }; - assert.deepStrictEqual(converters.AesCtrParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.AesCtrParams({ ...good, filtered: 'out' }, opts), good); for (const required of ['counter', 'length']) { assert.throws(() => converters.AesCtrParams({ ...good, [required]: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'AesCtrParams' because '${required}' is required in 'AesCtrParams'.`, + message: `${prefix}: ${context} cannot be converted to 'AesCtrParams' because '${required}' is required in 'AesCtrParams'.`, }); } } @@ -510,12 +566,12 @@ const opts = { prefix, context }; { subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits']).then((kp) => { const good = { name: 'ECDH', public: kp.publicKey }; - assert.deepStrictEqual(converters.EcdhKeyDeriveParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.EcdhKeyDeriveParams({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converters.EcdhKeyDeriveParams({ ...good, public: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'EcdhKeyDeriveParams' because 'public' is required in 'EcdhKeyDeriveParams'.`, + message: `${prefix}: ${context} cannot be converted to 'EcdhKeyDeriveParams' because 'public' is required in 'EcdhKeyDeriveParams'.`, }); }).then(common.mustCall()); } @@ -526,6 +582,28 @@ const opts = { prefix, context }; { name: 'Ed448', context: new Uint8Array() }, { name: 'Ed448' }, ]) { - assert.deepStrictEqual(converters.ContextParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.ContextParams({ ...good, filtered: 'out' }, opts), good); } } + +// Argon2Params +{ + const maxParallelism = 2 ** 24 - 1; + const good = { + name: 'Argon2id', + nonce: Buffer.alloc(8), + parallelism: maxParallelism, + memory: 8 * maxParallelism, + passes: 1, + }; + assertIdlDictionary(converters.Argon2Params({ ...good, filtered: 'out' }, opts), good); + + assert.throws(() => converters.Argon2Params({ + ...good, + parallelism: maxParallelism + 1, + memory: 8 * (maxParallelism + 1), + }, opts), { + name: 'OperationError', + message: 'parallelism must be > 0 and <= 16777215', + }); +}