From 8db66b416bb8810c90ebedcdec21ef310a986c21 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 17 Jun 2026 00:51:08 +0200 Subject: [PATCH] fix(release): reject unsafe Sparkle build floors --- scripts/sparkle-build.ts | 29 ++++++++++++++++++++++------- test/appcast.test.ts | 5 +++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/scripts/sparkle-build.ts b/scripts/sparkle-build.ts index 41b13c227c0..b76b71e6e13 100644 --- a/scripts/sparkle-build.ts +++ b/scripts/sparkle-build.ts @@ -54,13 +54,13 @@ export function sparkleBuildFloorsFromShortVersion( return null; } - const year = Number.parseInt(match[1], 10); - const month = Number.parseInt(match[2], 10); - const patch = Number.parseInt(match[3], 10); + const year = Number(match[1]); + const month = Number(match[2]); + const patch = Number(match[3]); if ( - !Number.isInteger(year) || - !Number.isInteger(month) || - !Number.isInteger(patch) || + !Number.isSafeInteger(year) || + !Number.isSafeInteger(month) || + !Number.isSafeInteger(patch) || month < 1 || month > 12 || patch < 1 @@ -87,10 +87,14 @@ export function sparkleBuildFloorsFromShortVersion( // Keep old appcast entries byte-stable, then switch to YYMMPPPPLL so // monthly patches beyond 31 stay monotonic without pretending to be dates. const releaseKey = monthlyPatchReleaseKey(year, month, patch); + const laneFloor = releaseKey + lane; + if (!isSafeSparkleFloor(releaseKey) || !isSafeSparkleFloor(laneFloor)) { + return null; + } return { releaseKey, legacyFloor: releaseKey, - laneFloor: releaseKey + lane, + laneFloor, lane, }; } @@ -98,6 +102,13 @@ export function sparkleBuildFloorsFromShortVersion( const releaseKey = legacyDateReleaseKey(year, month, patch); const legacyFloor = Number(`${releaseKey}0`); const laneFloor = Number(`${releaseKey}${String(lane).padStart(2, "0")}`); + if ( + !isSafeSparkleFloor(releaseKey) || + !isSafeSparkleFloor(legacyFloor) || + !isSafeSparkleFloor(laneFloor) + ) { + return null; + } return { releaseKey, legacyFloor, laneFloor, lane }; } @@ -105,6 +116,10 @@ export function canonicalSparkleBuildFromVersion(shortVersion: string): number | return sparkleBuildFloorsFromShortVersion(shortVersion)?.laneFloor ?? null; } +function isSafeSparkleFloor(value: number): boolean { + return Number.isSafeInteger(value) && value > 0; +} + function runCli(args: string[]): number { const [command, version] = args; if (command !== "canonical-build" || !version) { diff --git a/test/appcast.test.ts b/test/appcast.test.ts index 4251cb28999..0a1cd29aa8c 100644 --- a/test/appcast.test.ts +++ b/test/appcast.test.ts @@ -26,6 +26,11 @@ describe("canonicalSparkleBuildFromVersion", () => { expect(canonicalSparkleBuildFromVersion("2026.6.5-beta.0")).toBeNull(); expect(canonicalSparkleBuildFromVersion("2026.6.5-beta.9007199254740993")).toBeNull(); }); + + it("rejects unsafe numeric release parts and build floors", () => { + expect(canonicalSparkleBuildFromVersion("2026.6.9007199254740993")).toBeNull(); + expect(canonicalSparkleBuildFromVersion("2026.6.90071992547410")).toBeNull(); + }); }); function parseItems(appcast: string): AppcastItem[] {