mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-16 01:01:58 +00:00
Add Loki and Sentinel utility classes for web API endpoints
- Implemented LokiUtils class with GET and POST endpoints for managing scripts, jobs, and payloads. - Added SentinelUtils class with GET and POST endpoints for managing events, rules, devices, and notifications. - Both classes include error handling and JSON response formatting.
This commit is contained in:
748
loki/hidscript.py
Normal file
748
loki/hidscript.py
Normal file
@@ -0,0 +1,748 @@
|
||||
"""
|
||||
HIDScript parser and executor for Loki.
|
||||
|
||||
Supports P4wnP1-compatible HIDScript syntax:
|
||||
- Function calls: type("hello"); press("GUI r"); delay(500);
|
||||
- var declarations: var x = 1;
|
||||
- for / while loops
|
||||
- if / else conditionals
|
||||
- // and /* */ comments
|
||||
- String concatenation with +
|
||||
- Basic arithmetic (+, -, *, /)
|
||||
- console.log() for job output
|
||||
|
||||
Zero external dependencies — pure Python DSL parser.
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
from threading import Event
|
||||
|
||||
from logger import Logger
|
||||
|
||||
logger = Logger(name="loki.hidscript", level=logging.DEBUG)
|
||||
|
||||
# ── LED constants (available in scripts) ──────────────────────
|
||||
NUM = 0x01
|
||||
CAPS = 0x02
|
||||
SCROLL = 0x04
|
||||
ANY = 0xFF
|
||||
|
||||
# ── Mouse button constants ────────────────────────────────────
|
||||
BT1 = 1 # Left
|
||||
BT2 = 2 # Right
|
||||
BT3 = 4 # Middle
|
||||
BTNONE = 0
|
||||
|
||||
|
||||
class HIDScriptError(Exception):
|
||||
"""Error during HIDScript execution."""
|
||||
def __init__(self, message, line=None):
|
||||
self.line = line
|
||||
super().__init__(f"Line {line}: {message}" if line else message)
|
||||
|
||||
|
||||
class HIDScriptParser:
|
||||
"""Parse and execute P4wnP1-compatible HIDScript."""
|
||||
|
||||
def __init__(self, hid_controller, layout="us"):
|
||||
self.hid = hid_controller
|
||||
self._default_layout = layout
|
||||
self._output = [] # console.log output
|
||||
|
||||
def execute(self, source: str, stop_event: Event = None, job_id: str = ""):
|
||||
"""Parse and execute a HIDScript source string.
|
||||
|
||||
Returns list of console.log output lines.
|
||||
"""
|
||||
self._output = []
|
||||
self._stop = stop_event or Event()
|
||||
self._vars = {
|
||||
# Built-in constants
|
||||
"NUM": NUM, "CAPS": CAPS, "SCROLL": SCROLL, "ANY": ANY,
|
||||
"BT1": BT1, "BT2": BT2, "BT3": BT3, "BTNONE": BTNONE,
|
||||
"true": True, "false": False, "null": None,
|
||||
}
|
||||
|
||||
# Strip comments
|
||||
source = self._strip_comments(source)
|
||||
# Tokenize into statements
|
||||
stmts = self._parse_block(source)
|
||||
# Execute
|
||||
self._exec_stmts(stmts)
|
||||
|
||||
return self._output
|
||||
|
||||
# ── Comment stripping ──────────────────────────────────────
|
||||
|
||||
def _strip_comments(self, source: str) -> str:
|
||||
"""Remove // and /* */ comments."""
|
||||
# Block comments first
|
||||
source = re.sub(r'/\*.*?\*/', '', source, flags=re.DOTALL)
|
||||
# Line comments
|
||||
source = re.sub(r'//[^\n]*', '', source)
|
||||
return source
|
||||
|
||||
# ── Parser ─────────────────────────────────────────────────
|
||||
|
||||
def _parse_block(self, source: str) -> list:
|
||||
"""Parse source into a list of statement dicts."""
|
||||
stmts = []
|
||||
pos = 0
|
||||
source = source.strip()
|
||||
|
||||
while pos < len(source):
|
||||
if self._stop.is_set():
|
||||
break
|
||||
pos = self._skip_ws(source, pos)
|
||||
if pos >= len(source):
|
||||
break
|
||||
|
||||
# var declaration
|
||||
if source[pos:pos+4] == 'var ' or source[pos:pos+4] == 'let ':
|
||||
end = source.find(';', pos)
|
||||
if end == -1:
|
||||
end = len(source)
|
||||
decl = source[pos+4:end].strip()
|
||||
eq = decl.find('=')
|
||||
if eq >= 0:
|
||||
name = decl[:eq].strip()
|
||||
value_expr = decl[eq+1:].strip()
|
||||
stmts.append({"type": "assign", "name": name, "expr": value_expr})
|
||||
else:
|
||||
stmts.append({"type": "assign", "name": decl.strip(), "expr": "null"})
|
||||
pos = end + 1
|
||||
|
||||
# for loop
|
||||
elif source[pos:pos+4] == 'for ' or source[pos:pos+4] == 'for(':
|
||||
stmt, pos = self._parse_for(source, pos)
|
||||
stmts.append(stmt)
|
||||
|
||||
# while loop
|
||||
elif source[pos:pos+6] == 'while ' or source[pos:pos+6] == 'while(':
|
||||
stmt, pos = self._parse_while(source, pos)
|
||||
stmts.append(stmt)
|
||||
|
||||
# if statement
|
||||
elif source[pos:pos+3] == 'if ' or source[pos:pos+3] == 'if(':
|
||||
stmt, pos = self._parse_if(source, pos)
|
||||
stmts.append(stmt)
|
||||
|
||||
# Block: { ... }
|
||||
elif source[pos] == '{':
|
||||
end = self._find_matching_brace(source, pos)
|
||||
inner = source[pos+1:end]
|
||||
stmts.extend(self._parse_block(inner))
|
||||
pos = end + 1
|
||||
|
||||
# Expression statement (function call or assignment)
|
||||
else:
|
||||
end = source.find(';', pos)
|
||||
if end == -1:
|
||||
end = len(source)
|
||||
expr = source[pos:end].strip()
|
||||
if expr:
|
||||
# Check for assignment: name = expr
|
||||
m = re.match(r'^([a-zA-Z_]\w*)\s*=\s*(.+)$', expr)
|
||||
if m and not expr.startswith('=='):
|
||||
stmts.append({"type": "assign", "name": m.group(1), "expr": m.group(2)})
|
||||
else:
|
||||
stmts.append({"type": "expr", "expr": expr})
|
||||
pos = end + 1
|
||||
|
||||
return stmts
|
||||
|
||||
def _parse_for(self, source, pos):
|
||||
"""Parse: for (init; cond; incr) { body }"""
|
||||
# Find parenthesized header
|
||||
p_start = source.index('(', pos)
|
||||
p_end = self._find_matching_paren(source, p_start)
|
||||
header = source[p_start+1:p_end]
|
||||
|
||||
parts = header.split(';')
|
||||
if len(parts) != 3:
|
||||
raise HIDScriptError("Invalid for loop header")
|
||||
init_expr = parts[0].strip()
|
||||
cond_expr = parts[1].strip()
|
||||
incr_expr = parts[2].strip()
|
||||
|
||||
# Remove var/let prefix from init
|
||||
for prefix in ('var ', 'let '):
|
||||
if init_expr.startswith(prefix):
|
||||
init_expr = init_expr[len(prefix):]
|
||||
|
||||
# Find body
|
||||
body_start = self._skip_ws(source, p_end + 1)
|
||||
if body_start < len(source) and source[body_start] == '{':
|
||||
body_end = self._find_matching_brace(source, body_start)
|
||||
body = source[body_start+1:body_end]
|
||||
next_pos = body_end + 1
|
||||
else:
|
||||
semi = source.find(';', body_start)
|
||||
if semi == -1:
|
||||
semi = len(source)
|
||||
body = source[body_start:semi]
|
||||
next_pos = semi + 1
|
||||
|
||||
return {
|
||||
"type": "for",
|
||||
"init": init_expr,
|
||||
"cond": cond_expr,
|
||||
"incr": incr_expr,
|
||||
"body": body,
|
||||
}, next_pos
|
||||
|
||||
def _parse_while(self, source, pos):
|
||||
"""Parse: while (cond) { body }"""
|
||||
p_start = source.index('(', pos)
|
||||
p_end = self._find_matching_paren(source, p_start)
|
||||
cond = source[p_start+1:p_end].strip()
|
||||
|
||||
body_start = self._skip_ws(source, p_end + 1)
|
||||
if body_start < len(source) and source[body_start] == '{':
|
||||
body_end = self._find_matching_brace(source, body_start)
|
||||
body = source[body_start+1:body_end]
|
||||
next_pos = body_end + 1
|
||||
else:
|
||||
semi = source.find(';', body_start)
|
||||
if semi == -1:
|
||||
semi = len(source)
|
||||
body = source[body_start:semi]
|
||||
next_pos = semi + 1
|
||||
|
||||
return {"type": "while", "cond": cond, "body": body}, next_pos
|
||||
|
||||
def _parse_if(self, source, pos):
|
||||
"""Parse: if (cond) { body } [else { body }]"""
|
||||
p_start = source.index('(', pos)
|
||||
p_end = self._find_matching_paren(source, p_start)
|
||||
cond = source[p_start+1:p_end].strip()
|
||||
|
||||
body_start = self._skip_ws(source, p_end + 1)
|
||||
if body_start < len(source) and source[body_start] == '{':
|
||||
body_end = self._find_matching_brace(source, body_start)
|
||||
body = source[body_start+1:body_end]
|
||||
next_pos = body_end + 1
|
||||
else:
|
||||
semi = source.find(';', body_start)
|
||||
if semi == -1:
|
||||
semi = len(source)
|
||||
body = source[body_start:semi]
|
||||
next_pos = semi + 1
|
||||
|
||||
# Check for else
|
||||
else_body = None
|
||||
check = self._skip_ws(source, next_pos)
|
||||
if source[check:check+4] == 'else':
|
||||
after_else = self._skip_ws(source, check + 4)
|
||||
if after_else < len(source) and source[after_else] == '{':
|
||||
eb_end = self._find_matching_brace(source, after_else)
|
||||
else_body = source[after_else+1:eb_end]
|
||||
next_pos = eb_end + 1
|
||||
elif source[after_else:after_else+2] == 'if':
|
||||
# else if — parse recursively
|
||||
inner_if, next_pos = self._parse_if(source, after_else)
|
||||
else_body = inner_if # will be a dict, handle in exec
|
||||
else:
|
||||
semi = source.find(';', after_else)
|
||||
if semi == -1:
|
||||
semi = len(source)
|
||||
else_body = source[after_else:semi]
|
||||
next_pos = semi + 1
|
||||
|
||||
return {"type": "if", "cond": cond, "body": body, "else": else_body}, next_pos
|
||||
|
||||
# ── Executor ───────────────────────────────────────────────
|
||||
|
||||
def _exec_stmts(self, stmts: list):
|
||||
"""Execute a list of parsed statements."""
|
||||
for stmt in stmts:
|
||||
if self._stop.is_set():
|
||||
return
|
||||
stype = stmt["type"]
|
||||
|
||||
if stype == "assign":
|
||||
self._vars[stmt["name"]] = self._eval_expr(stmt["expr"])
|
||||
|
||||
elif stype == "expr":
|
||||
self._eval_expr(stmt["expr"])
|
||||
|
||||
elif stype == "for":
|
||||
self._exec_for(stmt)
|
||||
|
||||
elif stype == "while":
|
||||
self._exec_while(stmt)
|
||||
|
||||
elif stype == "if":
|
||||
self._exec_if(stmt)
|
||||
|
||||
def _exec_for(self, stmt):
|
||||
"""Execute a for loop."""
|
||||
# Parse init as assignment
|
||||
init = stmt["init"]
|
||||
eq = init.find('=')
|
||||
if eq >= 0:
|
||||
name = init[:eq].strip()
|
||||
self._vars[name] = self._eval_expr(init[eq+1:].strip())
|
||||
|
||||
max_iterations = 100000
|
||||
i = 0
|
||||
while i < max_iterations:
|
||||
if self._stop.is_set():
|
||||
return
|
||||
if not self._eval_expr(stmt["cond"]):
|
||||
break
|
||||
self._exec_stmts(self._parse_block(stmt["body"]))
|
||||
# Execute increment
|
||||
incr = stmt["incr"]
|
||||
if "++" in incr:
|
||||
var_name = incr.replace("++", "").strip()
|
||||
self._vars[var_name] = self._vars.get(var_name, 0) + 1
|
||||
elif "--" in incr:
|
||||
var_name = incr.replace("--", "").strip()
|
||||
self._vars[var_name] = self._vars.get(var_name, 0) - 1
|
||||
else:
|
||||
eq = incr.find('=')
|
||||
if eq >= 0:
|
||||
name = incr[:eq].strip()
|
||||
self._vars[name] = self._eval_expr(incr[eq+1:].strip())
|
||||
i += 1
|
||||
|
||||
def _exec_while(self, stmt):
|
||||
"""Execute a while loop."""
|
||||
max_iterations = 1000000
|
||||
i = 0
|
||||
while i < max_iterations:
|
||||
if self._stop.is_set():
|
||||
return
|
||||
if not self._eval_expr(stmt["cond"]):
|
||||
break
|
||||
self._exec_stmts(self._parse_block(stmt["body"]))
|
||||
i += 1
|
||||
|
||||
def _exec_if(self, stmt):
|
||||
"""Execute an if/else statement."""
|
||||
if self._eval_expr(stmt["cond"]):
|
||||
self._exec_stmts(self._parse_block(stmt["body"]))
|
||||
elif stmt.get("else"):
|
||||
else_part = stmt["else"]
|
||||
if isinstance(else_part, dict):
|
||||
# else if
|
||||
self._exec_if(else_part)
|
||||
else:
|
||||
self._exec_stmts(self._parse_block(else_part))
|
||||
|
||||
# ── Expression Evaluator ───────────────────────────────────
|
||||
|
||||
def _eval_expr(self, expr):
|
||||
"""Evaluate an expression string and return its value."""
|
||||
if isinstance(expr, (int, float, bool)):
|
||||
return expr
|
||||
if not isinstance(expr, str):
|
||||
return expr
|
||||
|
||||
expr = expr.strip()
|
||||
if not expr:
|
||||
return None
|
||||
|
||||
# String literal
|
||||
if (expr.startswith('"') and expr.endswith('"')) or \
|
||||
(expr.startswith("'") and expr.endswith("'")):
|
||||
return self._unescape(expr[1:-1])
|
||||
|
||||
# Numeric literal
|
||||
try:
|
||||
if '.' in expr:
|
||||
return float(expr)
|
||||
return int(expr)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Boolean / null
|
||||
if expr == 'true':
|
||||
return True
|
||||
if expr == 'false':
|
||||
return False
|
||||
if expr == 'null':
|
||||
return None
|
||||
|
||||
# String concatenation with +
|
||||
if self._has_top_level_op(expr, '+') and self._contains_string(expr):
|
||||
parts = self._split_top_level(expr, '+')
|
||||
result = ""
|
||||
for p in parts:
|
||||
val = self._eval_expr(p.strip())
|
||||
result += str(val) if val is not None else ""
|
||||
return result
|
||||
|
||||
# Comparison operators
|
||||
for op in ['===', '!==', '==', '!=', '>=', '<=', '>', '<']:
|
||||
if self._has_top_level_op(expr, op):
|
||||
parts = self._split_top_level(expr, op, max_splits=1)
|
||||
if len(parts) == 2:
|
||||
left = self._eval_expr(parts[0].strip())
|
||||
right = self._eval_expr(parts[1].strip())
|
||||
if op in ('==', '==='):
|
||||
return left == right
|
||||
elif op in ('!=', '!=='):
|
||||
return left != right
|
||||
elif op == '>':
|
||||
return left > right
|
||||
elif op == '<':
|
||||
return left < right
|
||||
elif op == '>=':
|
||||
return left >= right
|
||||
elif op == '<=':
|
||||
return left <= right
|
||||
|
||||
# Logical operators
|
||||
if self._has_top_level_op(expr, '&&'):
|
||||
parts = self._split_top_level(expr, '&&', max_splits=1)
|
||||
return self._eval_expr(parts[0]) and self._eval_expr(parts[1])
|
||||
if self._has_top_level_op(expr, '||'):
|
||||
parts = self._split_top_level(expr, '||', max_splits=1)
|
||||
return self._eval_expr(parts[0]) or self._eval_expr(parts[1])
|
||||
|
||||
# Arithmetic
|
||||
for op in ['+', '-']:
|
||||
if self._has_top_level_op(expr, op) and not self._contains_string(expr):
|
||||
parts = self._split_top_level(expr, op)
|
||||
result = self._eval_expr(parts[0].strip())
|
||||
for p in parts[1:]:
|
||||
val = self._eval_expr(p.strip())
|
||||
if op == '+':
|
||||
result = (result or 0) + (val or 0)
|
||||
else:
|
||||
result = (result or 0) - (val or 0)
|
||||
return result
|
||||
|
||||
for op in ['*', '/']:
|
||||
if self._has_top_level_op(expr, op):
|
||||
parts = self._split_top_level(expr, op)
|
||||
result = self._eval_expr(parts[0].strip())
|
||||
for p in parts[1:]:
|
||||
val = self._eval_expr(p.strip())
|
||||
if op == '*':
|
||||
result = (result or 0) * (val or 0)
|
||||
else:
|
||||
result = (result or 0) / (val or 1)
|
||||
return result
|
||||
|
||||
# Modulo
|
||||
if self._has_top_level_op(expr, '%'):
|
||||
parts = self._split_top_level(expr, '%')
|
||||
result = self._eval_expr(parts[0].strip())
|
||||
for p in parts[1:]:
|
||||
val = self._eval_expr(p.strip())
|
||||
result = (result or 0) % (val or 1)
|
||||
return result
|
||||
|
||||
# Negation
|
||||
if expr.startswith('!'):
|
||||
return not self._eval_expr(expr[1:])
|
||||
|
||||
# Parenthesized expression
|
||||
if expr.startswith('(') and self._find_matching_paren(expr, 0) == len(expr) - 1:
|
||||
return self._eval_expr(expr[1:-1])
|
||||
|
||||
# Function call
|
||||
m = re.match(r'^([a-zA-Z_][\w.]*)\s*\(', expr)
|
||||
if m:
|
||||
func_name = m.group(1)
|
||||
p_start = expr.index('(')
|
||||
p_end = self._find_matching_paren(expr, p_start)
|
||||
args_str = expr[p_start+1:p_end]
|
||||
args = self._parse_args(args_str)
|
||||
return self._call_func(func_name, args)
|
||||
|
||||
# Variable reference
|
||||
if re.match(r'^[a-zA-Z_]\w*$', expr):
|
||||
return self._vars.get(expr, 0)
|
||||
|
||||
# Increment/decrement as expression
|
||||
if expr.endswith('++'):
|
||||
name = expr[:-2].strip()
|
||||
val = self._vars.get(name, 0)
|
||||
self._vars[name] = val + 1
|
||||
return val
|
||||
if expr.endswith('--'):
|
||||
name = expr[:-2].strip()
|
||||
val = self._vars.get(name, 0)
|
||||
self._vars[name] = val - 1
|
||||
return val
|
||||
|
||||
logger.warning("Cannot evaluate expression: %r", expr)
|
||||
return 0
|
||||
|
||||
# ── Built-in Functions ─────────────────────────────────────
|
||||
|
||||
def _call_func(self, name: str, args: list):
|
||||
"""Dispatch a built-in function call."""
|
||||
# Evaluate all arguments
|
||||
evaled = [self._eval_expr(a) for a in args]
|
||||
|
||||
if name == "type":
|
||||
text = str(evaled[0]) if evaled else ""
|
||||
self.hid.type_string(text, stop_event=self._stop)
|
||||
|
||||
elif name == "press":
|
||||
combo = str(evaled[0]) if evaled else ""
|
||||
self.hid.press_combo(combo)
|
||||
|
||||
elif name == "delay":
|
||||
ms = int(evaled[0]) if evaled else 0
|
||||
if ms > 0:
|
||||
self._stop.wait(ms / 1000.0)
|
||||
|
||||
elif name == "layout":
|
||||
name_val = str(evaled[0]) if evaled else self._default_layout
|
||||
self.hid.set_layout(name_val)
|
||||
|
||||
elif name == "typingSpeed":
|
||||
min_ms = int(evaled[0]) if len(evaled) > 0 else 0
|
||||
max_ms = int(evaled[1]) if len(evaled) > 1 else min_ms
|
||||
self.hid.set_typing_speed(min_ms, max_ms)
|
||||
|
||||
elif name == "move":
|
||||
x = int(evaled[0]) if len(evaled) > 0 else 0
|
||||
y = int(evaled[1]) if len(evaled) > 1 else 0
|
||||
self.hid.mouse_move(x, y)
|
||||
|
||||
elif name == "moveTo":
|
||||
x = int(evaled[0]) if len(evaled) > 0 else 0
|
||||
y = int(evaled[1]) if len(evaled) > 1 else 0
|
||||
self.hid.mouse_move_stepped(x, y, step=5)
|
||||
|
||||
elif name == "moveStepped":
|
||||
x = int(evaled[0]) if len(evaled) > 0 else 0
|
||||
y = int(evaled[1]) if len(evaled) > 1 else 0
|
||||
step = int(evaled[2]) if len(evaled) > 2 else 10
|
||||
self.hid.mouse_move_stepped(x, y, step=step)
|
||||
|
||||
elif name == "click":
|
||||
btn = int(evaled[0]) if evaled else BT1
|
||||
self.hid.mouse_click(btn)
|
||||
|
||||
elif name == "doubleClick":
|
||||
btn = int(evaled[0]) if evaled else BT1
|
||||
self.hid.mouse_double_click(btn)
|
||||
|
||||
elif name == "button":
|
||||
mask = int(evaled[0]) if evaled else 0
|
||||
self.hid.send_mouse_report(mask, 0, 0)
|
||||
|
||||
elif name == "waitLED":
|
||||
mask = int(evaled[0]) if evaled else ANY
|
||||
timeout = float(evaled[1]) / 1000 if len(evaled) > 1 else 0
|
||||
return self.hid.wait_led(mask, self._stop, timeout)
|
||||
|
||||
elif name == "waitLEDRepeat":
|
||||
mask = int(evaled[0]) if evaled else ANY
|
||||
count = int(evaled[1]) if len(evaled) > 1 else 1
|
||||
return self.hid.wait_led_repeat(mask, count, self._stop)
|
||||
|
||||
elif name == "console.log" or name == "log":
|
||||
msg = " ".join(str(a) for a in evaled)
|
||||
self._output.append(msg)
|
||||
logger.debug("[HIDScript] %s", msg)
|
||||
|
||||
elif name in ("parseInt", "Number"):
|
||||
try:
|
||||
return int(float(evaled[0])) if evaled else 0
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
|
||||
elif name == "String":
|
||||
return str(evaled[0]) if evaled else ""
|
||||
|
||||
elif name == "Math.random":
|
||||
import random
|
||||
return random.random()
|
||||
|
||||
elif name == "Math.floor":
|
||||
import math
|
||||
return math.floor(evaled[0]) if evaled else 0
|
||||
|
||||
else:
|
||||
logger.warning("Unknown function: %s", name)
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────
|
||||
|
||||
def _parse_args(self, args_str: str) -> list:
|
||||
"""Split function arguments respecting string literals and parens."""
|
||||
args = []
|
||||
depth = 0
|
||||
current = ""
|
||||
in_str = None
|
||||
|
||||
for ch in args_str:
|
||||
if in_str:
|
||||
current += ch
|
||||
if ch == in_str and (len(current) < 2 or current[-2] != '\\'):
|
||||
in_str = None
|
||||
elif ch in ('"', "'"):
|
||||
in_str = ch
|
||||
current += ch
|
||||
elif ch == '(':
|
||||
depth += 1
|
||||
current += ch
|
||||
elif ch == ')':
|
||||
depth -= 1
|
||||
current += ch
|
||||
elif ch == ',' and depth == 0:
|
||||
if current.strip():
|
||||
args.append(current.strip())
|
||||
current = ""
|
||||
else:
|
||||
current += ch
|
||||
|
||||
if current.strip():
|
||||
args.append(current.strip())
|
||||
return args
|
||||
|
||||
def _unescape(self, s: str) -> str:
|
||||
"""Process escape sequences in a string."""
|
||||
return s.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r') \
|
||||
.replace('\\"', '"').replace("\\'", "'").replace('\\\\', '\\')
|
||||
|
||||
def _skip_ws(self, source: str, pos: int) -> int:
|
||||
"""Skip whitespace."""
|
||||
while pos < len(source) and source[pos] in ' \t\n\r':
|
||||
pos += 1
|
||||
return pos
|
||||
|
||||
def _find_matching_brace(self, source: str, pos: int) -> int:
|
||||
"""Find matching } for { at pos."""
|
||||
depth = 1
|
||||
i = pos + 1
|
||||
in_str = None
|
||||
while i < len(source):
|
||||
ch = source[i]
|
||||
if in_str:
|
||||
if ch == in_str and source[i-1] != '\\':
|
||||
in_str = None
|
||||
elif ch in ('"', "'"):
|
||||
in_str = ch
|
||||
elif ch == '{':
|
||||
depth += 1
|
||||
elif ch == '}':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i
|
||||
i += 1
|
||||
return len(source) - 1
|
||||
|
||||
def _find_matching_paren(self, source: str, pos: int) -> int:
|
||||
"""Find matching ) for ( at pos."""
|
||||
depth = 1
|
||||
i = pos + 1
|
||||
in_str = None
|
||||
while i < len(source):
|
||||
ch = source[i]
|
||||
if in_str:
|
||||
if ch == in_str and source[i-1] != '\\':
|
||||
in_str = None
|
||||
elif ch in ('"', "'"):
|
||||
in_str = ch
|
||||
elif ch == '(':
|
||||
depth += 1
|
||||
elif ch == ')':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i
|
||||
i += 1
|
||||
return len(source) - 1
|
||||
|
||||
def _has_top_level_op(self, expr: str, op: str) -> bool:
|
||||
"""Check if operator exists at top level (not inside parens/strings)."""
|
||||
depth = 0
|
||||
in_str = None
|
||||
i = 0
|
||||
while i < len(expr):
|
||||
ch = expr[i]
|
||||
if in_str:
|
||||
if ch == in_str and (i == 0 or expr[i-1] != '\\'):
|
||||
in_str = None
|
||||
elif ch in ('"', "'"):
|
||||
in_str = ch
|
||||
elif ch == '(':
|
||||
depth += 1
|
||||
elif ch == ')':
|
||||
depth -= 1
|
||||
elif depth == 0 and expr[i:i+len(op)] == op:
|
||||
# Don't match multi-char ops that are substrings of longer ones
|
||||
if len(op) == 1 and op in '+-':
|
||||
# Skip if part of ++ or --
|
||||
if i + 1 < len(expr) and expr[i+1] == op:
|
||||
i += 2
|
||||
continue
|
||||
if i > 0 and expr[i-1] == op:
|
||||
i += 1
|
||||
continue
|
||||
return True
|
||||
i += 1
|
||||
return False
|
||||
|
||||
def _split_top_level(self, expr: str, op: str, max_splits: int = -1) -> list:
|
||||
"""Split expression by operator at top level only."""
|
||||
parts = []
|
||||
depth = 0
|
||||
in_str = None
|
||||
current = ""
|
||||
i = 0
|
||||
splits = 0
|
||||
|
||||
while i < len(expr):
|
||||
ch = expr[i]
|
||||
if in_str:
|
||||
current += ch
|
||||
if ch == in_str and (i == 0 or expr[i-1] != '\\'):
|
||||
in_str = None
|
||||
elif ch in ('"', "'"):
|
||||
in_str = ch
|
||||
current += ch
|
||||
elif ch == '(':
|
||||
depth += 1
|
||||
current += ch
|
||||
elif ch == ')':
|
||||
depth -= 1
|
||||
current += ch
|
||||
elif depth == 0 and expr[i:i+len(op)] == op and (max_splits < 0 or splits < max_splits):
|
||||
# Don't split on ++ or -- when looking for + or -
|
||||
if len(op) == 1 and op in '+-':
|
||||
if i + 1 < len(expr) and expr[i+1] == op:
|
||||
current += ch
|
||||
i += 1
|
||||
current += expr[i]
|
||||
i += 1
|
||||
continue
|
||||
parts.append(current)
|
||||
current = ""
|
||||
i += len(op)
|
||||
splits += 1
|
||||
continue
|
||||
else:
|
||||
current += ch
|
||||
i += 1
|
||||
|
||||
parts.append(current)
|
||||
return parts
|
||||
|
||||
def _contains_string(self, expr: str) -> bool:
|
||||
"""Check if expression contains a string literal at top level."""
|
||||
depth = 0
|
||||
in_str = None
|
||||
for ch in expr:
|
||||
if in_str:
|
||||
if ch == in_str:
|
||||
return True # Found complete string
|
||||
elif ch in ('"', "'"):
|
||||
in_str = ch
|
||||
elif ch == '(':
|
||||
depth += 1
|
||||
elif ch == ')':
|
||||
depth -= 1
|
||||
return False
|
||||
Reference in New Issue
Block a user