165 lines
5.5 KiB
Python
165 lines
5.5 KiB
Python
"""Read .proto files as text and build a field table: {message: {num: [name, type, repeated]}}.
|
|||
|
|
|
||
|
|
Types are either a scalar keyword ('string', 'bytes', 'double', 'int32', 'bool', ...),
|
||
|
|
'enum', or the fully qualified name of a message ('rv.data.UUID').
|
||
|
|
"""
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import re
|
||
|
|
import sys
|
||
|
|
|
||
|
|
SCALARS = {"double", "float", "int32", "int64", "uint32", "uint64", "sint32", "sint64",
|
||
|
|
"fixed32", "fixed64", "sfixed32", "sfixed64", "bool", "string", "bytes"}
|
||
|
|
|
||
|
|
|
||
|
|
def tokenize(src):
|
||
|
|
src = re.sub(r"//[^\n]*", "", src)
|
||
|
|
src = re.sub(r"/\*.*?\*/", "", src, flags=re.S)
|
||
|
|
return re.findall(r'"(?:[^"\\]|\\.)*"|[A-Za-z_][\w.]*|\.[A-Za-z_][\w.]*|-?\d+(?:\.\d+)?|[{}\[\]();=<>,]', src)
|
||
|
|
|
||
|
|
|
||
|
|
def parse_file(path, messages, enums):
|
||
|
|
toks = tokenize(open(path).read())
|
||
|
|
pkg = ""
|
||
|
|
i = 0
|
||
|
|
scope = [] # stack of (kind, fqname)
|
||
|
|
pending = [] # fields with unresolved types: (msg_fq, num, name, rawtype, repeated, scope_names)
|
||
|
|
|
||
|
|
def cur_msg():
|
||
|
|
for kind, fq in reversed(scope):
|
||
|
|
if kind == "message":
|
||
|
|
return fq
|
||
|
|
return None
|
||
|
|
|
||
|
|
while i < len(toks):
|
||
|
|
t = toks[i]
|
||
|
|
if t == "package":
|
||
|
|
pkg = toks[i + 1]
|
||
|
|
i += 3
|
||
|
|
continue
|
||
|
|
if t in ("syntax", "import", "option"):
|
||
|
|
while toks[i] != ";":
|
||
|
|
i += 1
|
||
|
|
i += 1
|
||
|
|
continue
|
||
|
|
if t in ("message", "enum", "oneof", "service"):
|
||
|
|
name = toks[i + 1]
|
||
|
|
parent = scope[-1][1] if scope else pkg
|
||
|
|
fq = parent + "." + name if parent else name
|
||
|
|
if t == "oneof":
|
||
|
|
fq = scope[-1][1] # fields belong to the enclosing message
|
||
|
|
scope.append((t, fq))
|
||
|
|
if t == "message":
|
||
|
|
messages.setdefault(fq, {})
|
||
|
|
elif t == "enum":
|
||
|
|
enums.add(fq)
|
||
|
|
i += 3 # name {
|
||
|
|
continue
|
||
|
|
if t == "}":
|
||
|
|
scope.pop()
|
||
|
|
i += 1
|
||
|
|
continue
|
||
|
|
if t == "reserved" or t == "extensions":
|
||
|
|
while toks[i] != ";":
|
||
|
|
i += 1
|
||
|
|
i += 1
|
||
|
|
continue
|
||
|
|
if scope and scope[-1][0] == "enum":
|
||
|
|
while toks[i] != ";":
|
||
|
|
i += 1
|
||
|
|
i += 1
|
||
|
|
continue
|
||
|
|
if scope and scope[-1][0] in ("message", "oneof"):
|
||
|
|
repeated = False
|
||
|
|
if t in ("repeated", "optional", "required"):
|
||
|
|
repeated = t == "repeated"
|
||
|
|
i += 1
|
||
|
|
t = toks[i]
|
||
|
|
if t == "map":
|
||
|
|
# map<k, v> name = n;
|
||
|
|
j = toks.index(";", i)
|
||
|
|
name = toks[j - 3]
|
||
|
|
num = int(toks[j - 1]) if toks[j - 1] != "]" else None
|
||
|
|
vt = toks[i + 4]
|
||
|
|
pending.append((cur_msg(), int(toks[toks.index("=", i) + 1]), name, "map:" + vt, True,
|
||
|
|
[fq for _, fq in scope]))
|
||
|
|
i = j + 1
|
||
|
|
continue
|
||
|
|
rawtype = t
|
||
|
|
name = toks[i + 1]
|
||
|
|
num = int(toks[i + 3])
|
||
|
|
j = i + 4
|
||
|
|
if toks[j] == "[":
|
||
|
|
while toks[j] != "]":
|
||
|
|
j += 1
|
||
|
|
j += 1
|
||
|
|
pending.append((cur_msg(), num, name, rawtype, repeated, [fq for _, fq in scope]))
|
||
|
|
i = j + 1
|
||
|
|
continue
|
||
|
|
i += 1
|
||
|
|
return pending
|
||
|
|
|
||
|
|
|
||
|
|
def resolve(rawtype, scopes, messages, enums):
|
||
|
|
if rawtype in SCALARS:
|
||
|
|
return rawtype
|
||
|
|
if rawtype.startswith("."):
|
||
|
|
cand = rawtype[1:]
|
||
|
|
if cand in messages:
|
||
|
|
return cand
|
||
|
|
if cand in enums:
|
||
|
|
return "enum"
|
||
|
|
raise KeyError(rawtype)
|
||
|
|
# search enclosing scopes from innermost outwards
|
||
|
|
for sc in reversed(scopes):
|
||
|
|
parts = sc.split(".")
|
||
|
|
for k in range(len(parts), -1, -1):
|
||
|
|
cand = ".".join(parts[:k] + [rawtype]) if k else rawtype
|
||
|
|
if cand in messages:
|
||
|
|
return cand
|
||
|
|
if cand in enums:
|
||
|
|
return "enum"
|
||
|
|
raise KeyError(rawtype)
|
||
|
|
|
||
|
|
|
||
|
|
def build(proto_dir):
|
||
|
|
messages, enums, pending = {}, set(), []
|
||
|
|
for fn in sorted(os.listdir(proto_dir)):
|
||
|
|
if fn.endswith(".proto"):
|
||
|
|
pending += parse_file(os.path.join(proto_dir, fn), messages, enums)
|
||
|
|
for gdir in ("google/protobuf",):
|
||
|
|
d = os.path.join(proto_dir, gdir)
|
||
|
|
if os.path.isdir(d):
|
||
|
|
for fn in sorted(os.listdir(d)):
|
||
|
|
if fn.endswith(".proto"):
|
||
|
|
pending += parse_file(os.path.join(d, fn), messages, enums)
|
||
|
|
for msg, num, name, rawtype, rep, scopes in pending:
|
||
|
|
if rawtype.startswith("map:"):
|
||
|
|
messages[msg][num] = [name, "map", True]
|
||
|
|
continue
|
||
|
|
messages[msg][num] = [name, resolve(rawtype, scopes, messages, enums), rep]
|
||
|
|
return messages
|
||
|
|
|
||
|
|
|
||
|
|
def reachable(messages, root):
|
||
|
|
out, todo = {}, [root]
|
||
|
|
while todo:
|
||
|
|
m = todo.pop()
|
||
|
|
if m in out:
|
||
|
|
continue
|
||
|
|
out[m] = messages[m]
|
||
|
|
for name, typ, rep in messages[m].values():
|
||
|
|
if typ in messages and typ not in out:
|
||
|
|
todo.append(typ)
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
msgs = build(sys.argv[1])
|
||
|
|
sub = reachable(msgs, "rv.data.Presentation")
|
||
|
|
sub.update(reachable(msgs, "rv.data.Template.Document"))
|
||
|
|
sub.update(reachable(msgs, "rv.data.PlaylistDocument"))
|
||
|
|
json.dump({k: {str(n): v for n, v in f.items()} for k, f in sorted(sub.items())},
|
||
|
|
open(sys.argv[2], "w"), separators=(",", ":"), sort_keys=True)
|
||
|
|
print(len(msgs), "messages parsed;", len(sub), "reachable from Presentation + Template.Document")
|