129 lines
4.1 KiB
Python
129 lines
4.1 KiB
Python
import random
|
|
import re
|
|
|
|
import sqlite_static_helper
|
|
|
|
|
|
def _quote_str(text: str) -> str:
|
|
"""O'Reilly -> 'O''Reilly'"""
|
|
return "'" + text.replace("'", "''") + "'"
|
|
|
|
|
|
_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,
|
|
)
|
|
|
|
|
|
def _tokenize(s: str) -> list[tuple[str | None, str]]:
|
|
"""[("line_comment", "-- This is a comment!"), ("number", 0), ...]"""
|
|
return [(m.lastgroup, m.group()) for m in _TOKEN_RE.finditer(s)]
|
|
|
|
|
|
_UNSAFE_KINDS = {
|
|
"line_comment", "block_comment", "string", "dquoted", "bracket", "backtick",
|
|
"blob"
|
|
}
|
|
|
|
|
|
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)
|
|
|
|
|
|
_NUM_RE = re.compile(r'\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b')
|
|
|
|
|
|
def mut_replace_num_with_edge_int(s: str) -> str:
|
|
"""Replace the left-most number with a random edge-case integer"""
|
|
return _sub_in_safe(
|
|
s, _NUM_RE, lambda m: str(random.choice(sqlite_static_helper.EDGE_INTS)), 1)
|
|
|
|
|
|
def mut_replace_num_with_random_int(s: str) -> str:
|
|
"""Replace the left-most number with a random integer"""
|
|
return _sub_in_safe(s, _NUM_RE,
|
|
lambda m: str(random.randint(-(2**63), 2**63 - 1)), 1)
|
|
|
|
|
|
def mut_replace_num_with_edge_float(s: str) -> str:
|
|
"""Replace the left-most number with a random edge-case floating-point number"""
|
|
return _sub_in_safe(
|
|
s, _NUM_RE, lambda m: repr(random.choice(sqlite_static_helper.EDGE_FLOATS)),
|
|
1)
|
|
|
|
|
|
def mut_replace_num_with_edge_string(s: str) -> str:
|
|
"""Replace the left-most number with a random edge-case string"""
|
|
return _sub_in_safe(
|
|
s, _NUM_RE,
|
|
lambda m: _quote_str(random.choice(sqlite_static_helper.EDGE_STRINGS)), 1)
|
|
|
|
|
|
def mut_replace_num_with_random_hex(s: str) -> str:
|
|
"""Replace the left-most number with a random hex blob"""
|
|
n = random.randint(0, 16)
|
|
val = "x'" + ''.join(random.choices('0123456789abcdef', k=n * 2)) + "'"
|
|
return _sub_in_safe(s, _NUM_RE, lambda m: val, 1)
|
|
|
|
|
|
def mut_replace_num_with_null(s: str) -> str:
|
|
"""Replace the left-most number with NULL"""
|
|
return _sub_in_safe(s, _NUM_RE, lambda m: 'NULL', 1)
|
|
|
|
|
|
def mut_wrap_num_in_cast(s: str) -> str:
|
|
"""Cast the left-most number to a different data type"""
|
|
target = random.choice(sqlite_static_helper.TYPES)
|
|
return _sub_in_safe(s, _NUM_RE, lambda m: f'CAST({m.group(0)} AS {target})',
|
|
1)
|
|
|
|
|
|
def _wrap_value(s: str, templates: list[str]) -> str:
|
|
tpl = random.choice(templates)
|
|
return _sub_in_safe(s, _NUM_RE, lambda m: tpl.replace('{v}', m.group(0)), 1)
|
|
|
|
|
|
def mut_wrap_num_in_datetime_func(s: str) -> str:
|
|
"""Wrap the left-most number in a date/time function"""
|
|
return _wrap_value(s, sqlite_static_helper.DATETIME_FUNCS)
|
|
|
|
|
|
def mut_wrap_num_in_core_func(s: str) -> str:
|
|
"""Wrap the left-most number in core function"""
|
|
return _wrap_value(s, sqlite_static_helper.CORE_FUNCS)
|
|
|
|
|
|
def mut_wrap_num_in_math_func(s: str) -> str:
|
|
"""Wrap the left-most number in a math function"""
|
|
return _wrap_value(s, sqlite_static_helper.MATH_FUNCS)
|
|
|
|
|
|
def mut_wrap_num_in_json_func(s: str) -> str:
|
|
"""Wrap the left-most number in a JSON function"""
|
|
return _wrap_value(s, sqlite_static_helper.JSON_VALUE_FUNCS)
|