`init phone register` becomes a default (the group chat) plus a function of library calls that runs with the other defaults of a new game (phone_register.py). `init/python [early|hide] in some.store` wraps its code in an in_store node and `default some.store.name` may name a store; yield works as an expression (generator functions are marked gen); `import x as y`, `transform -150 name(...)`, `init:` with if/else, `config.special_namespaces` lines, `at` lists and tuples, transforms made by Python functions, `choice` weights that are expressions, `__len__`/`__bool__`, and the whitelist for os, calendar, itertools, pygame_sdl2 and the new runtime. The converter also records the size of every picture it converts (`imageSizes`) so `renpy.image_size` answers without loading. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
211 lines
9.1 KiB
Python
211 lines
9.1 KiB
Python
"""Validation of the expression/statement AST accepted by the web interpreter."""
|
|
|
|
|
|
def expression(value, depth=0):
|
|
if depth > 64 or not isinstance(value, dict):
|
|
raise ValueError("Некорректное PGVN-выражение")
|
|
kind = value.get("k")
|
|
|
|
def ex(key, optional=False):
|
|
v = value.get(key)
|
|
if not optional or v is not None:
|
|
expression(v, depth + 1)
|
|
|
|
def seq(key):
|
|
v = value.get(key)
|
|
if not isinstance(v, list):
|
|
raise ValueError(f"Выражение {kind}: нужен список {key}")
|
|
return v
|
|
|
|
if kind == "nani":
|
|
nani_expression(value.get("ast"), depth + 1)
|
|
elif kind == "const":
|
|
if "v" not in value:
|
|
raise ValueError("const без значения")
|
|
elif kind == "name":
|
|
if not isinstance(value.get("id"), str):
|
|
raise ValueError("Имя переменной должно быть строкой")
|
|
elif kind == "attr":
|
|
ex("o")
|
|
if not isinstance(value.get("a"), str):
|
|
raise ValueError("Атрибут должен быть строкой")
|
|
elif kind in ("sub", "slice"):
|
|
ex("o")
|
|
if kind == "sub":
|
|
ex("i")
|
|
else:
|
|
for key in ("lo", "hi", "st"):
|
|
ex(key, True)
|
|
elif kind in ("bin", "unary", "bool"):
|
|
from app.converter.pyexpr import BIN_OPS, UNARY_OPS
|
|
allowed = set(BIN_OPS.values()) if kind == "bin" else set(UNARY_OPS.values()) if kind == "unary" else {"and", "or"}
|
|
if value.get("op") not in allowed:
|
|
raise ValueError("Неизвестная операция выражения")
|
|
if kind == "bin":
|
|
ex("l")
|
|
ex("r")
|
|
elif kind == "unary":
|
|
ex("v")
|
|
else:
|
|
for item in seq("v"):
|
|
expression(item, depth + 1)
|
|
elif kind == "cmp":
|
|
from app.converter.pyexpr import CMP_OPS
|
|
ex("l")
|
|
operators, operands = seq("ops"), seq("r")
|
|
if len(operators) != len(operands) or any(op not in CMP_OPS.values() for op in operators):
|
|
raise ValueError("Некорректное сравнение")
|
|
for item in operands:
|
|
expression(item, depth + 1)
|
|
elif kind == "if":
|
|
for key in ("c", "t", "e"):
|
|
ex(key)
|
|
elif kind in ("list", "tuple", "fmt", "dict"):
|
|
for item in seq("v"):
|
|
if kind == "dict":
|
|
if not isinstance(item, list) or len(item) != 2:
|
|
raise ValueError("Некорректная запись словаря")
|
|
for part in item:
|
|
expression(part, depth + 1)
|
|
elif kind != "fmt" or not isinstance(item, str):
|
|
expression(item, depth + 1)
|
|
elif kind == "star":
|
|
ex("v")
|
|
elif kind == "yield":
|
|
ex("v", optional=True)
|
|
elif kind == "lambda":
|
|
parameters(value.get("params"), depth + 1)
|
|
ex("body")
|
|
elif kind == "call":
|
|
ex("f")
|
|
for item in seq("a"):
|
|
expression(item, depth + 1)
|
|
for item in value.get("ks", []):
|
|
expression(item, depth + 1)
|
|
if not isinstance(value.get("kw"), dict):
|
|
raise ValueError("Некорректные аргументы вызова")
|
|
for item in value["kw"].values():
|
|
expression(item, depth + 1)
|
|
elif kind == "comp":
|
|
ex("e")
|
|
ex("v", True)
|
|
if "gens" in value:
|
|
gens = seq("gens")
|
|
if not gens or len(gens) > 8:
|
|
raise ValueError("Некорректный генератор")
|
|
for gen in gens:
|
|
if not isinstance(gen, dict) or not isinstance(gen.get("if"), list):
|
|
raise ValueError("Некорректный генератор")
|
|
expression(gen.get("t"), depth + 1)
|
|
expression(gen.get("it"), depth + 1)
|
|
for item in gen["if"]:
|
|
expression(item, depth + 1)
|
|
else: # saved by an older converter
|
|
for key in ("t", "it"):
|
|
ex(key)
|
|
for item in seq("if"):
|
|
expression(item, depth + 1)
|
|
else:
|
|
raise ValueError(f"Неизвестное выражение: {kind}")
|
|
|
|
|
|
def parameters(params, depth):
|
|
if not isinstance(params, list) or len(params) > 64:
|
|
raise ValueError("Некорректные параметры функции")
|
|
for p in params:
|
|
if not isinstance(p, dict) or not isinstance(p.get("n"), str) or p.get("kind") not in ("p", "k", "*", "**"):
|
|
raise ValueError("Некорректный параметр функции")
|
|
if p.get("d") is not None:
|
|
expression(p["d"], depth + 1)
|
|
|
|
|
|
def statements(code, depth=0):
|
|
if depth > 64 or not isinstance(code, list):
|
|
raise ValueError("Некорректный PGVN AST")
|
|
for s in code:
|
|
if not isinstance(s, dict):
|
|
raise ValueError("Некорректный оператор AST")
|
|
op = s.get("op")
|
|
keys = {"set": ("t", "v"), "aug": ("t", "v"), "eval": ("e",),
|
|
"if": ("c",), "while": ("c",), "for": ("t", "it"),
|
|
"del": ("t",), "break": (), "continue": (), "def": (), "class": (),
|
|
"return": (), "global": (), "nonlocal": (), "try": (), "raise": (), "yield": (), "yield_from": ("v",), "in_store": (),
|
|
"assert": ("c",)}
|
|
if op not in keys:
|
|
raise ValueError(f"Неизвестный оператор AST: {op}")
|
|
for key in keys[op]:
|
|
expression(s.get(key), depth + 1)
|
|
if op == "in_store":
|
|
if not isinstance(s.get("ns"), str) or not all(part.isidentifier() for part in s["ns"].split(".")):
|
|
raise ValueError("Некорректное имя хранилища")
|
|
statements(s.get("body"), depth + 1)
|
|
if op == "if":
|
|
statements(s.get("then"), depth + 1)
|
|
statements(s.get("else"), depth + 1)
|
|
if op in ("while", "for"):
|
|
statements(s.get("body"), depth + 1)
|
|
statements(s.get("else", []), depth + 1)
|
|
if op == "try":
|
|
statements(s.get("body"), depth + 1)
|
|
statements(s.get("else", []), depth + 1)
|
|
statements(s.get("final", []), depth + 1)
|
|
handlers = s.get("handlers")
|
|
if not isinstance(handlers, list) or len(handlers) > 32:
|
|
raise ValueError("Некорректные обработчики исключений")
|
|
for h in handlers:
|
|
if not isinstance(h, dict) or not (h.get("name") is None or isinstance(h.get("name"), str)):
|
|
raise ValueError("Некорректный обработчик исключения")
|
|
if h.get("types") is not None:
|
|
expression(h["types"], depth + 1)
|
|
statements(h.get("body"), depth + 1)
|
|
if op == "raise" and s.get("v") is not None:
|
|
expression(s["v"], depth + 1)
|
|
if op == "assert" and s.get("m") is not None:
|
|
expression(s["m"], depth + 1)
|
|
if op == "def":
|
|
if not isinstance(s.get("name"), str):
|
|
raise ValueError("У функции нет имени")
|
|
parameters(s.get("params"), depth + 1)
|
|
statements(s.get("body"), depth + 1)
|
|
for item in s.get("decos", []):
|
|
expression(item, depth + 1)
|
|
if op == "class":
|
|
if not isinstance(s.get("name"), str):
|
|
raise ValueError("У класса нет имени")
|
|
for item in s.get("bases", []):
|
|
expression(item, depth + 1)
|
|
statements(s.get("body"), depth + 1)
|
|
if op == "return" and s.get("v") is not None:
|
|
expression(s["v"], depth + 1)
|
|
if op in ("global", "nonlocal") and not (
|
|
isinstance(s.get("names"), list) and all(isinstance(n, str) for n in s["names"])):
|
|
raise ValueError("Некорректный список имён")
|
|
|
|
|
|
def nani_expression(node, depth=0):
|
|
"""Validate the optional Naninovel AST already supported by the shared player."""
|
|
if depth > 64 or not isinstance(node, list) or not node:
|
|
raise ValueError("Некорректный AST Naninovel")
|
|
kind = node[0]
|
|
if kind == "const" and len(node) == 2 and (node[1] is None or isinstance(node[1], (str, int, float, bool))):
|
|
return
|
|
if kind == "name" and len(node) == 2 and isinstance(node[1], str):
|
|
return
|
|
children = None
|
|
if kind == "unary" and len(node) == 3 and node[1] in ("!", "not", "+", "-", "~"):
|
|
children = node[2:]
|
|
elif kind == "bin" and len(node) == 4:
|
|
from app.converter.nani_expr import PRECEDENCE
|
|
if node[1] in PRECEDENCE:
|
|
children = node[2:]
|
|
elif kind == "if" and len(node) == 4:
|
|
children = node[1:]
|
|
elif kind == "call" and len(node) == 3:
|
|
from app.converter.nani_expr import FUNCTIONS
|
|
if node[1] in FUNCTIONS and isinstance(node[2], list):
|
|
children = node[2]
|
|
if children is None:
|
|
raise ValueError("Неизвестная операция AST Naninovel")
|
|
for child in children:
|
|
nani_expression(child, depth + 1)
|