diff --git a/src/lib/repo-analytics-utils.ts b/src/lib/repo-analytics-utils.ts index 9c9c87d44..51c22924e 100644 --- a/src/lib/repo-analytics-utils.ts +++ b/src/lib/repo-analytics-utils.ts @@ -9,15 +9,21 @@ export interface ParsedRepo { /** * Validates and parses a raw "owner/repo" string. + * Strips a single leading slash and collapses consecutive slashes before + * validating, so inputs like "/owner/repo" or "owner//repo" (e.g. from + * referrer headers or query params) normalise to "owner/repo" instead of + * silently failing to match. * Returns the split components on success, or null if the value is invalid. */ export function parseRepoParam(raw: string): ParsedRepo | null { const trimmed = raw.trim(); - const match = REPO_IDENTIFIER_RE.exec(trimmed); + const withoutLeadingSlash = trimmed.startsWith("/") + ? trimmed.slice(1) + : trimmed; + const normalised = withoutLeadingSlash.replace(/\/{2,}/g, "/"); + const match = REPO_IDENTIFIER_RE.exec(normalised); if (!match) return null; - const [, owner, repo] = match; if (repo === "." || repo === "..") return null; - return { owner, repo }; } diff --git a/test/repo-analytics-utils-sanitize.test.ts b/test/repo-analytics-utils-sanitize.test.ts new file mode 100644 index 000000000..4f2d8e852 --- /dev/null +++ b/test/repo-analytics-utils-sanitize.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from "vitest"; +import { parseRepoParam } from "@/lib/repo-analytics-utils"; + +describe("parseRepoParam — input sanitisation", () => { + it("strips a single leading slash before validating", () => { + expect(parseRepoParam("/owner/repo")).toEqual({ + owner: "owner", + repo: "repo", + }); + }); + + it("collapses consecutive slashes before validating", () => { + expect(parseRepoParam("owner//repo")).toEqual({ + owner: "owner", + repo: "repo", + }); + }); + + it("handles mixed leading slash, double slashes, and surrounding spaces", () => { + expect(parseRepoParam(" /owner//repo ")).toEqual({ + owner: "owner", + repo: "repo", + }); + }); + + it("leaves normal input unchanged", () => { + expect(parseRepoParam("facebook/react")).toEqual({ + owner: "facebook", + repo: "react", + }); + }); +});