Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/lib/repo-analytics-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
32 changes: 32 additions & 0 deletions test/repo-analytics-utils-sanitize.test.ts
Original file line number Diff line number Diff line change
@@ -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",
});
});
});
Loading