---
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import random
|
||||
import re
|
||||
|
||||
from sqlite_static_helper import *
|
||||
|
||||
_TOKEN_RE = re.compile(
|
||||
r"""
|
||||
(?P<line_comment>--[^\n]*) |
|
||||
(?P<block_comment>/\*.*?\*/) |
|
||||
(?P<string>'(?:[^']|'')*') |
|
||||
(?P<dquoted>"(?:[^"]|"")*") |
|
||||
(?P<bracket>\[[^\]]*\]) |
|
||||
(?P<backtick>`(?:[^`]|``)*`) |
|
||||
(?P<blob>[xX]'[0-9a-fA-F]*') |
|
||||
(?P<number>\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b) |
|
||||
(?P<ident>[A-Za-z_][A-Za-z0-9_]*) |
|
||||
(?P<op>[<>!=]=|<>|\|\||::|->>?|[+\-*/%<>=&|^~,.;()@]) |
|
||||
(?P<ws>\s+) |
|
||||
(?P<other>.)
|
||||
""",
|
||||
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():]
|
||||
Reference in New Issue
Block a user