From 6bac41a8a367541a00cd0f67fb94a3e4611a3a63 Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Mon, 2 Mar 2026 11:35:19 -0800 Subject: [PATCH 1/5] Improve bigint encoding validation and assembly logic --- src/bigint-encoder.js | 56 +++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/src/bigint-encoder.js b/src/bigint-encoder.js index 861096d..11273d7 100644 --- a/src/bigint-encoder.js +++ b/src/bigint-encoder.js @@ -43,20 +43,32 @@ export function encodeBigIntFromBits(parts, size, unsigned) { throw new TypeError(`expected bigint-like values, got: ${parts} (${e})`); } - // check for sign mismatches for single inputs (this is a special case to - // handle one parameter passed to e.g. UnsignedHyper et al.) - // see https://github.com/stellar/js-xdr/pull/100#discussion_r1228770845 - if (unsigned && parts.length === 1 && parts[0] < 0n) { - throw new RangeError(`expected a positive value, got: ${parts}`); + // fast path: single value — validate and return directly without assembly + if (parts.length === 1) { + const value = parts[0]; + if (unsigned && value < 0n) { + throw new RangeError(`expected a positive value, got: ${parts}`); + } + const [min, max] = calculateBigIntBoundaries(size, unsigned); + if (value < min || value > max) { + throw new TypeError( + `bigint value ${value} for ${formatIntName( + size, + unsigned + )} out of range [${min}, ${max}]` + ); + } + return value; } - // encode in big-endian fashion, shifting each slice by the slice size - let result = BigInt.asUintN(sliceSize, parts[0]); // safe: len >= 1 - for (let i = 1; i < parts.length; i++) { + // multi-part assembly: encode in big-endian fashion, shifting each slice + let result = 0n; + + for (let i = 0; i < parts.length; i++) { + assertSliceFits(parts[i], sliceSize); result |= BigInt.asUintN(sliceSize, parts[i]) << BigInt(i * sliceSize); } - // interpret value as signed if necessary and clamp it if (!unsigned) { result = BigInt.asIntN(size, result); } @@ -106,15 +118,11 @@ export function sliceBigInt(value, iSize, sliceSize) { } const shift = BigInt(sliceSize); + const mask = (1n << shift) - 1n; - // iterate shift and mask application const result = new Array(total); for (let i = 0; i < total; i++) { - // we force a signed interpretation to preserve sign in each slice value, - // but downstream can convert to unsigned if it's appropriate - result[i] = BigInt.asIntN(sliceSize, value); // clamps to size - - // move on to the next chunk + result[i] = value & mask; value >>= shift; } @@ -139,3 +147,21 @@ export function calculateBigIntBoundaries(size, unsigned) { const boundary = 1n << BigInt(size - 1); return [0n - boundary, boundary - 1n]; } + +/** + * Asserts that a given part fits within the specified slice size. + * @param {bigint | number | string} part - The part to check. + * @param {number} sliceSize - The size of the slice in bits (e.g., 32, 64, 128) + * @returns {void} + * @throws {RangeError} If the part does not fit within the slice size. + */ +function assertSliceFits(part, sliceSize) { + const fitsSigned = BigInt.asIntN(sliceSize, part) === part; + const fitsUnsigned = BigInt.asUintN(sliceSize, part) === part; + + if (!fitsSigned && !fitsUnsigned) { + throw new RangeError( + `slice value ${part} does not fit in ${sliceSize} bits` + ); + } +} From 82e1bab4dcba4fc095c1cf53800beae69e5bd148 Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Mon, 2 Mar 2026 11:49:42 -0800 Subject: [PATCH 2/5] Enhance LargeInt read/write methods for signed/unsigned support and improve validation --- src/large-int.js | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/large-int.js b/src/large-int.js index 78f0835..5a24313 100644 --- a/src/large-int.js +++ b/src/large-int.js @@ -58,13 +58,21 @@ export class LargeInt extends XdrPrimitiveType { * @inheritDoc */ static read(reader) { - const { size } = this.prototype; - if (size === 64) return new this(reader.readBigUInt64BE()); - return new this( - ...Array.from({ length: size / 64 }, () => - reader.readBigUInt64BE() - ).reverse() - ); + const { size, unsigned } = this.prototype; + if (size === 64) { + return new this( + unsigned ? reader.readBigUInt64BE() : reader.readBigInt64BE() + ); + } + // assemble bigint directly from big-endian 64-bit chunks + let value = 0n; + for (let i = size / 64 - 1; i >= 0; i--) { + value |= reader.readBigUInt64BE() << BigInt(i * 64); + } + if (!unsigned) { + value = BigInt.asIntN(size, value); + } + return new this(value); } /** @@ -88,12 +96,12 @@ export class LargeInt extends XdrPrimitiveType { writer.writeBigInt64BE(value); } } else { - for (const part of sliceBigInt(value, size, 64).reverse()) { - if (unsigned) { - writer.writeBigUInt64BE(part); - } else { - writer.writeBigInt64BE(part); - } + // extract 64-bit chunks directly from bigint, big-endian order + const uvalue = unsigned ? value : BigInt.asUintN(size, value); + for (let i = size / 64 - 1; i >= 0; i--) { + writer.writeBigUInt64BE( + (uvalue >> BigInt(i * 64)) & 0xffffffffffffffffn + ); } } } @@ -102,7 +110,11 @@ export class LargeInt extends XdrPrimitiveType { * @inheritDoc */ static isValid(value) { - return typeof value === 'bigint' || value instanceof this; + if (value instanceof this) return true; + if (typeof value === 'bigint') { + return value >= this.MIN_VALUE && value <= this.MAX_VALUE; + } + return false; } /** From 89a19eed4bd8caf39dbc9599ceba21d63fff576d Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Mon, 2 Mar 2026 11:50:14 -0800 Subject: [PATCH 3/5] Add overflow/underflow tests for Hyper and UnsignedHyper; introduce Int128 and UnsignedInt128 tests --- test/unit/bigint-encoder_test.js | 90 ++++++++++++- test/unit/hyper_test.js | 22 ++++ test/unit/large-int-128_test.js | 216 +++++++++++++++++++++++++++++++ test/unit/unsigned-hyper_test.js | 25 ++++ 4 files changed, 347 insertions(+), 6 deletions(-) create mode 100644 test/unit/large-int-128_test.js diff --git a/test/unit/bigint-encoder_test.js b/test/unit/bigint-encoder_test.js index 1424c84..e7e6cc9 100644 --- a/test/unit/bigint-encoder_test.js +++ b/test/unit/bigint-encoder_test.js @@ -250,6 +250,84 @@ describe('encodeBigIntWithPrecision', function () { } } }); + + it(`throws on slice overflow and underflow`, () => { + const invalidCases = [ + { + parts: [0n, 0x100000000n], + bits: 64, + unsigned: true, + reason: 'u32 slice overflow (+2^32)' + }, + { + parts: [0n, -0x80000001n], + bits: 64, + unsigned: false, + reason: 'i32 slice underflow (< -2^31)' + }, + { + parts: [0n, 0n, 0n, 0x10000000000000000n], + bits: 256, + unsigned: true, + reason: 'u64 slice overflow (+2^64)' + }, + { + parts: [0n, 0n, 0n, -0x8000000000000001n], + bits: 256, + unsigned: false, + reason: 'i64 slice underflow (< -2^63)' + } + ]; + + for (const { parts, bits, unsigned, reason } of invalidCases) { + expect( + () => encodeBigIntFromBits(parts, bits, unsigned), + `${formatIntName(bits, unsigned)} should throw for ${reason}` + ).to.throw(RangeError, /does not fit/i); + } + }); + + it(`accepts exact slice boundary values`, () => { + const validCases = [ + { + parts: [0n, 0xffffffffn], + bits: 64, + unsigned: true, + expected: 0xffffffff00000000n, + reason: 'u32 upper boundary (2^32 - 1)' + }, + { + parts: [0n, -0x80000000n], + bits: 64, + unsigned: false, + expected: -0x8000000000000000n, + reason: 'i32 lower boundary (-2^31)' + }, + { + parts: [0n, 0n, 0n, 0xffffffffffffffffn], + bits: 256, + unsigned: true, + expected: + 0xffffffffffffffff000000000000000000000000000000000000000000000000n, + reason: 'u64 upper boundary (2^64 - 1)' + }, + { + parts: [0n, 0n, 0n, -0x8000000000000000n], + bits: 256, + unsigned: false, + expected: + -0x8000000000000000000000000000000000000000000000000000000000000000n, + reason: 'i64 lower boundary (-2^63)' + } + ]; + + for (const { parts, bits, unsigned, expected, reason } of validCases) { + expect( + encodeBigIntFromBits(parts, bits, unsigned), + `${formatIntName(bits, unsigned)} should accept ${reason}` + ).to.eq(expected); + } + }); }); describe('sliceBigInt', function () { @@ -257,8 +335,8 @@ describe('sliceBigInt', function () { const testCases = [ [0n, 64, 64, [0n]], [0n, 256, 256, [0n]], - [-1n, 64, 32, [-1n, -1n]], - [0xfffffffffffffffen, 64, 32, [-2n, -1n]], + [-1n, 64, 32, [0xffffffffn, 0xffffffffn]], + [0xfffffffffffffffen, 64, 32, [0xfffffffen, 0xffffffffn]], [ 0x7fffffffffffffff5cffffffffffffffn, 128, @@ -269,13 +347,13 @@ describe('sliceBigInt', function () { 0x80000000ffffffff0000000100000001n, 128, 32, - [1n, 1n, -1n, -0x80000000n] + [1n, 1n, 0xffffffffn, 0x80000000n] ], [ -0x158fffffffffffffea6fffffffffffffea6fffffffffffffea7n, 256, 64, - [345n, 345n, 345n, -345n] + [0x159n, 0x159n, 0x159n, 0xfffffffffffffea7n] ], [ 0x0000000800000007000000060000000500000004000000030000000200000001n, @@ -287,13 +365,13 @@ describe('sliceBigInt', function () { -0x7fffffff8fffffff9fffffffafffffffbfffffffcfffffffdffffffffn, 256, 32, - [1n, 2n, 3n, 4n, 5n, 6n, 7n, -8n] + [1n, 2n, 3n, 4n, 5n, 6n, 7n, 0xfffffff8n] ], [ -0x7fffffff800000005fffffffa00000003fffffffc00000001ffffffffn, 256, 32, - [1n, -2n, 3n, -4n, 5n, -6n, 7n, -8n] + [1n, 0xfffffffen, 3n, 0xfffffffcn, 5n, 0xfffffffan, 7n, 0xfffffff8n] ] ]; for (let [value, size, sliceSize, expected] of testCases) { diff --git a/test/unit/hyper_test.js b/test/unit/hyper_test.js index 2d5acd4..587c665 100644 --- a/test/unit/hyper_test.js +++ b/test/unit/hyper_test.js @@ -84,3 +84,25 @@ describe('Hyper.fromString', function () { expect(() => Hyper.fromString('105946095601.5')).to.throw(/bigint/); }); }); + +describe('Hyper overflow/underflow', function () { + it('throws when constructing with a value exceeding i64 max', function () { + const tooBig = 2n ** 63n; // one above MAX_VALUE + expect(() => new Hyper(tooBig)).to.throw(/out of range|does not fit/i); + }); + + it('throws when constructing with a value below i64 min', function () { + const tooSmall = -(2n ** 63n) - 1n; // one below MIN_VALUE + expect(() => new Hyper(tooSmall)).to.throw(/out of range|does not fit/i); + }); + + it('throws for a 300-bit bigint', function () { + const huge = 2n ** 300n; + expect(() => new Hyper(huge)).to.throw(/out of range|does not fit/i); + }); + + it('accepts exact boundary values without throwing', function () { + expect(() => new Hyper(Hyper.MAX_VALUE)).to.not.throw(); + expect(() => new Hyper(Hyper.MIN_VALUE)).to.not.throw(); + }); +}); diff --git a/test/unit/large-int-128_test.js b/test/unit/large-int-128_test.js new file mode 100644 index 0000000..cc4af9e --- /dev/null +++ b/test/unit/large-int-128_test.js @@ -0,0 +1,216 @@ +import { XdrWriter } from '../../src/serialization/xdr-writer'; +import { XdrReader } from '../../src/serialization/xdr-reader'; +import { LargeInt } from '../../src/large-int'; + +// -- Inline 128-bit subclasses for testing the >64 bit LargeInt paths -- + +class Int128 extends LargeInt { + constructor(...args) { + super(args); + } + get size() { + return 128; + } + get unsigned() { + return false; + } +} +Int128.defineIntBoundaries(); + +class UnsignedInt128 extends LargeInt { + constructor(...args) { + super(args); + } + get size() { + return 128; + } + get unsigned() { + return true; + } +} +UnsignedInt128.defineIntBoundaries(); + +// ---------- Construction ---------- + +describe('Int128 construction', function () { + it('constructs zero', function () { + const val = new Int128(0n); + expect(val.toBigInt()).to.eql(0n); + }); + + it('constructs positive values', function () { + const val = new Int128(123456789012345678901234n); + expect(val.toBigInt()).to.eql(123456789012345678901234n); + }); + + it('constructs negative values', function () { + const val = new Int128(-42n); + expect(val.toBigInt()).to.eql(-42n); + }); + + it('constructs from exact boundary values', function () { + expect(() => new Int128(Int128.MAX_VALUE)).to.not.throw(); + expect(() => new Int128(Int128.MIN_VALUE)).to.not.throw(); + expect(new Int128(Int128.MAX_VALUE).toBigInt()).to.eql(Int128.MAX_VALUE); + expect(new Int128(Int128.MIN_VALUE).toBigInt()).to.eql(Int128.MIN_VALUE); + }); + + it('constructs from two 64-bit chunks', function () { + // low=1, high=2 => 2 * 2^64 + 1 + const val = new Int128(1n, 2n); + expect(val.toBigInt()).to.eql(2n * 2n ** 64n + 1n); + }); + + it('constructs from four 32-bit chunks', function () { + const val = new Int128(1n, 2n, 3n, 4n); + expect(val.toBigInt()).to.eql((4n << 96n) | (3n << 64n) | (2n << 32n) | 1n); + }); +}); + +describe('UnsignedInt128 construction', function () { + it('constructs zero', function () { + const val = new UnsignedInt128(0n); + expect(val.toBigInt()).to.eql(0n); + }); + + it('constructs max value', function () { + const val = new UnsignedInt128(UnsignedInt128.MAX_VALUE); + expect(val.toBigInt()).to.eql(2n ** 128n - 1n); + }); +}); + +// ---------- Overflow / Underflow ---------- + +describe('Int128 overflow/underflow', function () { + it('throws when value exceeds i128 max', function () { + const tooBig = 2n ** 127n; // one above MAX_VALUE + expect(() => new Int128(tooBig)).to.throw(/out of range|does not fit/i); + }); + + it('throws when value is below i128 min', function () { + const tooSmall = -(2n ** 127n) - 1n; + expect(() => new Int128(tooSmall)).to.throw(/out of range|does not fit/i); + }); + + it('throws for a 300-bit bigint', function () { + expect(() => new Int128(2n ** 300n)).to.throw(/out of range|does not fit/i); + }); +}); + +describe('UnsignedInt128 overflow/underflow', function () { + it('throws when value exceeds u128 max', function () { + const tooBig = 2n ** 128n; + expect(() => new UnsignedInt128(tooBig)).to.throw( + /out of range|does not fit/i + ); + }); + + it('throws for negative values', function () { + expect(() => new UnsignedInt128(-1n)).to.throw(/positive/i); + }); +}); + +// ---------- Read / Write round-trip ---------- + +describe('Int128 read/write', function () { + it('round-trips zero', function () { + const original = new Int128(0n); + const writer = new XdrWriter(16); + Int128.write(original, writer); + const reader = new XdrReader(writer.finalize()); + const decoded = Int128.read(reader); + expect(decoded.toBigInt()).to.eql(0n); + }); + + it('round-trips positive values', function () { + const value = 123456789012345678901234n; + const original = new Int128(value); + const writer = new XdrWriter(16); + Int128.write(original, writer); + const reader = new XdrReader(writer.finalize()); + const decoded = Int128.read(reader); + expect(decoded.toBigInt()).to.eql(value); + }); + + it('round-trips negative values', function () { + const value = -987654321098765432109876n; + const original = new Int128(value); + const writer = new XdrWriter(16); + Int128.write(original, writer); + const reader = new XdrReader(writer.finalize()); + const decoded = Int128.read(reader); + expect(decoded.toBigInt()).to.eql(value); + }); + + it('round-trips max value', function () { + const writer = new XdrWriter(16); + Int128.write(new Int128(Int128.MAX_VALUE), writer); + const reader = new XdrReader(writer.finalize()); + expect(Int128.read(reader).toBigInt()).to.eql(Int128.MAX_VALUE); + }); + + it('round-trips min value', function () { + const writer = new XdrWriter(16); + Int128.write(new Int128(Int128.MIN_VALUE), writer); + const reader = new XdrReader(writer.finalize()); + expect(Int128.read(reader).toBigInt()).to.eql(Int128.MIN_VALUE); + }); +}); + +describe('UnsignedInt128 read/write', function () { + it('round-trips zero', function () { + const writer = new XdrWriter(16); + UnsignedInt128.write(new UnsignedInt128(0n), writer); + const reader = new XdrReader(writer.finalize()); + expect(UnsignedInt128.read(reader).toBigInt()).to.eql(0n); + }); + + it('round-trips max value', function () { + const writer = new XdrWriter(16); + UnsignedInt128.write(new UnsignedInt128(UnsignedInt128.MAX_VALUE), writer); + const reader = new XdrReader(writer.finalize()); + expect(UnsignedInt128.read(reader).toBigInt()).to.eql( + UnsignedInt128.MAX_VALUE + ); + }); +}); + +// ---------- isValid ---------- + +describe('Int128.isValid', function () { + it('returns true for in-range bigint values', function () { + expect(Int128.isValid(0n)).to.be.true; + expect(Int128.isValid(-1n)).to.be.true; + expect(Int128.isValid(Int128.MAX_VALUE)).to.be.true; + expect(Int128.isValid(Int128.MIN_VALUE)).to.be.true; + }); + + it('returns false for out-of-range bigint values', function () { + expect(Int128.isValid(2n ** 127n)).to.be.false; + expect(Int128.isValid(-(2n ** 127n) - 1n)).to.be.false; + }); + + it('returns true for instances', function () { + expect(Int128.isValid(new Int128(42n))).to.be.true; + }); + + it('returns false for non-bigint/non-instance values', function () { + expect(Int128.isValid(42)).to.be.false; + expect(Int128.isValid('42')).to.be.false; + expect(Int128.isValid(null)).to.be.false; + }); +}); + +// ---------- toString / toJSON ---------- + +describe('Int128 serialization helpers', function () { + it('toString returns decimal string', function () { + const val = new Int128(-42n); + expect(val.toString()).to.eql('-42'); + }); + + it('toJSON returns object with string value', function () { + const val = new Int128(999n); + expect(val.toJSON()).to.eql({ _value: '999' }); + }); +}); diff --git a/test/unit/unsigned-hyper_test.js b/test/unit/unsigned-hyper_test.js index 6a1f773..8100f57 100644 --- a/test/unit/unsigned-hyper_test.js +++ b/test/unit/unsigned-hyper_test.js @@ -72,3 +72,28 @@ describe('UnsignedHyper.fromString', function () { expect(() => UnsignedHyper.fromString('105946095601.5')).to.throw(/bigint/); }); }); + +describe('UnsignedHyper overflow', function () { + it('throws when constructing with a value exceeding u64 max', function () { + const tooBig = 2n ** 64n; // one above MAX_VALUE + expect(() => new UnsignedHyper(tooBig)).to.throw( + /out of range|does not fit/i + ); + }); + + it('throws for a 300-bit bigint', function () { + const huge = 2n ** 300n; + expect(() => new UnsignedHyper(huge)).to.throw( + /out of range|does not fit/i + ); + }); + + it('throws for negative values', function () { + expect(() => new UnsignedHyper(-1n)).to.throw(/positive/); + }); + + it('accepts exact boundary values without throwing', function () { + expect(() => new UnsignedHyper(UnsignedHyper.MAX_VALUE)).to.not.throw(); + expect(() => new UnsignedHyper(UnsignedHyper.MIN_VALUE)).to.not.throw(); + }); +}); From 003d162a463043b1ac83e665ebe73bd17ffa7637 Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Mon, 2 Mar 2026 11:52:31 -0800 Subject: [PATCH 4/5] add changelog entry --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec7adda..e7ff75e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ project adheres to [Semantic Versioning](http://semver.org/). * Decoding Array and VarArray now fast fails when the array length exceeds remaining bytes to decode ([#132](https://github.com/stellar/js-xdr/pull/132)) +* Fixed silent truncation of bigint values exceeding the range of sized integers (`Hyper`, `UnsignedHyper`, and other `LargeInt` subtypes). Construction, encoding, and multi-part assembly now throw on overflow/underflow instead of silently clamping. `isValid` also validates value range. `sliceBigInt` now returns unsigned slice values for consistency ([#133](https://github.com/stellar/js-xdr/pull/133)). + + + + ## [v3.1.2](https://github.com/stellar/js-xdr/compare/v3.1.1...v3.1.2) ### Fixed From b46359a05be84911df89ee6211cc83fdf77c04cd Mon Sep 17 00:00:00 2001 From: Ryan Yang Date: Tue, 3 Mar 2026 16:05:10 -0800 Subject: [PATCH 5/5] revert sliceBigInt to use int64 parsing --- src/bigint-encoder.js | 8 ++++++-- test/unit/bigint-encoder_test.js | 12 ++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/bigint-encoder.js b/src/bigint-encoder.js index 11273d7..defa6c4 100644 --- a/src/bigint-encoder.js +++ b/src/bigint-encoder.js @@ -118,11 +118,15 @@ export function sliceBigInt(value, iSize, sliceSize) { } const shift = BigInt(sliceSize); - const mask = (1n << shift) - 1n; + // iterate shift and mask application const result = new Array(total); for (let i = 0; i < total; i++) { - result[i] = value & mask; + // we force a signed interpretation to preserve sign in each slice value, + // but downstream can convert to unsigned if it's appropriate + result[i] = BigInt.asIntN(sliceSize, value); // clamps to size + + // move on to the next chunk value >>= shift; } diff --git a/test/unit/bigint-encoder_test.js b/test/unit/bigint-encoder_test.js index e7e6cc9..e2f978d 100644 --- a/test/unit/bigint-encoder_test.js +++ b/test/unit/bigint-encoder_test.js @@ -335,8 +335,8 @@ describe('sliceBigInt', function () { const testCases = [ [0n, 64, 64, [0n]], [0n, 256, 256, [0n]], - [-1n, 64, 32, [0xffffffffn, 0xffffffffn]], - [0xfffffffffffffffen, 64, 32, [0xfffffffen, 0xffffffffn]], + [-1n, 64, 32, [-1n, -1n]], + [0xfffffffffffffffen, 64, 32, [-2n, -1n]], [ 0x7fffffffffffffff5cffffffffffffffn, 128, @@ -347,13 +347,13 @@ describe('sliceBigInt', function () { 0x80000000ffffffff0000000100000001n, 128, 32, - [1n, 1n, 0xffffffffn, 0x80000000n] + [1n, 1n, -1n, -0x80000000n] ], [ -0x158fffffffffffffea6fffffffffffffea6fffffffffffffea7n, 256, 64, - [0x159n, 0x159n, 0x159n, 0xfffffffffffffea7n] + [345n, 345n, 345n, -345n] ], [ 0x0000000800000007000000060000000500000004000000030000000200000001n, @@ -365,13 +365,13 @@ describe('sliceBigInt', function () { -0x7fffffff8fffffff9fffffffafffffffbfffffffcfffffffdffffffffn, 256, 32, - [1n, 2n, 3n, 4n, 5n, 6n, 7n, 0xfffffff8n] + [1n, 2n, 3n, 4n, 5n, 6n, 7n, -8n] ], [ -0x7fffffff800000005fffffffa00000003fffffffc00000001ffffffffn, 256, 32, - [1n, 0xfffffffen, 3n, 0xfffffffcn, 5n, 0xfffffffan, 7n, 0xfffffff8n] + [1n, -2n, 3n, -4n, 5n, -6n, 7n, -8n] ] ]; for (let [value, size, sliceSize, expected] of testCases) {