diff --git a/packages/protons-runtime/package.json b/packages/protons-runtime/package.json index a3a5cd9..ab669f8 100644 --- a/packages/protons-runtime/package.json +++ b/packages/protons-runtime/package.json @@ -128,6 +128,13 @@ "dep-check": "aegir dep-check", "doc-check": "aegir doc-check", "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main", "release": "aegir release" }, "dependencies": { diff --git a/packages/protons-runtime/src/utils/longbits.ts b/packages/protons-runtime/src/utils/longbits.ts index 31b120f..0682618 100644 --- a/packages/protons-runtime/src/utils/longbits.ts +++ b/packages/protons-runtime/src/utils/longbits.ts @@ -122,26 +122,10 @@ export class LongBits { return this.fromNumber(Number(value)) } - const negative = value < 0n + // two's complement in one step, so a zero low word carries into the high word + const bits = BigInt.asUintN(64, value) - if (negative) { - value = -value - } - - let hi = value >> 32n - let lo = value - (hi << 32n) - - if (negative) { - hi = ~hi | 0n - lo = ~lo | 0n - - if (++lo > TWO_32) { - lo = 0n - if (++hi > TWO_32) { hi = 0n } - } - } - - return new LongBits(Number(lo), Number(hi)) + return new LongBits(Number(bits & 0xffffffffn), Number(bits >> 32n)) } /** @@ -185,5 +169,3 @@ const zero = new LongBits(0, 0) zero.toBigInt = function () { return 0n } zero.zzEncode = zero.zzDecode = function () { return this } zero.length = function () { return 1 } - -const TWO_32 = 4294967296n diff --git a/packages/protons-runtime/test/longbits.spec.ts b/packages/protons-runtime/test/longbits.spec.ts new file mode 100644 index 0000000..882bd5f --- /dev/null +++ b/packages/protons-runtime/test/longbits.spec.ts @@ -0,0 +1,51 @@ +import { expect } from 'aegir/chai' +import { reader, writer } from '../src/index.ts' + +// negative values below -(2^53 - 1) whose low 32 bits are zero used to lose +// the two's-complement carry into the high word and encode as value - 2^32 +describe('negative 64-bit values', () => { + const cases: Record<'int64' | 'sint64' | 'sfixed64', Array<[bigint, number[]]>> = { + int64: [ + [-(2n ** 53n), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xf0, 0xff, 0x01]], + [-(2n ** 53n + 2n ** 32n), [0x80, 0x80, 0x80, 0x80, 0xf0, 0xff, 0xff, 0xef, 0xff, 0x01]], + [-(2n ** 60n), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xf0, 0x01]], + [-(2n ** 63n - 2n ** 32n), [0x80, 0x80, 0x80, 0x80, 0x90, 0x80, 0x80, 0x80, 0x80, 0x01]], + [-(2n ** 63n), [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01]], + [-(2n ** 53n + 1n), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xff, 0x01]] + ], + sint64: [ + [-(2n ** 53n), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f]], + [-(2n ** 53n + 2n ** 32n), [0xff, 0xff, 0xff, 0xff, 0x9f, 0x80, 0x80, 0x20]], + [-(2n ** 60n), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f]], + [-(2n ** 63n - 2n ** 32n), [0xff, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff, 0xff, 0xff, 0x01]], + [-(2n ** 63n), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]], + [-(2n ** 53n + 1n), [0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x20]] + ], + sfixed64: [ + [-(2n ** 53n), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xff]], + [-(2n ** 53n + 2n ** 32n), [0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xdf, 0xff]], + [-(2n ** 60n), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0]], + [-(2n ** 63n - 2n ** 32n), [0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80]], + [-(2n ** 63n), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80]], + [-(2n ** 53n + 1n), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xff]] + ] + } + + for (const method of ['int64', 'sint64', 'sfixed64'] as const) { + for (const [value, bytes] of cases[method]) { + it(`should encode ${method} ${value}`, () => { + const w = writer() + w[method](value) + + expect(w.finish()).to.equalBytes(bytes) + }) + + it(`should round trip ${method} ${value}`, () => { + const w = writer() + w[method](value) + + expect(reader(w.finish())[method]()).to.equal(value) + }) + } + } +})