From b6405f911cffe34c9dee413b4581be05aeba15e4 Mon Sep 17 00:00:00 2001 From: DeepView Autofix <276251120+deepview-autofix@users.noreply.github.com> Date: Sat, 25 Apr 2026 02:28:49 +0300 Subject: [PATCH] fix(webidl): wrap negative values in ConvertToInt via two's complement ConvertToInt's step 10 used JS `%`, whose result has the sign of the dividend, so negative inputs were not wrapped to the modulo range. For unsigned types without [EnforceRange]/[Clamp] this returned a negative number (e.g. -3 in 8 bits returned -3 instead of 253), and for signed types values in the one-wrap band below the lower bound were returned unchanged and outside the valid range (e.g. -200 in 8 bits returned -200 instead of 56). Implement ECMA-262's "x modulo y" semantics so the result lies in [0, 2^bitLength), matching typed-array behavior, and normalize -0 to +0. Co-Authored-By: Claude Co-Authored-By: DeepView Autofix <276251120+deepview-autofix@users.noreply.github.com> Co-Authored-By: Nikita Skovoroda Signed-off-by: Nikita Skovoroda --- lib/web/webidl/index.js | 11 ++++++++++- test/webidl/util.js | 13 ++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/web/webidl/index.js b/lib/web/webidl/index.js index 05a6a4bce58..411403e7cfb 100644 --- a/lib/web/webidl/index.js +++ b/lib/web/webidl/index.js @@ -268,7 +268,16 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, flags) { x = webidl.util.IntegerPart(x) // 10. Set x to x modulo 2^bitLength. - x = x % Math.pow(2, bitLength) + // Use the ECMA-262 "modulo" definition whose result has the sign of the + // divisor, so negative inputs wrap via two's complement instead of being + // returned as-is. Normalize a potential -0 to +0. + const y = Math.pow(2, bitLength) + x = x % y + if (x < 0) { + x += y + } else if (x === 0) { + x = 0 + } // 11. If signedness is "signed" and x ≥ 2^(bitLength − 1), // then return x − 2^bitLength. diff --git a/test/webidl/util.js b/test/webidl/util.js index 30b01a0a745..56efc04288e 100644 --- a/test/webidl/util.js +++ b/test/webidl/util.js @@ -72,7 +72,7 @@ test('webidl.util.ConvertToInt(V)', () => { assert.equal(ConvertToInt(-max - 1, 64, 'signed'), -max, 'signed neg') assert.equal(ConvertToInt(max + 1, 64, 'unsigned'), max + 1, 'unsigned pos') - assert.equal(ConvertToInt(-max - 1, 64, 'unsigned'), -max - 1, 'unsigned neg') + assert.equal(ConvertToInt(-max - 1, 64, 'unsigned'), 2 ** 64 - max, 'unsigned neg wraps to two\'s complement') for (const signedness of ['signed', 'unsigned']) { assert.equal(ConvertToInt(Infinity, 64, signedness), 0) @@ -144,4 +144,15 @@ test('webidl.util.ConvertToInt(V)', () => { -(2 ** 31), '32-bit signed EnforceRange lower bound' ) + + // Negative inputs must wrap via two's complement instead of being returned + // as-is. These mirror the corresponding typed-array behavior, e.g. + // new Uint8Array([-3])[0] === 253, new Int8Array([-200])[0] === 56. + assert.equal(ConvertToInt(-3, 8, 'unsigned'), 253, '8-bit unsigned wraps -3 to 253') + assert.equal(ConvertToInt(-1, 32, 'unsigned'), 4294967295, '32-bit unsigned wraps -1') + assert.equal(ConvertToInt(-200, 8, 'signed'), 56, '8-bit signed wraps -200 to 56') + assert.ok( + Object.is(ConvertToInt(-256, 8, 'unsigned'), 0), + 'modulo wrap of negative multiple of 2^bitLength returns +0, not -0' + ) })