export function isUnsafeRegex(pattern: string): boolean { const groups = findQuantifiedGroups(pattern); return groups.some((group) => containsQuantifier(group) || containsOverlappingAlternation(group)); } function containsOverlappingAlternation(pattern: string): boolean { const branches = splitTopLevelAlternation(stripGroupPrefix(pattern)); if (branches.length < 2) return false; for (let i = 0; i < branches.length; i++) { const current = branches[i]!; if (current === "") continue; for (let j = i + 1; j < branches.length; j++) { const next = branches[j]!; if (next === "") continue; if (current === next || current.startsWith(next) || next.startsWith(current)) return true; } } return false; } function containsQuantifier(pattern: string): boolean { const input = stripGroupPrefix(pattern); let inCharClass = false; for (let i = 0; i < input.length; i++) { const char = input[i]!; if (isEscaped(input, i)) continue; if (char === "[") { inCharClass = true; continue; } if (char === "]") { inCharClass = false; continue; } if (inCharClass) continue; if (char === "*" || char === "+" || char === "?") return true; if (char === "{" && readQuantifierBody(input, i) !== null) return true; } return false; } function findQuantifiedGroups(pattern: string): string[] { const groups: string[] = []; const stack: number[] = []; let inCharClass = false; for (let i = 0; i < pattern.length; i++) { const char = pattern[i]!; if (isEscaped(pattern, i)) continue; if (char === "[") { inCharClass = true; continue; } if (char === "]") { inCharClass = false; continue; } if (inCharClass) continue; if (char === "(") { stack.push(i); continue; } if (char === ")") { const start = stack.pop(); if (start === undefined) continue; if (hasRepeatingQuantifierAt(pattern, i + 1)) { groups.push(pattern.slice(start + 1, i)); } } } return groups; } function hasRepeatingQuantifierAt(pattern: string, index: number): boolean { const char = pattern[index]; if (char === "*" || char === "+") return true; if (char !== "{") return false; const body = readQuantifierBody(pattern, index); if (body === null) return false; const parts = body.split(","); if (parts.length === 1) return Number(parts[0]) > 1; if (parts[1] === "") return true; return Number(parts[1]) > 1; } function isEscaped(pattern: string, index: number): boolean { let slashCount = 0; for (let i = index - 1; i >= 0 && pattern[i] === "\\"; i--) { slashCount++; } return slashCount % 2 === 1; } function readQuantifierBody(pattern: string, index: number): null | string { const end = pattern.indexOf("}", index + 1); if (end === -1) return null; const body = pattern.slice(index + 1, end); return /^\d+(?:,\d*)?$/.test(body) ? body : null; } function splitTopLevelAlternation(pattern: string): string[] { const branches: string[] = []; let start = 0; let depth = 0; let inCharClass = false; for (let i = 0; i < pattern.length; i++) { const char = pattern[i]!; if (isEscaped(pattern, i)) continue; if (char === "[") { inCharClass = true; continue; } if (char === "]") { inCharClass = false; continue; } if (inCharClass) continue; if (char === "(") { depth++; continue; } if (char === ")") { depth = Math.max(0, depth - 1); continue; } if (char === "|" && depth === 0) { branches.push(pattern.slice(start, i)); start = i + 1; } } branches.push(pattern.slice(start)); return branches; } function stripGroupPrefix(pattern: string): string { if (pattern.startsWith("?:") || pattern.startsWith("?=") || pattern.startsWith("?!")) return pattern.slice(2); if (pattern.startsWith("?<=") || pattern.startsWith("?]+>/.exec(pattern); return namedCapture ? pattern.slice(namedCapture[0].length) : pattern; }