From d33eac7af93d400765563e2f31ba7842eace0be3 Mon Sep 17 00:00:00 2001 From: James Wyatt Cready-Pyle Date: Mon, 31 Jul 2023 14:59:35 -0400 Subject: [PATCH] Test pb-long without BI support and fix found issues (#573) --- packages/runtime/spec/pb-long.spec.ts | 148 ++++++++++++++++++-------- packages/runtime/src/goog-varint.ts | 4 +- packages/runtime/src/pb-long.ts | 24 +++-- 3 files changed, 124 insertions(+), 52 deletions(-) diff --git a/packages/runtime/spec/pb-long.spec.ts b/packages/runtime/spec/pb-long.spec.ts index be0b8076..12bbc0e6 100644 --- a/packages/runtime/spec/pb-long.spec.ts +++ b/packages/runtime/spec/pb-long.spec.ts @@ -1,8 +1,9 @@ import {PbLong, PbULong} from "../src"; import {int64toString} from "../src/goog-varint"; +import {detectBi} from '../src/pb-long'; -describe('PbULong', function () { +describeWithAndWithoutBISupport('PbULong', function () { it('can be constructed with bits', function () { let bi = new PbULong(0, 1); @@ -19,7 +20,7 @@ describe('PbULong', function () { it('should from() max unsigned int64 native bigint', function () { if (globalThis.BigInt === undefined) - pending('No BigInt support on current platform'); + return expect().nothing(); // @ts-ignore let uLong = PbULong.from(18446744073709551615n); @@ -39,18 +40,27 @@ describe('PbULong', function () { expect(uLong.toString()).toBe('18446744073709551615'); }); - it('should toBigInt()', function () { - if (globalThis.BigInt === undefined) - pending('No BigInt support on current platform'); + it('should toNumber()', function () { + let bi = new PbLong(4294967295, 2097151); + expect(bi.toNumber()).toBe(Number.MAX_SAFE_INTEGER); + // signed max value + bi = new PbLong(4294967295, 2097152); + expect(() => bi.toNumber()).toThrowError("cannot convert to safe number"); + }); + + it('should toBigInt()', function () { let uLong = new PbULong(-1, -1); - // @ts-ignore - expect(uLong.toBigInt()).toBe(18446744073709551615n); + if (globalThis.BigInt === undefined) + expect(() => uLong.toBigInt()).toThrowError(); + else + // @ts-ignore + expect(uLong.toBigInt()).toBe(18446744073709551615n); }); it('should fail from() max unsigned int64 native bigint', function () { if (globalThis.BigInt === undefined) - pending('No BigInt support on current platform'); + return expect().nothing(); // @ts-ignore expect(() => PbULong.from(18446744073709551615n + 10n)).toThrowError("ulong too large"); @@ -65,6 +75,7 @@ describe('PbULong', function () { expect(() => PbULong.from(-1)).toThrowError(); expect(() => PbULong.from(Number.NaN)).toThrowError(); expect(() => PbULong.from(Number.POSITIVE_INFINITY)).toThrowError(); + expect(() => PbULong.from(true as unknown as number)).toThrowError(); }); it('0 has lo = 0, hi = 0', function () { @@ -73,17 +84,24 @@ describe('PbULong', function () { expect(ulong.lo).toBe(0); }); - it('int64toString should serialize the same was as BigInt.toString()', function () { + it('int64toString should serialize the same way as BigInt.toString()', function () { let ulong = PbULong.from(1661324400000); expect(ulong.hi).toBe(386); expect(ulong.lo).toBe(-827943552); expect(ulong.toString()).toBe('1661324400000') expect(int64toString(ulong.lo, ulong.hi)).toBe('1661324400000') }); -}); + it('should return ZERO for "0", 0, or 0n', function () { + expect(PbULong.from("0").isZero()).toBe(true); + expect(PbULong.from(0).isZero()).toBe(true); + if (globalThis.BigInt !== undefined) + // @ts-ignore + expect(PbULong.from(0n).isZero()).toBe(true); + }); +}); -describe('PbLong', function () { +describeWithAndWithoutBISupport('PbLong', function () { it('can be constructed with bits', function () { let bi = new PbLong(0, 1); @@ -93,36 +111,36 @@ describe('PbLong', function () { it('should from() max signed int64 string', function () { let str = '9223372036854775807'; - let uLong = PbLong.from(str); - expect(uLong.lo).toBe(-1); - expect(uLong.hi).toBe(2147483647); - expect(uLong.toString()).toBe("9223372036854775807"); + let long = PbLong.from(str); + expect(long.lo).toBe(-1); + expect(long.hi).toBe(2147483647); + expect(long.toString()).toBe("9223372036854775807"); }); it('should from() min signed int64 string', function () { let str = '-9223372036854775808'; - let uLong = PbLong.from(str); - expect(uLong.lo).toBe(0); - expect(uLong.hi).toBe(-2147483648); + let long = PbLong.from(str); + expect(long.lo).toBe(0); + expect(long.hi).toBe(-2147483648); }); it('should toString() min signed int64', function () { - let uLong = new PbLong(0, -2147483648); - expect(uLong.toString()).toBe('-9223372036854775808'); + let long = new PbLong(0, -2147483648); + expect(long.toString()).toBe('-9223372036854775808'); }); it('should from() max safe integer number', function () { - let uLong = PbLong.from(Number.MAX_SAFE_INTEGER); - expect(uLong.lo).toBe(-1); - expect(uLong.hi).toBe(2097151); - expect(uLong.toNumber()).toBe(Number.MAX_SAFE_INTEGER); + let long = PbLong.from(Number.MAX_SAFE_INTEGER); + expect(long.lo).toBe(-1); + expect(long.hi).toBe(2097151); + expect(long.toNumber()).toBe(Number.MAX_SAFE_INTEGER); }); it('should from() min safe integer number', function () { - let uLong = PbLong.from(Number.MIN_SAFE_INTEGER); - expect(uLong.lo).toBe(1); - expect(uLong.hi).toBe(-2097152); - expect(uLong.toNumber()).toBe(Number.MIN_SAFE_INTEGER); + let long = PbLong.from(Number.MIN_SAFE_INTEGER); + expect(long.lo).toBe(1); + expect(long.hi).toBe(-2097152); + expect(long.toNumber()).toBe(Number.MIN_SAFE_INTEGER); }); it('should fail invalid from() value', function () { @@ -130,17 +148,22 @@ describe('PbLong', function () { expect(() => PbLong.from("0.75")).toThrowError(); expect(() => PbLong.from("1,000")).toThrowError(); expect(() => PbLong.from("1-000")).toThrowError(); - let maxSignedPlusOneStr = "9223372036854775808" - expect(() => PbLong.from(maxSignedPlusOneStr)).toThrowError(); + let maxSignedPlusOneStr = "9223372036854775808"; + expect(() => PbLong.from(maxSignedPlusOneStr)).toThrowError('signed long too large'); + let minSignedMinusOneStr = "-9223372036854775809"; + expect(() => PbLong.from(minSignedMinusOneStr)).toThrowError('signed long too small'); + let minUnsignedStr = '-18446744073709551616'; + expect(() => PbLong.from(minUnsignedStr)).toThrowError('signed long too small'); expect(() => PbLong.from(Number.NaN)).toThrowError(); expect(() => PbLong.from(Number.POSITIVE_INFINITY)).toThrowError(); + expect(() => PbLong.from(true as unknown as number)).toThrowError(); }); it('should toBigInt()', function () { + let bi = new PbLong(-620756991, 53471156); if (globalThis.BigInt === undefined) - pending('No BigInt support on current platform'); + return expect(() => bi.toBigInt()).toThrowError(); - let bi = new PbLong(-620756991, 53471156); // @ts-ignore expect(bi.toBigInt()).toBe(229656869973524481n); @@ -152,7 +175,15 @@ describe('PbLong', function () { bi = new PbLong(-1, 2147483647); // @ts-ignore expect(bi.toBigInt()).toBe(9223372036854775807n); + }); + + it('should toNumber()', function () { + let bi = new PbLong(4294967295, 2097151); + expect(bi.toNumber()).toBe(Number.MAX_SAFE_INTEGER); + // signed max value + bi = new PbLong(4294967295, 2097152); + expect(() => bi.toNumber()).toThrowError("cannot convert to safe number"); }); @@ -173,7 +204,7 @@ describe('PbLong', function () { it('should isNegative() with negative native bigint', function () { if (globalThis.BigInt === undefined) - pending('No BigInt support on current platform'); + return expect().nothing(); // @ts-ignore let long = PbLong.from(-9223372036854775808n) @@ -187,7 +218,7 @@ describe('PbLong', function () { it('from(bigint) set expected bits', function () { if (globalThis.BigInt === undefined) - pending('No BigInt support on current platform'); + return expect().nothing(); // @ts-ignore let bi = PbLong.from(9223372036854775807n); @@ -205,14 +236,21 @@ describe('PbLong', function () { expect(ulong.lo).toBe(0); }); -}); + it('should return ZERO for "0", 0, or 0n', function () { + expect(PbLong.from("0").isZero()).toBe(true); + expect(PbLong.from(0).isZero()).toBe(true); + if (globalThis.BigInt !== undefined) + // @ts-ignore + expect(PbLong.from(0n).isZero()).toBe(true); + }); -describe('testing native bigint', function () { +}); +describeWithAndWithoutBISupport('native bigint', function () { it('max uint64 value should survive string conversion', function () { if (globalThis.BigInt === undefined) - pending('No BigInt support on current platform'); + return expect().nothing(); // @ts-ignore let m = 18446744073709551615n; @@ -223,7 +261,7 @@ describe('testing native bigint', function () { it('min int64 value should survive string conversion', function () { if (globalThis.BigInt === undefined) - pending('No BigInt support on current platform'); + return expect().nothing(); // @ts-ignore let m = -9223372036854775808n; @@ -234,7 +272,7 @@ describe('testing native bigint', function () { it('max int64 value should survive string conversion', function () { if (globalThis.BigInt === undefined) - pending('No BigInt support on current platform'); + return expect().nothing(); // @ts-ignore let m = 9223372036854775807n; @@ -246,7 +284,7 @@ describe('testing native bigint', function () { it('max uint64 value should survive DataView round trip', function () { if (globalThis.BigInt === undefined) - pending('No BigInt support on current platform'); + return expect().nothing(); // @ts-ignore let expected = 18446744073709551615n; @@ -268,7 +306,7 @@ describe('testing native bigint', function () { it('max int64 value should survive DataView round trip', function () { if (globalThis.BigInt === undefined) - pending('No BigInt support on current platform'); + return expect().nothing(); // @ts-ignore let expected = 9223372036854775807n; @@ -290,7 +328,7 @@ describe('testing native bigint', function () { it('min int64 value should survive DataView round trip', function () { if (globalThis.BigInt === undefined) - pending('No BigInt support on current platform'); + return expect().nothing(); // @ts-ignore let expected = -9223372036854775808n; @@ -311,3 +349,29 @@ describe('testing native bigint', function () { }); +function describeWithAndWithoutBISupport(description: string, specDefinitions: () => void) { + describe(description, function () { + describe('(with BI support)', function () { + if (globalThis.BigInt === undefined) + return it('cannot be tested', function () { + pending('No BigInt support on current platform'); + }); + specDefinitions(); + }); + describe('(without BI support)', function () { + specDefinitions(); + let BICtor: typeof globalThis.BigInt; + beforeAll(() => { + BICtor = globalThis.BigInt; + // @ts-ignore + globalThis.BigInt = undefined; + detectBi(); + }); + afterAll(() => { + globalThis.BigInt = BICtor; + detectBi(); + }); + }); + }); +} + diff --git a/packages/runtime/src/goog-varint.ts b/packages/runtime/src/goog-varint.ts index 89922b55..3d57be46 100644 --- a/packages/runtime/src/goog-varint.ts +++ b/packages/runtime/src/goog-varint.ts @@ -154,7 +154,7 @@ export function int64fromString(dec: string): [boolean, number, number] { const digit1e6 = Number(dec.slice(begin, end)); highBits *= base; lowBits = lowBits * base + digit1e6; - // Carry bits from lowBits to + // Carry bits from lowBits to highBits if (lowBits >= TWO_PWR_32_DBL) { highBits = highBits + ((lowBits / TWO_PWR_32_DBL) | 0); lowBits = lowBits % TWO_PWR_32_DBL; @@ -177,7 +177,7 @@ export function int64fromString(dec: string): [boolean, number, number] { export function int64toString(bitsLow: number, bitsHigh: number): string { // Skip the expensive conversion if the number is small enough to use the // built-in conversions. - if (bitsHigh <= 0x1FFFFF) { + if ((bitsHigh >>> 0) <= 0x1FFFFF) { return '' + (TWO_PWR_32_DBL * bitsHigh + (bitsLow >>> 0)); } diff --git a/packages/runtime/src/pb-long.ts b/packages/runtime/src/pb-long.ts index 078f702d..3cc95bed 100644 --- a/packages/runtime/src/pb-long.ts +++ b/packages/runtime/src/pb-long.ts @@ -41,14 +41,16 @@ interface BiSupport { C(v: number | string | bigint): bigint; } -function detectBi(): BiSupport | undefined { +let BI: BiSupport | undefined; + +export function detectBi(): void { const dv = new DataView(new ArrayBuffer(8)); const ok = globalThis.BigInt !== undefined && typeof dv.getBigInt64 === "function" && typeof dv.getBigUint64 === "function" && typeof dv.setBigInt64 === "function" && typeof dv.setBigUint64 === "function"; - return ok ? { + BI = ok ? { MIN: BigInt("-9223372036854775808"), MAX: BigInt("9223372036854775807"), UMIN: BigInt("0"), @@ -58,7 +60,7 @@ function detectBi(): BiSupport | undefined { } : undefined; } -const BI = detectBi(); +detectBi(); function assertBi(bi: BiSupport | undefined): asserts bi is BiSupport { if (!bi) throw new Error("BigInt unavailable, see https://github.com/timostamm/protobuf-ts/blob/v1.0.8/MANUAL.md#bigint-support"); @@ -68,7 +70,8 @@ function assertBi(bi: BiSupport | undefined): asserts bi is BiSupport { const RE_DECIMAL_STR = /^-?[0-9]+$/; // constants for binary math -const TWO_PWR_32_DBL = (1 << 16) * (1 << 16); +const TWO_PWR_32_DBL = 0x100000000; +const HALF_2_PWR_32 = 0x080000000; // base class for PbLong and PbULong provides shared code @@ -177,7 +180,7 @@ export class PbULong extends SharedPbLong { throw new Error('string is no integer'); let [minus, lo, hi] = int64fromString(value); if (minus) - throw new Error('signed value'); + throw new Error('signed value for ulong'); return new PbULong(lo, hi); case "number": @@ -247,9 +250,9 @@ export class PbLong extends SharedPbLong { if (!value) return this.ZERO; if (value < BI.MIN) - throw new Error('ulong too small'); + throw new Error('signed long too small'); if (value > BI.MAX) - throw new Error('ulong too large'); + throw new Error('signed long too large'); BI.V.setBigInt64(0, value, true); return new PbLong( BI.V.getInt32(0, true), @@ -266,6 +269,11 @@ export class PbLong extends SharedPbLong { if (!RE_DECIMAL_STR.test(value)) throw new Error('string is no integer'); let [minus, lo, hi] = int64fromString(value); + if (minus) { + if (hi > HALF_2_PWR_32 || (hi == HALF_2_PWR_32 && lo != 0)) + throw new Error('signed long too small'); + } else if (hi >= HALF_2_PWR_32) + throw new Error('signed long too large'); let pbl = new PbLong(lo, hi); return minus ? pbl.negate() : pbl; @@ -285,7 +293,7 @@ export class PbLong extends SharedPbLong { * Do we have a minus sign? */ isNegative(): boolean { - return (this.hi & 0x80000000) !== 0; + return (this.hi & HALF_2_PWR_32) !== 0; } /**