diff --git a/src/lib/share.test.ts b/src/lib/share.test.ts index 3937dd3..ba5f54c 100644 --- a/src/lib/share.test.ts +++ b/src/lib/share.test.ts @@ -75,6 +75,30 @@ describe("mapStateToSearch / parseMapState round-trip", () => { expect(bogus.lng).toBeNull(); }); + it("rejects out-of-range viewport coordinates and zoom", () => { + expect(parseMapState("?lat=90&lng=180&zoom=20")).toMatchObject({ + lat: 90, + lng: 180, + zoom: 20, + }); + + expect(parseMapState("?lat=91&lng=180&zoom=20")).toMatchObject({ + lat: null, + lng: null, + zoom: null, + }); + expect(parseMapState("?lat=-90&lng=-181&zoom=20")).toMatchObject({ + lat: null, + lng: null, + zoom: null, + }); + expect(parseMapState("?lat=0&lng=0&zoom=21")).toMatchObject({ + lat: 0, + lng: 0, + zoom: null, + }); + }); + it("omits the zoom when it is null", () => { const search = mapStateToSearch({ ...emptyState, lat: 19.076, lng: 72.8777 }); expect(search).toBe("?lat=19.076&lng=72.8777"); diff --git a/src/lib/share.ts b/src/lib/share.ts index 98ac1d3..0ffa268 100644 --- a/src/lib/share.ts +++ b/src/lib/share.ts @@ -21,19 +21,22 @@ function parseList(raw: string | null, allowed: Set): string[] { .filter((value) => allowed.has(value)); } -function parseNumber(raw: string | null): number | null { +function parseNumber(raw: string | null, min?: number, max?: number): number | null { if (raw === null || raw.trim() === "") return null; const value = Number(raw); - return Number.isFinite(value) ? value : null; + if (!Number.isFinite(value)) return null; + if (min !== undefined && value < min) return null; + if (max !== undefined && value > max) return null; + return value; } /** Read filter, focused-pin, and viewport state out of a URL query string. */ export function parseMapState(search: string): MapShareState { const params = new URLSearchParams(search); const city = params.get("city")?.trim() || null; - const lat = parseNumber(params.get("lat")); - const lng = parseNumber(params.get("lng")); - const zoom = parseNumber(params.get("zoom")); + const lat = parseNumber(params.get("lat"), -90, 90); + const lng = parseNumber(params.get("lng"), -180, 180); + const zoom = parseNumber(params.get("zoom"), 0, 20); // A viewport only makes sense with both coordinates; otherwise ignore all three. const hasViewport = lat !== null && lng !== null; return {