diff --git a/package.json b/package.json index a08c7e0..f2a13f5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "node": ">=18.17" }, "scripts": { - "generate": "PATH=./node_modules/.bin:$PATH buf generate", + "generate": "PATH=./node_modules/.bin:$PATH buf generate && bun run scripts/soften-int64.mjs", "generate:http": "openapi-ts -i gen/openapi/imessage.swagger.json -o packages/http/src/generated/http -c @hey-api/client-fetch", "verify:codegen": "bun run generate && bun run generate:http && git diff --exit-code -- gen packages/core/src/generated packages/http/src/generated", "build": "bun run generate && bun run --cwd packages/advanced-imessage build", diff --git a/packages/core/src/generated/google/protobuf/descriptor.ts b/packages/core/src/generated/google/protobuf/descriptor.ts index 006fe92..d0c1cd9 100644 --- a/packages/core/src/generated/google/protobuf/descriptor.ts +++ b/packages/core/src/generated/google/protobuf/descriptor.ts @@ -7235,14 +7235,7 @@ export type DeepPartial = T extends Builtin ? T : Partial; function longToNumber(int64: { toString(): string }): number { - const num = globalThis.Number(int64.toString()); - if (num > globalThis.Number.MAX_SAFE_INTEGER) { - throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); - } - if (num < globalThis.Number.MIN_SAFE_INTEGER) { - throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); - } - return num; + return globalThis.Number(int64.toString()); } function isSet(value: any): boolean { diff --git a/packages/core/src/generated/google/protobuf/timestamp.ts b/packages/core/src/generated/google/protobuf/timestamp.ts index d6792cd..de8a664 100644 --- a/packages/core/src/generated/google/protobuf/timestamp.ts +++ b/packages/core/src/generated/google/protobuf/timestamp.ts @@ -201,14 +201,7 @@ export type DeepPartial = T extends Builtin ? T : Partial; function longToNumber(int64: { toString(): string }): number { - const num = globalThis.Number(int64.toString()); - if (num > globalThis.Number.MAX_SAFE_INTEGER) { - throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); - } - if (num < globalThis.Number.MIN_SAFE_INTEGER) { - throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); - } - return num; + return globalThis.Number(int64.toString()); } function isSet(value: any): boolean { diff --git a/packages/core/src/generated/photon/imessage/v1/attachment_types.ts b/packages/core/src/generated/photon/imessage/v1/attachment_types.ts index f509ada..1e32c41 100644 --- a/packages/core/src/generated/photon/imessage/v1/attachment_types.ts +++ b/packages/core/src/generated/photon/imessage/v1/attachment_types.ts @@ -646,14 +646,7 @@ export type DeepPartial = T extends Builtin ? T : Partial; function longToNumber(int64: { toString(): string }): number { - const num = globalThis.Number(int64.toString()); - if (num > globalThis.Number.MAX_SAFE_INTEGER) { - throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); - } - if (num < globalThis.Number.MIN_SAFE_INTEGER) { - throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); - } - return num; + return globalThis.Number(int64.toString()); } function isSet(value: any): boolean { diff --git a/packages/core/src/generated/photon/imessage/v1/chat_service.ts b/packages/core/src/generated/photon/imessage/v1/chat_service.ts index 72253de..ef6a755 100644 --- a/packages/core/src/generated/photon/imessage/v1/chat_service.ts +++ b/packages/core/src/generated/photon/imessage/v1/chat_service.ts @@ -1780,14 +1780,7 @@ export type DeepPartial = T extends Builtin ? T : Partial; function longToNumber(int64: { toString(): string }): number { - const num = globalThis.Number(int64.toString()); - if (num > globalThis.Number.MAX_SAFE_INTEGER) { - throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); - } - if (num < globalThis.Number.MIN_SAFE_INTEGER) { - throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); - } - return num; + return globalThis.Number(int64.toString()); } function isSet(value: any): boolean { diff --git a/packages/core/src/generated/photon/imessage/v1/event_service.ts b/packages/core/src/generated/photon/imessage/v1/event_service.ts index f24c2da..7c2789d 100644 --- a/packages/core/src/generated/photon/imessage/v1/event_service.ts +++ b/packages/core/src/generated/photon/imessage/v1/event_service.ts @@ -420,14 +420,7 @@ export type DeepPartial = T extends Builtin ? T : Partial; function longToNumber(int64: { toString(): string }): number { - const num = globalThis.Number(int64.toString()); - if (num > globalThis.Number.MAX_SAFE_INTEGER) { - throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); - } - if (num < globalThis.Number.MIN_SAFE_INTEGER) { - throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); - } - return num; + return globalThis.Number(int64.toString()); } function isSet(value: any): boolean { diff --git a/packages/core/src/generated/photon/imessage/v1/group_service.ts b/packages/core/src/generated/photon/imessage/v1/group_service.ts index 1bf1bc6..bae73ce 100644 --- a/packages/core/src/generated/photon/imessage/v1/group_service.ts +++ b/packages/core/src/generated/photon/imessage/v1/group_service.ts @@ -1588,14 +1588,7 @@ export type DeepPartial = T extends Builtin ? T : Partial; function longToNumber(int64: { toString(): string }): number { - const num = globalThis.Number(int64.toString()); - if (num > globalThis.Number.MAX_SAFE_INTEGER) { - throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); - } - if (num < globalThis.Number.MIN_SAFE_INTEGER) { - throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); - } - return num; + return globalThis.Number(int64.toString()); } function isSet(value: any): boolean { diff --git a/packages/core/src/generated/photon/imessage/v1/location_types.ts b/packages/core/src/generated/photon/imessage/v1/location_types.ts index 1a4ab0d..9dcf911 100644 --- a/packages/core/src/generated/photon/imessage/v1/location_types.ts +++ b/packages/core/src/generated/photon/imessage/v1/location_types.ts @@ -473,14 +473,7 @@ function fromJsonTimestamp(o: any): Date { } function longToNumber(int64: { toString(): string }): number { - const num = globalThis.Number(int64.toString()); - if (num > globalThis.Number.MAX_SAFE_INTEGER) { - throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); - } - if (num < globalThis.Number.MIN_SAFE_INTEGER) { - throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); - } - return num; + return globalThis.Number(int64.toString()); } function isSet(value: any): boolean { diff --git a/packages/core/src/generated/photon/imessage/v1/message_service.ts b/packages/core/src/generated/photon/imessage/v1/message_service.ts index c91f3ee..5fb7c1b 100644 --- a/packages/core/src/generated/photon/imessage/v1/message_service.ts +++ b/packages/core/src/generated/photon/imessage/v1/message_service.ts @@ -4251,14 +4251,7 @@ function fromJsonTimestamp(o: any): Date { } function longToNumber(int64: { toString(): string }): number { - const num = globalThis.Number(int64.toString()); - if (num > globalThis.Number.MAX_SAFE_INTEGER) { - throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); - } - if (num < globalThis.Number.MIN_SAFE_INTEGER) { - throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); - } - return num; + return globalThis.Number(int64.toString()); } function isSet(value: any): boolean { diff --git a/packages/core/src/generated/photon/imessage/v1/poll_service.ts b/packages/core/src/generated/photon/imessage/v1/poll_service.ts index a7d34e5..495bfa6 100644 --- a/packages/core/src/generated/photon/imessage/v1/poll_service.ts +++ b/packages/core/src/generated/photon/imessage/v1/poll_service.ts @@ -996,14 +996,7 @@ export type DeepPartial = T extends Builtin ? T : Partial; function longToNumber(int64: { toString(): string }): number { - const num = globalThis.Number(int64.toString()); - if (num > globalThis.Number.MAX_SAFE_INTEGER) { - throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); - } - if (num < globalThis.Number.MIN_SAFE_INTEGER) { - throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); - } - return num; + return globalThis.Number(int64.toString()); } function isSet(value: any): boolean { diff --git a/packages/core/tests/long-to-number.test.ts b/packages/core/tests/long-to-number.test.ts new file mode 100644 index 0000000..3d92a30 --- /dev/null +++ b/packages/core/tests/long-to-number.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "bun:test"; +import { Timestamp } from "../src/generated/google/protobuf/timestamp.ts"; +import { SubscribePollEventsResponse } from "../src/generated/photon/imessage/v1/poll_service.ts"; + +// Far past Number.MAX_SAFE_INTEGER (2^53 - 1). ts-proto types these fields as +// `number`, but the wire format can carry the full signed/unsigned 64-bit +// range, so the cast is required to construct a regression fixture for #23. +const OVERSIZED_UINT64 = Number("18446744073709551615"); // 2^64 - 1 (max uint64) +const OVERSIZED_INT64 = Number("9223372036854775807"); // 2^63 - 1 (max int64) + +describe("longToNumber overflow handling (issue #23)", () => { + it("decodes a top-level oversized uint64 sequence without throwing", () => { + const bytes = SubscribePollEventsResponse.encode( + SubscribePollEventsResponse.create({ + sequence: OVERSIZED_UINT64, + pollChanged: { + chatGuid: "iMessage;-;+15551234567", + pollMessageGuid: "poll-guid", + occurredAt: new Date("2026-07-16T00:00:00Z"), + isFromMe: false, + created: { title: "Lunch?", options: [] }, + }, + }) + ).finish(); + + const decoded = SubscribePollEventsResponse.decode(bytes); + + expect(decoded.pollChanged?.created?.title).toBe("Lunch?"); + expect(typeof decoded.sequence).toBe("number"); + expect(Number.isFinite(decoded.sequence)).toBe(true); + }); + + it("decodes a nested Timestamp.seconds oversized past MAX_SAFE_INTEGER without throwing", () => { + const bytes = Timestamp.encode( + Timestamp.create({ seconds: OVERSIZED_INT64, nanos: 0 }) + ).finish(); + + expect(() => Timestamp.decode(bytes)).not.toThrow(); + + const decoded = Timestamp.decode(bytes); + expect(typeof decoded.seconds).toBe("number"); + expect(Number.isFinite(decoded.seconds)).toBe(true); + }); +}); diff --git a/scripts/soften-int64.mjs b/scripts/soften-int64.mjs new file mode 100644 index 0000000..a39ca57 --- /dev/null +++ b/scripts/soften-int64.mjs @@ -0,0 +1,59 @@ +#!/usr/bin/env node +import { readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +const ROOT = "packages/core/src/generated"; + +function walk(dir) { + const files = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...walk(path)); + } else if (entry.name.endsWith(".ts")) { + files.push(path); + } + } + return files; +} + +const THROWING_BODY = `function longToNumber(int64: { toString(): string }): number { + const num = globalThis.Number(int64.toString()); + if (num > globalThis.Number.MAX_SAFE_INTEGER) { + throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); + } + if (num < globalThis.Number.MIN_SAFE_INTEGER) { + throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); + } + return num; +}`; + +const SOFTENED_BODY = `function longToNumber(int64: { toString(): string }): number { + return globalThis.Number(int64.toString()); +}`; + +const files = walk(ROOT); +let patched = 0; +let alreadySoft = 0; + +for (const file of files) { + const contents = readFileSync(file, "utf8"); + if (contents.includes(THROWING_BODY)) { + writeFileSync(file, contents.replace(THROWING_BODY, SOFTENED_BODY)); + patched++; + } else if (contents.includes("function longToNumber")) { + alreadySoft++; + } +} + +if (patched === 0 && alreadySoft === 0) { + throw new Error( + "soften-int64: found no longToNumber helper under " + + ROOT + + " — ts-proto's emitted shape may have changed; update THROWING_BODY." + ); +} + +console.log( + `soften-int64: patched ${patched} file(s), ${alreadySoft} already softened` +);