93 lines
3.2 KiB
Python
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')
|