export type PatchLineKind = "hunk" | "added" | "deleted" | "context" | "note"; export interface ParsedPatchLine { kind: PatchLineKind; text: string; oldLine?: number; newLine?: number; } export interface ParsedPatch { valid: boolean; lines: ParsedPatchLine[]; } interface HunkState { oldLine: number; newLine: number; oldRemaining: number; newRemaining: number; } const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/; function invalidPatch(): ParsedPatch { return { valid: false, lines: [] }; } export function parseUnifiedPatch( patch: string, maxBytes: number, maxLines = 20_000, ): ParsedPatch { if (Buffer.byteLength(patch, "utf8") > maxBytes) return invalidPatch(); let physicalLines = 1; for (const character of patch) { if (character !== "\n") continue; physicalLines += 1; if (physicalLines > maxLines) return invalidPatch(); } const parsedLines: ParsedPatchLine[] = []; let hunk: HunkState | undefined; for (const line of patch.split("\n")) { const header = HUNK_HEADER.exec(line); if (header) { if (hunk && (hunk.oldRemaining !== 0 || hunk.newRemaining !== 0)) { return invalidPatch(); } const oldStart = Number.parseInt(header[1]!, 10); const oldCount = header[2] === undefined ? 1 : Number.parseInt(header[2], 10); const newStart = Number.parseInt(header[3]!, 10); const newCount = header[4] === undefined ? 1 : Number.parseInt(header[4], 10); if (![oldStart, oldCount, newStart, newCount].every(Number.isSafeInteger)) { return invalidPatch(); } hunk = { oldLine: oldStart, newLine: newStart, oldRemaining: oldCount, newRemaining: newCount, }; parsedLines.push({ kind: "hunk", text: line }); continue; } if (!hunk) continue; if (line === "\\ No newline at end of file") { parsedLines.push({ kind: "note", text: line }); continue; } if (hunk.oldRemaining === 0 && hunk.newRemaining === 0) { hunk = undefined; continue; } const marker = line[0]; if (marker === " ") { if (hunk.oldRemaining <= 0 || hunk.newRemaining <= 0) return invalidPatch(); parsedLines.push({ kind: "context", text: line, oldLine: hunk.oldLine, newLine: hunk.newLine, }); hunk.oldLine += 1; hunk.newLine += 1; hunk.oldRemaining -= 1; hunk.newRemaining -= 1; continue; } if (marker === "-") { if (hunk.oldRemaining <= 0) return invalidPatch(); parsedLines.push({ kind: "deleted", text: line, oldLine: hunk.oldLine }); hunk.oldLine += 1; hunk.oldRemaining -= 1; continue; } if (marker === "+") { if (hunk.newRemaining <= 0) return invalidPatch(); parsedLines.push({ kind: "added", text: line, newLine: hunk.newLine }); hunk.newLine += 1; hunk.newRemaining -= 1; continue; } return invalidPatch(); } if (hunk && (hunk.oldRemaining !== 0 || hunk.newRemaining !== 0)) { return invalidPatch(); } return { valid: true, lines: parsedLines }; } export function sanitizeTerminalText(text: string, tabSize = 4): string { let result = ""; let column = 0; for (const character of text) { const code = character.codePointAt(0)!; let replacement: string; if (code === 0x09) { const spaces = tabSize - (column % tabSize); replacement = " ".repeat(spaces); } else if (code === 0x0a) { replacement = "\\n"; } else if (code === 0x0d) { replacement = "\\r"; } else if (code <= 0x1f || code === 0x7f) { replacement = `\\x${code.toString(16).padStart(2, "0")}`; } else if (code >= 0x80 && code <= 0x9f) { replacement = `\\u${code.toString(16).padStart(4, "0")}`; } else { replacement = character; } result += replacement; column += replacement.length; } return result; }