diff --git a/lib/internal/perf/performance.js b/lib/internal/perf/performance.js index 0053ec23a7afcb..77c8c7ae1c4904 100644 --- a/lib/internal/perf/performance.js +++ b/lib/internal/perf/performance.js @@ -44,6 +44,7 @@ const timerify = require('internal/perf/timerify'); const { customInspectSymbol: kInspect, kEnumerableProperty, kEmptyObject } = require('internal/util'); const { inspect } = require('util'); const { validateInternalField } = require('internal/validators'); +const { convertToInt } = require('internal/webidl'); const { getTimeOriginTimestamp @@ -144,6 +145,8 @@ class Performance extends EventTarget { if (arguments.length === 0) { throw new ERR_MISSING_ARGS('maxSize'); } + // unsigned long + maxSize = convertToInt('maxSize', maxSize, 32); return setResourceTimingBufferSize(maxSize); } diff --git a/lib/internal/webidl.js b/lib/internal/webidl.js new file mode 100644 index 00000000000000..66edd63ca16a65 --- /dev/null +++ b/lib/internal/webidl.js @@ -0,0 +1,163 @@ +'use strict'; + +const { + MathAbs, + MathMax, + MathMin, + MathPow, + MathSign, + MathTrunc, + NumberIsNaN, + NumberMAX_SAFE_INTEGER, + NumberMIN_SAFE_INTEGER, +} = primordials; + +const { + codes: { + ERR_INVALID_ARG_VALUE, + }, +} = require('internal/errors'); +const { kEmptyObject } = require('internal/util'); + +// https://webidl.spec.whatwg.org/#abstract-opdef-integerpart +const integerPart = MathTrunc; + +/* 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 */ +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) { + return i % 2 === 0 ? i : i + sign; + } + const r = reminder < 0.5 ? i : i + sign; + // Convert -0 to +0. + if (r === 0) { + return 0; + } + return r; +} + +function pow2(exponent) { + // << operates on 32 bit signed integers. + if (exponent < 31) { + return 1 << exponent; + } + if (exponent === 31) { + return 0x8000_0000; + } + if (exponent === 32) { + return 0x1_0000_0000; + } + 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. +function modulo(x, y) { + const r = x % y; + // Convert -0 to +0. + if (r === 0) { + return 0; + } + return r; +} + +// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint +function convertToInt(name, value, bitLength, options = kEmptyObject) { + const { signed = false, enforceRange = false, clamp = false } = options; + + let upperBound; + let lowerBound; + // 1. If bitLength is 64, then: + if (bitLength === 64) { + // 1.1. Let upperBound be 2^53 − 1. + 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; + } else if (!signed) { + // 2. Otherwise, if signedness is "unsigned", then: + // 2.1. Let lowerBound be 0. + // 2.2. Let upperBound be 2^bitLength − 1. + lowerBound = 0; + upperBound = pow2(bitLength) - 1; + } 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. + 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); + } + // 6.2. Set x to IntegerPart(x). + x = integerPart(x); + + // 6.3. If x < lowerBound or x > upperBound, then throw a TypeError. + if (x < lowerBound || x > upperBound) { + throw new ERR_INVALID_ARG_VALUE(name, x); + } + + // 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). + x = MathMin(MathMax(x, lowerBound), upperBound); + + // 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); + + // 7.3. Return x. + return x; + } + + // 8. If x is NaN, +0, +∞, or −∞, then return +0. + if (NumberIsNaN(x) || x === 0 || x === Infinity || x === -Infinity) { + return 0; + } + + // 9. Set x to IntegerPart(x). + x = integerPart(x); + + // 10. Set x to x modulo 2^bitLength. + x = modulo(x, pow2(bitLength)); + + // 11. If signedness is "signed" and x ≥ 2^(bitLength − 1), then return x − + // 2^bitLength. + if (signed && x >= pow2(bitLength - 1)) { + return x - pow2(bitLength); + } + + // 12. Otherwise, return x. + return x; +} + +module.exports = { + convertToInt, + evenRound, +}; diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index ae8dcf315d5131..577db94c6eb9b8 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -149,6 +149,7 @@ const expectedModules = new Set([ 'NativeModule internal/validators', 'NativeModule internal/vm/module', 'NativeModule internal/wasm_web_api', + 'NativeModule internal/webidl', 'NativeModule internal/webstreams/adapters', 'NativeModule internal/webstreams/compression', 'NativeModule internal/webstreams/encoding', diff --git a/test/parallel/test-internal-webidl-converttoint.js b/test/parallel/test-internal-webidl-converttoint.js new file mode 100644 index 00000000000000..7e7c024387a0ec --- /dev/null +++ b/test/parallel/test-internal-webidl-converttoint.js @@ -0,0 +1,58 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); +const assert = require('assert'); +const { convertToInt, evenRound } = 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); + +// 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); + +// EnforceRange +const OutOfRangeValues = [ NaN, Infinity, -Infinity, 2 ** 53, -(2 ** 53) ]; +for (const value of OutOfRangeValues) { + assert.throws(() => convertToInt('x', value, 64, { enforceRange: true }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + }); +} + +// 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); + +// 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); + +// 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); diff --git a/test/parallel/test-performance-resourcetimingbuffersize.js b/test/parallel/test-performance-resourcetimingbuffersize.js index eba46fa3ce95e0..3b9e77799b3b25 100644 --- a/test/parallel/test-performance-resourcetimingbuffersize.js +++ b/test/parallel/test-performance-resourcetimingbuffersize.js @@ -30,6 +30,17 @@ const initiatorType = ''; const cacheMode = ''; async function main() { + // 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) { + performance.setResourceTimingBufferSize(value); + performance.markResourceTiming(timingInfo, requestedUrl, initiatorType, globalThis, cacheMode); + assert.strictEqual(performance.getEntriesByType('resource').length, 0); + performance.clearResourceTimings(); + } + // Wait for the buffer full event to be cleared. + await waitBufferFullEvent(); + performance.setResourceTimingBufferSize(1); performance.markResourceTiming(timingInfo, requestedUrl, initiatorType, globalThis, cacheMode); // Trigger a resourcetimingbufferfull event.