const maximumDiffBytes = 500_000; const maximumFindings = 100; export interface DiffAnchor { path: string; side: "old" | "new"; line: number; } export interface UnifiedDiffLine { text: string; anchors: DiffAnchor[]; } export interface ParsedUnifiedDiff { lines: UnifiedDiffLine[]; anchors: DiffAnchor[]; } export interface StructuredDiffFinding extends DiffAnchor { body: string; } export interface PullReviewCommentInput { path: string; body: string; old_position: number; new_position: number; } interface HunkState { path: string; oldLine: number; newLine: number; oldEnd: number; newEnd: number; } export function parseUnifiedDiff(diff: string): ParsedUnifiedDiff { if (Buffer.byteLength(diff) > maximumDiffBytes) throw new Error(`Unified diff exceeds ${maximumDiffBytes} bytes`); const lines: UnifiedDiffLine[] = []; const anchors: DiffAnchor[] = []; const keys = new Set(); let oldPath: string | undefined; let path: string | undefined; let hunk: HunkState | undefined; for (const text of diff.split("\n")) { const lineAnchors: DiffAnchor[] = []; if (hunk && text === "\\ No newline at end of file") { lines.push({ text, anchors: lineAnchors }); continue; } if (hunk && /^[- +]/.test(text)) { consumeHunkLine(hunk, text, lineAnchors); for (const anchor of lineAnchors) { const key = anchorKey(anchor); if (keys.has(key)) throw new Error(`Unified diff repeats anchor ${key}`); keys.add(key); anchors.push(anchor); } lines.push({ text, anchors: lineAnchors }); continue; } finishHunk(hunk); hunk = undefined; if (text.startsWith("diff --git ")) { oldPath = undefined; path = undefined; } else if (text.startsWith("--- ")) { oldPath = parseHeaderPath(text.slice(4), "a/"); } else if (text.startsWith("+++ ")) { const newPath = parseHeaderPath(text.slice(4), "b/"); path = newPath === "/dev/null" ? oldPath : newPath; if (!path || path === "/dev/null") throw new Error("Unified diff has no usable file path"); } else if (text.startsWith("@@")) { if (!path) throw new Error("Unified diff hunk has no file path"); hunk = parseHunk(text, path); } lines.push({ text, anchors: lineAnchors }); } finishHunk(hunk); return { lines, anchors }; } export function renderUnifiedDiff(value: string | ParsedUnifiedDiff): string { const parsed = typeof value === "string" ? parseUnifiedDiff(value) : value; const rendered = parsed.lines .map((line) => { if (!line.anchors.length) return line.text; const refs = line.anchors.map(renderDiffAnchor).join(" "); return `${refs} ${line.text}`; }) .join("\n"); if (Buffer.byteLength(rendered) > maximumDiffBytes * 2) throw new Error("Rendered unified diff exceeds the output limit"); return rendered; } export function renderDiffAnchor(anchor: DiffAnchor): string { return `[${anchor.side}:${anchor.line}:${JSON.stringify(anchor.path)}]`; } export function isValidDiffAnchor( parsed: ParsedUnifiedDiff, anchor: DiffAnchor, ): boolean { const expected = anchorKey(anchor); return parsed.anchors.some( (candidate) => anchorKey(candidate) === expected, ); } export function validateStructuredFindings( value: unknown, diff: string | ParsedUnifiedDiff, ): StructuredDiffFinding[] { if (!Array.isArray(value) || value.length > maximumFindings) throw new Error("Review findings must be a bounded array"); const parsed = typeof diff === "string" ? parseUnifiedDiff(diff) : diff; let bodyBytes = 0; return value.map((item, index) => { if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error(`Review finding ${index} must be an object`); const finding = item as Partial; if ( typeof finding.path !== "string" || !safePath(finding.path) || (finding.side !== "old" && finding.side !== "new") || !Number.isSafeInteger(finding.line) || Number(finding.line) <= 0 ) { throw new Error(`Review finding ${index} has a malformed anchor`); } const anchor = { path: finding.path, side: finding.side, line: Number(finding.line), }; if (!isValidDiffAnchor(parsed, anchor)) throw new Error( `Review finding ${index} is not anchored in the diff`, ); if (typeof finding.body !== "string" || !finding.body.trim()) throw new Error(`Review finding ${index} has no body`); const body = finding.body.trim(); bodyBytes += Buffer.byteLength(body); if (body.length > 10_000 || bodyBytes > maximumDiffBytes) throw new Error("Review findings exceed the output limit"); return { ...anchor, body }; }); } export function renderPullReviewComments( findings: StructuredDiffFinding[], ): PullReviewCommentInput[] { return findings.map((finding) => ({ path: finding.path, body: finding.body, old_position: finding.side === "old" ? finding.line : 0, new_position: finding.side === "new" ? finding.line : 0, })); } function parseHunk(text: string, path: string): HunkState { const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(text); if (!match) throw new Error(`Malformed unified diff hunk: ${text}`); const oldLine = Number(match[1]); const oldCount = Number(match[2] ?? 1); const newLine = Number(match[3]); const newCount = Number(match[4] ?? 1); if ( ![oldLine, oldCount, newLine, newCount].every(Number.isSafeInteger) || (oldCount > 0 && oldLine < 1) || (newCount > 0 && newLine < 1) ) throw new Error(`Malformed unified diff hunk: ${text}`); return { path, oldLine, newLine, oldEnd: oldLine + oldCount, newEnd: newLine + newCount, }; } function consumeHunkLine( hunk: HunkState, text: string, anchors: DiffAnchor[], ): void { const prefix = text[0]; if (prefix === " " || prefix === "-") { if (hunk.oldLine >= hunk.oldEnd) throw new Error("Unified diff exceeds its old-line hunk range"); anchors.push({ path: hunk.path, side: "old", line: hunk.oldLine++ }); } if (prefix === " " || prefix === "+") { if (hunk.newLine >= hunk.newEnd) throw new Error("Unified diff exceeds its new-line hunk range"); anchors.push({ path: hunk.path, side: "new", line: hunk.newLine++ }); } } function finishHunk(hunk: HunkState | undefined): void { if (hunk && (hunk.oldLine !== hunk.oldEnd || hunk.newLine !== hunk.newEnd)) throw new Error( "Unified diff hunk ended before its declared line counts", ); } function parseHeaderPath(value: string, prefix: string): string { const decoded = value.startsWith('"') ? decodeQuotedPath(value) : value; if (decoded === "/dev/null") return decoded; const path = decoded.startsWith(prefix) ? decoded.slice(prefix.length) : decoded; if (!safePath(path)) throw new Error("Unified diff contains an unsafe path"); return path; } function decodeQuotedPath(value: string): string { try { return JSON.parse(value) as string; } catch { throw new Error("Unified diff contains a malformed quoted path"); } } function safePath(path: string): boolean { return Boolean( path && path.length <= 1_000 && !path.startsWith("/") && !path.includes("\0") && !path.includes("\n") && !path.split("/").includes(".."), ); } function anchorKey(anchor: DiffAnchor): string { return JSON.stringify([anchor.path, anchor.side, anchor.line]); }