From 7543d04fbe5044324cb87fd0e3c4ee2215271f55 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Tue, 30 Jun 2026 14:31:16 +0100 Subject: [PATCH 1/2] fix: throw when encoding a BigInt outside int64/uint64 range With `useBigInt64: true`, `encodeBigInt64` branched only on sign and called the native `DataView.setBigUint64`/`setBigInt64`, which truncate mod 2^64 without throwing. A BigInt outside the representable range was silently corrupted on encode: encode(2n ** 64n, { useBigInt64: true }) // decoded as 0n encode(2n ** 64n + 1n, { useBigInt64: true }) // decoded as 1n encode(-(2n ** 63n) - 1n,{ useBigInt64: true }) // sign flip: 9223372036854775807n The MessagePack int family is fixed 64-bit, so the only representable range is the union of int64 and uint64: [-2^63, 2^64 - 1]. Add a range check that throws for anything outside it, matching how the encoder already rejects other unrepresentable inputs (too-long strings, too-large arrays/maps/binaries). --- src/Encoder.ts | 3 +++ test/bigint64.test.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/Encoder.ts b/src/Encoder.ts index b047c1d..0ac65f8 100644 --- a/src/Encoder.ts +++ b/src/Encoder.ts @@ -291,6 +291,9 @@ export class Encoder { } private encodeBigInt64(object: bigint): void { + if (object < -(BigInt(2) ** BigInt(63)) || object > BigInt(2) ** BigInt(64) - BigInt(1)) { + throw new Error(`Cannot encode BigInt as int64/uint64 because it is out of range: ${object}`); + } if (object >= BigInt(0)) { // uint 64 this.writeU8(0xcf); diff --git a/test/bigint64.test.ts b/test/bigint64.test.ts index e2bf08f..f6f5c96 100644 --- a/test/bigint64.test.ts +++ b/test/bigint64.test.ts @@ -35,4 +35,30 @@ describe("useBigInt64: true", () => { const encoded = encode(value, { useBigInt64: true }); assert.deepStrictEqual(decode(encoded, { useBigInt64: true }), value); }); + + it("round-trips the boundary values of int64/uint64", () => { + const values = [ + BigInt(0), + BigInt(42), + BigInt(2) ** BigInt(63) - BigInt(1), // max int64 + -(BigInt(2) ** BigInt(63)), // min int64 + BigInt(2) ** BigInt(64) - BigInt(1), // max uint64 + ]; + for (const value of values) { + const encoded = encode(value, { useBigInt64: true }); + assert.deepStrictEqual(decode(encoded, { useBigInt64: true }), value); + } + }); + + it("throws when a bigint is out of the int64/uint64 range", () => { + const values = [ + BigInt(2) ** BigInt(64), // uint64 max + 1 + BigInt(2) ** BigInt(64) + BigInt(1), + -(BigInt(2) ** BigInt(63)) - BigInt(1), // int64 min - 1 + -(BigInt(2) ** BigInt(100)), + ]; + for (const value of values) { + assert.throws(() => encode(value, { useBigInt64: true }), /out of range/); + } + }); }); From 1a99635b4fd32a4f1ebd5f1a124a9212826d2103 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Fri, 3 Jul 2026 13:08:15 +0100 Subject: [PATCH 2/2] refactor: hoist int64/uint64 bounds to module constants Compute the int64/uint64 range bounds once at module load instead of on every encodeBigInt64 call, per review feedback. --- src/Encoder.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Encoder.ts b/src/Encoder.ts index 0ac65f8..76051fe 100644 --- a/src/Encoder.ts +++ b/src/Encoder.ts @@ -72,6 +72,10 @@ export type EncoderOptions = Partial< > & ContextOf; +// Bounds of the int64/uint64 range msgpack can represent, precomputed once for encodeBigInt64. +const INT64_MIN = -(BigInt(2) ** BigInt(63)); +const UINT64_MAX = BigInt(2) ** BigInt(64) - BigInt(1); + export class Encoder { private readonly extensionCodec: ExtensionCodecType; private readonly context: ContextType; @@ -291,7 +295,7 @@ export class Encoder { } private encodeBigInt64(object: bigint): void { - if (object < -(BigInt(2) ** BigInt(63)) || object > BigInt(2) ** BigInt(64) - BigInt(1)) { + if (object < INT64_MIN || object > UINT64_MAX) { throw new Error(`Cannot encode BigInt as int64/uint64 because it is out of range: ${object}`); } if (object >= BigInt(0)) {