Files
GaCy-GO/tools/inspect_luau_v4.py
2026-10-28 18:54:43 +07:00

93 lines
3.2 KiB
Python

"""Read version-4 Luau chunks without executing them.
Layout: upstream Luau VM/src/lvmload.cpp, Common/include/Luau/Bytecode.h.
Only v4/type1 is accepted; later/custom layouts fail instead of guessing.
"""
import argparse
import json
import struct
from pathlib import Path
def inspect(path):
data = Path(path).read_bytes()
offset = 0
def read(size):
nonlocal offset
value = data[offset:offset + size]
if len(value) != size:
raise ValueError('truncated chunk')
offset += size
return value
def byte():
return read(1)[0]
def integer():
value, shift = 0, 0
while True:
v = byte()
value |= (v & 127) << shift
if v < 128:
return value
shift += 7
if shift > 28:
raise ValueError('oversized integer')
if (byte(), byte()) != (4, 1):
raise ValueError('only Luau v4/type1 supported')
strings = [None] + [read(integer()).decode('utf-8', 'replace') for _ in range(integer())]
protos = []
for index in range(integer()):
stack, params, upvalues, vararg, flags = [byte() for _ in range(5)]
read(integer())
code = [struct.unpack('<I', read(4))[0] for _ in range(integer())]
constants = []
for _ in range(integer()):
kind = byte()
if kind == 0:
value = None
elif kind == 1:
value = bool(byte())
elif kind == 2:
value = struct.unpack('<d', read(8))[0]
elif kind == 3:
value = strings[integer()]
elif kind == 4:
value = {'import': struct.unpack('<I', read(4))[0]}
elif kind == 5:
value = {'table_keys': [integer() for _ in range(integer())]}
elif kind == 6:
value = {'closure': integer()}
else:
raise ValueError(f'unsupported constant {kind}')
constants.append(value)
children = [integer() for _ in range(integer())]
line, name = integer(), strings[integer()]
if byte():
gap = byte()
read(len(code))
read((((len(code) - 1) >> gap) + 1) * 4)
locals_ = []
ups = []
if byte():
for _ in range(integer()):
locals_.append([strings[integer()], integer(), integer(), byte()])
ups = [strings[integer()] for _ in range(integer())]
protos.append(dict(index=index, name=name, line=line, stack=stack,
params=params, upvalues=ups, children=children,
constants=constants, code=code, locals=locals_))
main = integer()
if offset != len(data):
raise ValueError(f'unconsumed bytes: {len(data) - offset}')
return dict(main=main, strings=strings, protos=protos)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('path')
parser.add_argument('--output', required=True)
args = parser.parse_args()
Path(args.output).write_text(json.dumps(inspect(args.path), indent=2, ensure_ascii=False), encoding='utf-8')