Skip to content
Closed
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
2 changes: 2 additions & 0 deletions packages/loopover-miner/lib/contribution-profile-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
resolveLocalStoreDbPath,
} from "./local-store.js";
import { applySchemaMigrations } from "./schema-version.js";
import { isValidRepoSegment } from "./repo-clone.js";
import {
CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC,
purgeStoreByRepo,
Expand Down Expand Up @@ -58,6 +59,7 @@ function normalizeRepoFullName(repoFullName: unknown): string {
const [owner, repo, extra] = repoFullName.trim().split("/");
if (!owner || !repo || extra !== undefined)
throw new Error("invalid_repo_full_name");
if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) throw new Error("invalid_repo_full_name");
return `${owner}/${repo}`;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/loopover-miner/lib/prediction-ledger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { DatabaseSync } from "node:sqlite";
import { normalizeLocalStoreDbPath, openLocalStoreAdapter, resolveLocalStoreDbPath } from "./local-store.js";
import { applySchemaMigrations } from "./schema-version.js";
import { isValidRepoSegment } from "./repo-clone.js";
import {
PREDICTION_LEDGER_PURGE_SPEC,
PREDICTION_LEDGER_RETENTION_SPEC,
Expand Down Expand Up @@ -87,6 +88,7 @@ function normalizeRepoFullName(repoFullName: string): string {
if (typeof repoFullName !== "string") throw new Error("invalid_repo_full_name");
const [owner, repo, extra] = repoFullName.trim().split("/");
if (!owner || !repo || extra !== undefined) throw new Error("invalid_repo_full_name");
if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) throw new Error("invalid_repo_full_name");
return `${owner}/${repo}`;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/loopover-miner/lib/replay-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { join } from "node:path";
import { removeWorktree } from "@loopover/engine";
import type { WorktreeExecFn, WorktreeRemoveResult } from "@loopover/engine";
import { openLocalStoreAdapter, resolveLocalStoreDbPath, normalizeLocalStoreDbPath } from "./local-store.js";
import { isValidRepoSegment } from "./repo-clone.js";

// Freeze/snapshot mechanism for historical replay targets (#3010). Given a repo and a commit SHA T, exports:
// (a) the full working tree checked out AT T via a DETACHED git worktree -- the same isolation primitive
Expand Down Expand Up @@ -72,6 +73,7 @@ function normalizeRepoFullName(repoFullName: string): string {
if (typeof repoFullName !== "string") throw new Error("invalid_repo_full_name");
const [owner, repo, extra] = repoFullName.trim().split("/");
if (!owner || !repo || extra !== undefined) throw new Error("invalid_repo_full_name");
if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) throw new Error("invalid_repo_full_name");
return `${owner}/${repo}`;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/loopover-miner/lib/run-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DEFAULT_FORGE_CONFIG } from "./forge-config.js";
import { normalizeLocalStoreDbPath, openLocalStoreAdapter, resolveLocalStoreDbPath } from "./local-store.js";
import { applySchemaMigrations } from "./schema-version.js";
import { RUN_STATE_PURGE_SPEC, purgeStoreByRepo } from "./store-maintenance.js";
import { isValidRepoSegment } from "./repo-clone.js";

export type RunState = "idle" | "discovering" | "planning" | "preparing";

Expand Down Expand Up @@ -57,6 +58,7 @@ function normalizeRepoFullName(repoFullName: string): string {
const trimmed = repoFullName.trim();
const [owner, repo, extra] = trimmed.split("/");
if (!owner || !repo || extra !== undefined) throw new Error("invalid_repo_full_name");
if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) throw new Error("invalid_repo_full_name");
return `${owner}/${repo}`;
}

Expand Down
4 changes: 4 additions & 0 deletions test/unit/miner-contribution-profile-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ describe("contribution-profile cache store (#6797)", () => {
expect(() => store.put({ repoFullName: 42 } as never)).toThrow(
"invalid_repo_full_name",
);
// #7795: a `.`/`..`/control-char owner or repo segment must be rejected, matching the sibling stores.
expect(() => store.get("../etc")).toThrow("invalid_repo_full_name");
expect(() => store.get("owner/..")).toThrow("invalid_repo_full_name");
expect(() => store.get("owner/re\tpo")).toThrow("invalid_repo_full_name");
});

it("exposes module-level get/put helpers backed by the default DB path", () => {
Expand Down
4 changes: 4 additions & 0 deletions test/unit/miner-prediction-ledger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ describe("miner prediction ledger (#4263)", () => {
it("rejects invalid inputs field by field", () => {
const ledger = tempLedger();
expect(() => ledger.appendPrediction({ ...VALID, repoFullName: "no-slash" })).toThrow(/invalid_repo_full_name/);
// #7795: `.`/`..`/control-char segments must be rejected, matching the sibling stores.
expect(() => ledger.appendPrediction({ ...VALID, repoFullName: "../etc" })).toThrow(/invalid_repo_full_name/);
expect(() => ledger.appendPrediction({ ...VALID, repoFullName: "owner/.." })).toThrow(/invalid_repo_full_name/);
expect(() => ledger.appendPrediction({ ...VALID, repoFullName: "own\ter/repo" })).toThrow(/invalid_repo_full_name/);
expect(() => ledger.appendPrediction({ ...VALID, targetId: 0 })).toThrow(/invalid_target_id/);
expect(() => ledger.appendPrediction({ ...VALID, conclusion: "" })).toThrow(/invalid_conclusion/);
expect(() => ledger.appendPrediction({ ...VALID, engineVersion: "" })).toThrow(/invalid_engine_version/);
Expand Down
7 changes: 7 additions & 0 deletions test/unit/miner-replay-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ describe("exportReplaySnapshot (#3010)", () => {
expect(snapshot.readme).toEqual({ filename: "README.md", content: "# hello\n" });
});

it("rejects a `.`/`..`/control-char repo segment before touching the store (#7795)", () => {
const store = tempStore();
expect(() => store.getSnapshot("../etc", "abc123")).toThrow("invalid_repo_full_name");
expect(() => store.getSnapshot("owner/..", "abc123")).toThrow("invalid_repo_full_name");
expect(() => store.getSnapshot("ow\tner/repo", "abc123")).toThrow("invalid_repo_full_name");
});

it("returns the cached snapshot on a repeat export of the same (repo, commit) pair, without calling git again", async () => {
const store = tempStore();
const first = scriptedExec(happyPathScripts());
Expand Down
4 changes: 4 additions & 0 deletions test/unit/miner-run-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ describe("loopover-miner run-state store (#2289)", () => {
try {
expect(() => store.getRunState("not-a-full-name")).toThrow("invalid_repo_full_name");
expect(() => store.setRunState("owner/repo/extra", "idle")).toThrow("invalid_repo_full_name");
// #7795: `.`/`..`/control-char segments must be rejected, matching the sibling stores.
expect(() => store.setRunState("../etc", "idle")).toThrow("invalid_repo_full_name");
expect(() => store.setRunState("owner/..", "idle")).toThrow("invalid_repo_full_name");
expect(() => store.getRunState("ow\tner/repo")).toThrow("invalid_repo_full_name");
expect(() => store.setRunState("owner/repo", "blocked" as never)).toThrow("invalid_run_state");
expect(store.getRunState("owner/repo")).toBeNull();
} finally {
Expand Down
Loading