import random import re from sqlite_static_helper import * _TOKEN_RE = re.compile( r""" (?P--[^\n]*) | (?P/\*.*?\*/) | (?P'(?:[^']|'')*') | (?P"(?:[^"]|"")*") | (?P\[[^\]]*\]) | (?P`(?:[^`]|``)*`) | (?P[xX]'[0-9a-fA-F]*') | (?P\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b) | (?P[A-Za-z_][A-Za-z0-9_]*) | (?P[<>!=]=|<>|\|\||::|->>?|[+\-*/%<>=&|^~,.;()@]) | (?P\s+) | (?P.) """, re.VERBOSE | re.DOTALL, ) _UNSAFE_KINDS = { "line_comment", "block_comment", "string", "dquoted", "bracket", "backtick", "blob" } def _tokenize(s: str): return [(m.lastgroup, m.group()) for m in _TOKEN_RE.finditer(s)] def _sub_in_safe(s: str, pattern: re.Pattern, repl, max_subs: int = 1) -> str: """Run `pattern.sub(repl, …)` only outside strings/comments/blob literals.""" if max_subs <= 0: return s parts: list[str] = [] done = 0 for kind, text in _tokenize(s): if done >= max_subs or kind in _UNSAFE_KINDS: parts.append(text) continue new_text, n = pattern.subn(repl, text, count=max_subs - done) parts.append(new_text) done += n return ''.join(parts) _CMP_RE = re.compile(r'(?!=])(<=|>=|<>|!=|==|=|<|>)(?!=)') def mut_swap_comparison(s: str) -> str: """Swap the left-most comparison operator with a different one""" def repl(m: re.Match) -> str: op = m.group(1) return random.choice([o for o in CMP_OPS if o != op]) return _sub_in_safe(s, _CMP_RE, repl, 1) _BOOL_RE = re.compile(r'\b(AND|OR)\b', re.I) def mut_swap_boolean(s: str) -> str: """Swap the left-most boolean operator with a different one""" def repl(m: re.Match) -> str: return 'OR' if m.group(1).upper() == 'AND' else 'AND' return _sub_in_safe(s, _BOOL_RE, repl, 1) def mut_negate_where(s: str) -> str: """Swap the left-most WHERE with WHERE NOT""" if not re.search(r'\bWHERE\b', s, re.I): return s return _sub_in_safe(s, re.compile(r'\bWHERE\b', re.I), 'WHERE NOT', 1) def mut_change_type(s: str) -> str: """Swap the left-most data type with a different one""" pat = re.compile( r'\b(' + '|'.join( re.escape(t) for t in sorted(TYPES, key=len, reverse=True)) + r')\b', re.I, ) def repl(m: re.Match) -> str: cur = m.group(1).upper() return random.choice( [t for t in TYPES if t.upper() != cur]) return _sub_in_safe(s, pat, repl, 1) def mut_swap_join_type(s: str) -> str: """Swap one join type to a different join type""" join_pattern = '|'.join( re.escape(j) for j in sorted(JOINS, key=len, reverse=True)) pat = re.compile(rf'\b({join_pattern})\b', re.I) m = pat.search(s) if not m: return s matched = m.group(1).upper() alternatives = [j for j in JOINS if j.upper() != matched] if not alternatives: return s replacement = random.choice(alternatives) if m.group(1).isupper(): replacement = replacement.upper() elif m.group(1).islower(): replacement = replacement.lower() elif m.group(1)[0].isupper() and m.group(1)[1:].islower(): replacement = replacement.title() return s[:m.start()] + replacement + s[m.end():]