-
Notifications
You must be signed in to change notification settings - Fork 3
fix:soften longToNumber overflow to prevent stream teardown #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 = "18446744073709551615" as unknown as number; // 2^64 - 1 (max uint64) | ||
| const OVERSIZED_INT64 = "9223372036854775807" as unknown as number; // 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); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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++; | ||
| } | ||
|
IshaanXCoder marked this conversation as resolved.
|
||
| } | ||
|
|
||
| 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` | ||
| ); | ||
Uh oh!
There was an error while loading. Please reload this page.