229 lines
6.7 KiB
Python
229 lines
6.7 KiB
Python
class IKVToken:
|
|
__slots__ = ("type", "val")
|
|
def __init__(self, t, v=None):
|
|
self.type = t
|
|
self.val = v
|
|
|
|
def ikv_lex(src):
|
|
n = len(src)
|
|
i = 0
|
|
def is_ws(c):
|
|
return c in " \t\r\n"
|
|
|
|
while i < n:
|
|
c = src[i]
|
|
if is_ws(c):
|
|
i += 1
|
|
continue
|
|
if c == "/" and i + 1 < n and src[i + 1] == "/":
|
|
i += 2
|
|
while i < n and src[i] != "\n":
|
|
i += 1
|
|
continue
|
|
if c == "#":
|
|
i += 1
|
|
while i < n and src[i] != "\n":
|
|
i += 1
|
|
continue
|
|
if c in "{}[],":
|
|
i += 1
|
|
yield IKVToken(c)
|
|
continue
|
|
if c == '"':
|
|
i += 1
|
|
out = []
|
|
while i < n:
|
|
ch = src[i]
|
|
if ch == "\\" and i + 1 < n:
|
|
nx = src[i + 1]
|
|
if nx == "n":
|
|
out.append("\n")
|
|
i += 2
|
|
continue
|
|
if nx == "r":
|
|
out.append("\r")
|
|
i += 2
|
|
continue
|
|
if nx == "t":
|
|
out.append("\t")
|
|
i += 2
|
|
continue
|
|
if nx == "\\":
|
|
out.append("\\")
|
|
i += 2
|
|
continue
|
|
if nx == '"':
|
|
out.append('"')
|
|
i += 2
|
|
continue
|
|
out.append(nx)
|
|
i += 2
|
|
continue
|
|
if ch == '"':
|
|
i += 1
|
|
break
|
|
out.append(ch)
|
|
i += 1
|
|
yield IKVToken("STRING", "".join(out))
|
|
continue
|
|
|
|
j = i
|
|
while j < n and (not is_ws(src[j])) and src[j] not in "{}[],":
|
|
j += 1
|
|
w = src[i:j]
|
|
i = j
|
|
yield IKVToken("WORD", w)
|
|
|
|
yield IKVToken("EOF")
|
|
|
|
class IKVParser:
|
|
def __init__(self, src):
|
|
self.tokens = ikv_lex(src)
|
|
self.cur = next(self.tokens)
|
|
|
|
def _eat(self, t):
|
|
if self.cur.type != t:
|
|
raise ValueError(f"IKV parse: expected {t}, got {self.cur.type}")
|
|
self.cur = next(self.tokens)
|
|
|
|
def _maybe(self, t):
|
|
if self.cur.type == t:
|
|
self.cur = next(self.tokens)
|
|
return True
|
|
return False
|
|
|
|
def parse(self):
|
|
if self.cur.type == "WORD" and self.cur.val == "ikv1":
|
|
self._eat("WORD")
|
|
if self.cur.type in ("STRING", "WORD"):
|
|
self.cur = next(self.tokens)
|
|
if self.cur.type != "{":
|
|
raise ValueError("IKV parse: expected { after header")
|
|
return self._parse_object()
|
|
if self.cur.type == "{":
|
|
return self._parse_object()
|
|
return self._parse_object_flat()
|
|
|
|
def _parse_object(self):
|
|
self._eat("{")
|
|
obj = {}
|
|
while True:
|
|
if self._maybe("}"):
|
|
break
|
|
if self._maybe(","):
|
|
continue
|
|
if self.cur.type != "STRING":
|
|
raise ValueError("IKV parse: object key must be string")
|
|
key = self.cur.val
|
|
self._eat("STRING")
|
|
val = self._parse_value()
|
|
obj[key] = val
|
|
self._maybe(",")
|
|
return obj
|
|
|
|
def _parse_array(self):
|
|
self._eat("[")
|
|
arr = []
|
|
while True:
|
|
if self._maybe("]"):
|
|
break
|
|
if self._maybe(","):
|
|
continue
|
|
arr.append(self._parse_value())
|
|
self._maybe(",")
|
|
return arr
|
|
|
|
def _parse_value(self):
|
|
if self.cur.type == "{":
|
|
return self._parse_object()
|
|
if self.cur.type == "[":
|
|
return self._parse_array()
|
|
if self.cur.type == "STRING":
|
|
s = self.cur.val
|
|
self._eat("STRING")
|
|
return s
|
|
if self.cur.type == "WORD":
|
|
w = self.cur.val
|
|
self._eat("WORD")
|
|
if w == "true":
|
|
return True
|
|
if w == "false":
|
|
return False
|
|
if w == "null":
|
|
return None
|
|
try:
|
|
if any(ch in w for ch in ".eE"):
|
|
return float(w)
|
|
return int(w, 10)
|
|
except Exception:
|
|
return w
|
|
raise ValueError("IKV parse: unexpected token")
|
|
|
|
def _parse_object_flat(self):
|
|
obj = {}
|
|
while self.cur.type != "EOF":
|
|
if self._maybe(","):
|
|
continue
|
|
if self.cur.type != "STRING":
|
|
raise ValueError("IKV parse: expected string key")
|
|
key = self.cur.val
|
|
self._eat("STRING")
|
|
obj[key] = self._parse_value()
|
|
self._maybe(",")
|
|
return obj
|
|
|
|
def parse_ikv_text(txt):
|
|
return IKVParser(txt).parse()
|
|
|
|
def pack_value_from_index_gen(index, generation):
|
|
return ((int(generation) & 0xFFFF) << 16) | (int(index) & 0xFFFF)
|
|
|
|
def extract_handles_recursive(node):
|
|
out = []
|
|
|
|
def maybe_add_value_type_meta(d):
|
|
if not isinstance(d, dict):
|
|
return
|
|
if "value" in d and "type" in d and "meta" in d:
|
|
v = d["value"]
|
|
t = d["type"]
|
|
m = d["meta"]
|
|
if isinstance(v, int) and isinstance(t, int) and isinstance(m, int):
|
|
out.append((int(v) & 0xFFFFFFFF, int(t) & 0xFFFF, int(m) & 0xFFFF))
|
|
|
|
def maybe_add_index_gen(d):
|
|
if not isinstance(d, dict):
|
|
return
|
|
if "index" in d and "generation" in d and "type" in d and "meta" in d:
|
|
idx = d["index"]
|
|
gen = d["generation"]
|
|
t = d["type"]
|
|
m = d["meta"]
|
|
if isinstance(idx, int) and isinstance(gen, int) and isinstance(t, int) and isinstance(m, int):
|
|
v = pack_value_from_index_gen(idx, gen)
|
|
out.append((int(v) & 0xFFFFFFFF, int(t) & 0xFFFF, int(m) & 0xFFFF))
|
|
|
|
def visit(x):
|
|
if isinstance(x, dict):
|
|
if "ihandle" in x and isinstance(x["ihandle"], dict):
|
|
maybe_add_value_type_meta(x["ihandle"])
|
|
maybe_add_index_gen(x["ihandle"])
|
|
maybe_add_value_type_meta(x)
|
|
maybe_add_index_gen(x)
|
|
for vv in x.values():
|
|
visit(vv)
|
|
elif isinstance(x, list):
|
|
for vv in x:
|
|
visit(vv)
|
|
|
|
visit(node)
|
|
|
|
seen = set()
|
|
uniq = []
|
|
for h in out:
|
|
k = (h[0] | (h[1] << 32) | (h[2] << 48))
|
|
if k not in seen:
|
|
seen.add(k)
|
|
uniq.append(h)
|
|
return uniq
|