Skip to content

Instantly share code, notes, and snippets.

@skylord123
Created June 1, 2026 18:44
Show Gist options
  • Select an option

  • Save skylord123/6fa28f1ce6633213fea0d45d75cda4ba to your computer and use it in GitHub Desktop.

Select an option

Save skylord123/6fa28f1ce6633213fea0d45d75cda4ba to your computer and use it in GitHub Desktop.
AgeOfTime DSO decompiler

dso-decompiler

A decompiler for Age of Time .dso files — the compiled TorqueScript bytecode the AgeOfTime engine executes (compiled from .cs / .gui source).

Point it at a .dso and it reconstructs a .cs source file that recompiles to equivalent bytecode and behaves the same as the original. It handles functions, control flow (if/else, while/for, do/while, break/continue, ternaries, &&/||), expressions and string building, and new / datablock object declarations with nested children.

AoT uses DSO bytecode version 33 (a custom TGE 1.x fork). For the binary format and the opcode table, see DSO-FORMAT.md.

Requirements

Python 3 only — no third-party packages. The logic is deliberately kept dependency-free and straightforward.

Usage

# write FILE.cs next to the dso  (won't overwrite an existing file; use --force)
python3 dsodecomp.py path/to/FILE.cs.dso

# write to a specific path
python3 dsodecomp.py FILE.cs.dso -o /tmp/out.cs

# print the reconstructed source straight to the terminal
python3 dsodecomp.py FILE.cs.dso --stdout

# raw disassembly instead of source (debugging aid)
python3 dsodecomp.py FILE.cs.dso --disasm --stdout

# recurse a directory, decompiling every *.dso it finds
python3 dsodecomp.py ../AgeOfTime/base/skylord
flag meaning
-o, --output PATH write to PATH (single file only)
--stdout print to terminal instead of writing a file
--disasm emit annotated bytecode disassembly, not source
-f, --force allow overwriting an existing output file
--quiet suppress the "wrote ..." line

Note on the default output name: a foo.cs.dso decompiles to foo.cs. If a foo.cs already exists (the original source), the tool refuses to clobber it unless you pass --force or redirect with -o / --stdout.

What you get, and what you don't

The bytecode is a faithful encoding of program behaviour, so the reconstructed source is behaviourally equivalent and recompilable. A few surface details are erased at compile time and can't be recovered exactly: comments and whitespace, the exact spelling of number literals, true/false (folded to 1/0), identifier case (function/variable names take their first-seen casing), and for-vs-while sugar (a for comes back as a while with the increment as the loop body's last statement). See the end of DSO-FORMAT.md for details.

Files

File Purpose
dsodecomp.py The loader, disassembler and decompiler (run this).
DSO-FORMAT.md Reverse-engineered container + opcode spec.
README.md This file.

Status

Decompiles all 184 .dso files shipped with Age of Time without errors and with balanced output (no unresolved opcodes, no unhandled control flow).

Age of Time .dso format (DSO bytecode version 33)

A .dso file is the compiled form of a TorqueScript (.cs / .gui) source file: the bytecode the Torque Game Engine console VM executes. Age of Time runs a custom fork of TGE 1.x whose DSO version is 33 (the first u32 of every file is 0x21). Stock TGE 1.4 is version 36; the container layout and the opcode numbering both differ from stock, so a stock-TGE decompiler will not read these files correctly.

This spec was reverse-engineered against the TGE 1.4 source (the engine's console/codeBlock.cc, compiler.cc, compiledEval.cc, astNodes.cc, stringStack.h) and validated by disassembling all 184 shipped .dso files cleanly (every string-table reference resolves, every jump lands on an instruction boundary, every file is consumed to the last byte).

Container

All integers are little-endian. u32 = 4 bytes, f64 = IEEE-754 double.

u32   version                     // 33
u32   globalStringSize            // bytes
byte  globalStrings[globalStringSize]
u32   globalFloatCount
f64   globalFloats[globalFloatCount]
u32   functionStringSize
byte  functionStrings[functionStringSize]
u32   functionFloatCount
f64   functionFloats[functionFloatCount]
u32   codeSize                    // number of code "words"
u32   lineBreakPairCount
... code stream (see below) ...
u32   lineBreaks[lineBreakPairCount * 2]
... ident table (see below) ...

Difference from stock v36: stock orders the four tables globalStr, functionStr, globalFloat, functionFloat. v33 groups them global-then-function: globalStr, globalFloat, functionStr, functionFloat.

String tables

Each string table is a flat blob of NUL-terminated strings, packed end to end. A string is referenced by its byte offset into the blob. Top-level code uses the global tables; code inside a function body uses the function tables.

Float tables

A simple array of f64, referenced by index. Global vs function as above.

Code stream

codeSize "words", each a u32, stored with a one-byte compression:

read u8 b
if b == 0xFF:  word = next u32
else:          word = b

The first word of an instruction is the opcode; the rest are operands (see the opcode table). Jump operands are absolute word indices into the code stream.

Line-break pairs

lineBreakPairCount * 2 raw u32s (debugger line-number ↔ ip info). Not needed to decompile; skipped.

Ident table

u32 identCount
repeat identCount:
    u32 offset            // into the GLOBAL string table
    u32 ipCount
    u32 ips[ipCount]      // code positions to patch

StringTableEntry operands (variable names, function names, namespaces, field names, the bareword pushed by OP_LOADIMMED_IDENT, object parent names, function argument names) are written into the code stream as 0 and resolved here: for each table entry, the string at offset is the value of code[ip] for every listed ip. Empty STEs are not added to the table — an empty namespace, package, or parent name simply stays 0 with no ident entry.

Opcodes

The VM opcode enum is the stock TGE enum with three custom Age-of-Time opcodes inserted (never emitted by the script compiler, so they never appear in real bytecode), at values 2, 3 and 72. The full ordered table (value → name):

# opcode # opcode # opcode
0 FUNC_DECL 28 SHL 56 STR_TO_UINT
1 CREATE_OBJECT 29 AND 57 STR_TO_FLT
2 UNUSED_2 30 OR 58 STR_TO_NONE
3 UNUSED_3 31 ADD 59 FLT_TO_UINT
4 ADD_OBJECT 32 SUB 60 FLT_TO_STR
5 END_OBJECT 33 MUL 61 FLT_TO_NONE
6 JMPIFFNOT 34 DIV 62 UINT_TO_FLT
7 JMPIFNOT 35 NEG 63 UINT_TO_STR
8 JMPIFF 36 SETCURVAR 64 UINT_TO_NONE
9 JMPIF 37 SETCURVAR_CREATE 65 LOADIMMED_UINT
10 JMPIFNOT_NP 38 SETCURVAR_ARRAY 66 LOADIMMED_FLT
11 JMPIF_NP 39 SETCURVAR_ARRAY_CREATE 67 TAG_TO_STR
12 JMP 40 LOADVAR_UINT 68 LOADIMMED_STR
13 RETURN 41 LOADVAR_FLT 69 LOADIMMED_IDENT
14 CMPEQ 42 LOADVAR_STR 70 CALLFUNC_RESOLVE
15 CMPGR 43 SAVEVAR_UINT 71 CALLFUNC
16 CMPGE 44 SAVEVAR_FLT 72 UNUSED_72
17 CMPLT 45 SAVEVAR_STR 73 ADVANCE_STR
18 CMPLE 46 SETCUROBJECT 74 ADVANCE_STR_APPENDCHAR
19 CMPNE 47 SETCUROBJECT_NEW 75 ADVANCE_STR_COMMA
20 XOR 48 SETCURFIELD 76 ADVANCE_STR_NUL
21 MOD 49 SETCURFIELD_ARRAY 77 REWIND_STR
22 BITAND 50 LOADFIELD_UINT 78 TERMINATE_REWIND_STR
23 BITOR 51 LOADFIELD_FLT 79 COMPARE_STR
24 NOT 52 LOADFIELD_STR 80 PUSH
25 NOTF 53 SAVEFIELD_UINT 81 PUSH_FRAME
26 ONESCOMPLEMENT 54 SAVEFIELD_FLT 82 BREAK
27 SHR 55 SAVEFIELD_STR 83 INVALID

(The OPCODES array in dsodecomp.py is the authoritative source; this table is generated from it.)

Mapping rule from a stock-TGE opcode index N to its v33 value: N if N <= 1; N + 2 if 2 <= N <= 69; N + 3 if N >= 70.

Operand counts

Fixed operands after the opcode word:

  • FUNC_DECL — variable: name, namespace, package, hasBody, endIp, argc, then argc argument-name STEs (6 + argc words).
  • CREATE_OBJECTparentName(STE), isDatablock, failIp (3).
  • CALLFUNC / CALLFUNC_RESOLVEfuncName(STE), namespace(STE), callType (3).
  • all conditional/unconditional jumps — target (1).
  • SETCURVAR, SETCURVAR_CREATE, SETCURFIELD, LOADIMMED_IDENT, LOADIMMED_UINT, LOADIMMED_FLT, TAG_TO_STR, LOADIMMED_STR — 1.
  • ADVANCE_STR_APPENDCHARchar (1). Easy to miss; omitting it desyncs a linear sweep.
  • ADD_OBJECT, END_OBJECTplaceAtRoot (1).
  • everything else — 0.

callType: 0 = FunctionCall, 1 = MethodCall (first pushed arg is the object), 2 = ParentCall (Parent::).

How decompilation works

The VM is a stack machine with three logical stacks — a string stack (STR, with concatenation/argument machinery), a float stack and an integer stack — plus "current" registers (current variable / object / field). dsodecomp.py symbolically executes the bytecode over those same stacks, building source-text fragments instead of values, and recovers if/else, while/for, do/while, break/continue, ternaries and &&/|| from the jump targets.

What is and isn't recoverable

The bytecode is a faithful, lossless encoding of the program's behaviour, but some surface details of the original source are erased at compile time and cannot be recovered exactly:

  • Comments and whitespace — gone.
  • Numeric literal spelling0x10, 1.0, 16 all become a canonical form (e.g. 16, 1). Numbers used in string position are stored as their decimal text in the string table.
  • true / false — the AoT lexer folds these to 1 / 0.
  • Identifier case — the ident table is case-insensitive, so function and variable names take the casing of their first occurrence (strLenstrlen).
  • for vs while — a for(init; c; incr) is reconstructed as init; then a while(c) with incr; as the loop body's last statement. Behaviour is identical and it recompiles to equivalent bytecode.

Everything that affects behaviour — control flow, calls, operators, string building, object/datablock declarations with nested children — round-trips.

#!/usr/bin/env python3
"""
dsodecomp.py - decompiler for Age of Time / Torque Game Engine ".dso" files.
A .dso is the compiled bytecode the TGE console (TorqueScript) VM executes. This
tool reads a .dso, disassembles its bytecode, and reconstructs a ".cs" source
file that compiles back to equivalent bytecode and behaves the same as the .dso.
Age of Time uses DSO bytecode **version 33** (a custom TGE 1.x fork). See
DSO-FORMAT.md for the reverse-engineered container + opcode spec.
Usage:
python3 dsodecomp.py FILE.cs.dso # write FILE.cs next to the dso
python3 dsodecomp.py FILE.cs.dso -o OUT.cs # write to a specific path
python3 dsodecomp.py FILE.cs.dso --stdout # print source to the terminal
python3 dsodecomp.py FILE.cs.dso --disasm # print raw disassembly (debug)
python3 dsodecomp.py DIR # recurse, decompile every *.dso
No third-party dependencies - standard library only. The logic is kept simple
and dependency-free so it can later be ported to JavaScript for an in-browser
decompiler on the wiki.
"""
import argparse
import os
import struct
import sys
DSO_VERSION = 33
STRING_TAG_PREFIX_BYTE = 0x01
# ---------------------------------------------------------------------------
# Opcodes (TorqueScript VM). Order == numeric value.
#
# This is AoT's DSO v33 layout, which differs from stock TGE 1.4 (v36): three
# extra opcodes (never emitted by the script compiler) are present, so every
# stock opcode is shifted up. They were located empirically: an enum that puts
# them at values 2, 3 and 72 disassembles all 184 shipped .dso files cleanly
# (every StringTableEntry operand lands on an ident-table entry, every jump
# lands on an instruction boundary). Stock opcode N maps to: N (N<=1),
# N+2 (2<=N<=69), N+3 (N>=70). See DSO-FORMAT.md.
# ---------------------------------------------------------------------------
OPCODES = [
"OP_FUNC_DECL",
"OP_CREATE_OBJECT",
"OP_UNUSED_2", # custom AoT opcode, never emitted by the compiler
"OP_UNUSED_3", # custom AoT opcode, never emitted by the compiler
"OP_ADD_OBJECT",
"OP_END_OBJECT",
"OP_JMPIFFNOT",
"OP_JMPIFNOT",
"OP_JMPIFF",
"OP_JMPIF",
"OP_JMPIFNOT_NP",
"OP_JMPIF_NP",
"OP_JMP",
"OP_RETURN",
"OP_CMPEQ",
"OP_CMPGR",
"OP_CMPGE",
"OP_CMPLT",
"OP_CMPLE",
"OP_CMPNE",
"OP_XOR",
"OP_MOD",
"OP_BITAND",
"OP_BITOR",
"OP_NOT",
"OP_NOTF",
"OP_ONESCOMPLEMENT",
"OP_SHR",
"OP_SHL",
"OP_AND",
"OP_OR",
"OP_ADD",
"OP_SUB",
"OP_MUL",
"OP_DIV",
"OP_NEG",
"OP_SETCURVAR",
"OP_SETCURVAR_CREATE",
"OP_SETCURVAR_ARRAY",
"OP_SETCURVAR_ARRAY_CREATE",
"OP_LOADVAR_UINT",
"OP_LOADVAR_FLT",
"OP_LOADVAR_STR",
"OP_SAVEVAR_UINT",
"OP_SAVEVAR_FLT",
"OP_SAVEVAR_STR",
"OP_SETCUROBJECT",
"OP_SETCUROBJECT_NEW",
"OP_SETCURFIELD",
"OP_SETCURFIELD_ARRAY",
"OP_LOADFIELD_UINT",
"OP_LOADFIELD_FLT",
"OP_LOADFIELD_STR",
"OP_SAVEFIELD_UINT",
"OP_SAVEFIELD_FLT",
"OP_SAVEFIELD_STR",
"OP_STR_TO_UINT",
"OP_STR_TO_FLT",
"OP_STR_TO_NONE",
"OP_FLT_TO_UINT",
"OP_FLT_TO_STR",
"OP_FLT_TO_NONE",
"OP_UINT_TO_FLT",
"OP_UINT_TO_STR",
"OP_UINT_TO_NONE",
"OP_LOADIMMED_UINT",
"OP_LOADIMMED_FLT",
"OP_TAG_TO_STR",
"OP_LOADIMMED_STR",
"OP_LOADIMMED_IDENT",
"OP_CALLFUNC_RESOLVE",
"OP_CALLFUNC",
"OP_UNUSED_72", # custom AoT opcode, never emitted by the compiler
"OP_ADVANCE_STR",
"OP_ADVANCE_STR_APPENDCHAR",
"OP_ADVANCE_STR_COMMA",
"OP_ADVANCE_STR_NUL",
"OP_REWIND_STR",
"OP_TERMINATE_REWIND_STR",
"OP_COMPARE_STR",
"OP_PUSH",
"OP_PUSH_FRAME",
"OP_BREAK",
"OP_INVALID",
]
OP = {name: i for i, name in enumerate(OPCODES)}
# Number of fixed code words consumed AFTER the opcode word. OP_FUNC_DECL is
# variable-length (6 + argc) and handled specially in the disassembler.
FIXED_OPERANDS = {
"OP_CREATE_OBJECT": 3,
"OP_ADD_OBJECT": 1,
"OP_END_OBJECT": 1,
"OP_JMPIFFNOT": 1, "OP_JMPIFNOT": 1, "OP_JMPIFF": 1, "OP_JMPIF": 1,
"OP_JMPIFNOT_NP": 1, "OP_JMPIF_NP": 1, "OP_JMP": 1,
"OP_SETCURVAR": 1, "OP_SETCURVAR_CREATE": 1,
"OP_SETCURFIELD": 1,
"OP_LOADIMMED_UINT": 1, "OP_LOADIMMED_FLT": 1,
"OP_TAG_TO_STR": 1, "OP_LOADIMMED_STR": 1, "OP_LOADIMMED_IDENT": 1,
"OP_CALLFUNC_RESOLVE": 3, "OP_CALLFUNC": 3,
"OP_ADVANCE_STR_APPENDCHAR": 1,
}
# Which operand words are StringTableEntry references (resolved through the
# ident table, not the inline string table). Index is the operand position
# AFTER the opcode word.
STE_OPERANDS = {
"OP_CREATE_OBJECT": [0], # parent object name
"OP_SETCURVAR": [0],
"OP_SETCURVAR_CREATE": [0],
"OP_SETCURFIELD": [0],
"OP_LOADIMMED_IDENT": [0],
"OP_CALLFUNC": [0, 1], # funcName, namespace
"OP_CALLFUNC_RESOLVE": [0, 1],
}
CALL_FUNCTION = 0
CALL_METHOD = 1
CALL_PARENT = 2
# ---------------------------------------------------------------------------
# Binary stream reader
# ---------------------------------------------------------------------------
class Reader:
def __init__(self, data):
self.data = data
self.pos = 0
def u8(self):
v = self.data[self.pos]
self.pos += 1
return v
def u32(self):
v = struct.unpack_from("<I", self.data, self.pos)[0]
self.pos += 4
return v
def f64(self):
v = struct.unpack_from("<d", self.data, self.pos)[0]
self.pos += 8
return v
def bytes(self, n):
v = self.data[self.pos:self.pos + n]
self.pos += n
return v
# ---------------------------------------------------------------------------
# DSO container
# ---------------------------------------------------------------------------
class StringTable:
"""A packed blob of NUL-terminated strings, indexed by byte offset."""
def __init__(self, blob):
self.blob = blob
def get(self, offset):
end = self.blob.find(b"\x00", offset)
if end < 0:
end = len(self.blob)
return self.blob[offset:end]
def get_str(self, offset):
return self.get(offset).decode("latin-1")
class CodeBlock:
def __init__(self):
self.version = 0
self.global_strings = StringTable(b"")
self.function_strings = StringTable(b"")
self.global_floats = []
self.function_floats = []
self.code = [] # list of U32
self.code_size = 0
self.line_break_pairs = []
# ip -> StringTableEntry (decoded string) from the ident table
self.idents = {}
@classmethod
def load(cls, data):
cb = cls()
r = Reader(data)
cb.version = r.u32()
# NOTE: v33 groups the tables global-then-function:
# globalStrings, globalFloats, functionStrings, functionFloats
# (TGE 1.4 / v36 instead interleaves them str,str,flt,flt.)
size = r.u32()
global_size = size
cb.global_strings = StringTable(r.bytes(size) if size else b"")
size = r.u32()
cb.global_floats = [r.f64() for _ in range(size)]
size = r.u32()
cb.function_strings = StringTable(r.bytes(size) if size else b"")
size = r.u32()
cb.function_floats = [r.f64() for _ in range(size)]
code_size = r.u32()
line_break_pair_count = r.u32()
cb.code_size = code_size
code = [0] * (code_size + line_break_pair_count * 2)
for i in range(code_size):
b = r.u8()
code[i] = r.u32() if b == 0xFF else b
for i in range(code_size, code_size + line_break_pair_count * 2):
code[i] = r.u32()
cb.code = code
cb.line_break_pairs = code[code_size:]
# Ident table: patch in StringTableEntry references.
ident_count = r.u32()
for _ in range(ident_count):
offset = r.u32()
ste = (cb.global_strings.get_str(offset)
if offset < global_size else "")
count = r.u32()
for _ in range(count):
ip = r.u32()
cb.idents[ip] = ste
cb.trailing = len(data) - r.pos
return cb
# ---------------------------------------------------------------------------
# Disassembler
# ---------------------------------------------------------------------------
class Instr:
__slots__ = ("ip", "op", "operands")
def __init__(self, ip, op, operands):
self.ip = ip # code index of the opcode word
self.op = op # opcode name
self.operands = operands # list of raw U32 operand words
def __repr__(self):
return "%-6d %-26s %s" % (self.ip, self.op, self.operands)
def disassemble(cb):
"""Return (instrs, by_ip) for the executable code region."""
instrs = []
by_ip = {}
code = cb.code
ip = 0
n = cb.code_size
while ip < n:
opcode = code[ip]
if opcode >= len(OPCODES):
raise ValueError("bad opcode %d at ip %d" % (opcode, ip))
name = OPCODES[opcode]
start = ip
ip += 1
if name == "OP_FUNC_DECL":
# name, ns, package, hasBody, endip, argc, then argc arg-name STEs
fixed = code[ip:ip + 6]
argc = code[ip + 5]
operands = code[ip:ip + 6 + argc]
ip += 6 + argc
else:
k = FIXED_OPERANDS.get(name, 0)
operands = code[ip:ip + k]
ip += k
instr = Instr(start, name, operands)
instrs.append(instr)
by_ip[start] = instr
return instrs, by_ip
# ---------------------------------------------------------------------------
# Disassembly pretty-printer (debug aid)
# ---------------------------------------------------------------------------
def format_disasm(cb):
instrs, _ = disassemble(cb)
# figure out which code region (global vs function) each ip is in
fn_ranges = []
for ins in instrs:
if ins.op == "OP_FUNC_DECL":
end = ins.operands[4]
fn_ranges.append((ins.ip, end))
def in_fn(ip):
return any(a <= ip < b for a, b in fn_ranges)
lines = []
lines.append("; DSO version %d, code_size=%d, %d global strings bytes, "
"%d func string bytes, %d/%d floats"
% (cb.version, cb.code_size, len(cb.global_strings.blob),
len(cb.function_strings.blob),
len(cb.global_floats), len(cb.function_floats)))
for ins in instrs:
ann = annotate(cb, ins, in_fn(ins.ip))
lines.append("%-7d %-26s %-22s %s"
% (ins.ip, ins.op,
" ".join(str(o) for o in ins.operands), ann))
return "\n".join(lines)
def annotate(cb, ins, in_fn):
strings = cb.function_strings if in_fn else cb.global_strings
floats = cb.function_floats if in_fn else cb.global_floats
op = ins.op
o = ins.operands
if op in ("OP_SETCURVAR", "OP_SETCURVAR_CREATE", "OP_SETCURFIELD",
"OP_LOADIMMED_IDENT", "OP_CREATE_OBJECT"):
return "; %r" % cb.idents.get(ins.ip + 1, "")
if op in ("OP_CALLFUNC", "OP_CALLFUNC_RESOLVE"):
fn = cb.idents.get(ins.ip + 1, "")
ns = cb.idents.get(ins.ip + 2, "")
ct = {0: "func", 1: "method", 2: "parent"}.get(o[2], o[2])
return "; %s%s%s [%s]" % (ns, "::" if ns else "", fn, ct)
if op in ("OP_LOADIMMED_STR", "OP_TAG_TO_STR"):
return "; %r" % strings.get_str(o[0])
if op == "OP_LOADIMMED_FLT":
return "; %s" % (floats[o[0]] if o[0] < len(floats) else "?")
if op == "OP_ADVANCE_STR_APPENDCHAR":
return "; %r" % chr(o[0])
if op == "OP_FUNC_DECL":
fn = cb.idents.get(ins.ip + 1, "")
ns = cb.idents.get(ins.ip + 2, "")
args = [cb.idents.get(ins.ip + 7 + i, "") for i in range(o[5])]
return "; %s%s%s(%s) end=%d body=%d" % (
ns, "::" if ns else "", fn, ", ".join(args), o[4], o[3])
return ""
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def default_output_path(path, disasm):
if disasm:
return path + ".disasm"
# strip the trailing .dso: foo.cs.dso -> foo.cs
return path[:-4] if path.lower().endswith(".dso") else path + ".cs"
def decompile_file(path, out=None, to_stdout=False, disasm=False, quiet=False,
force=False):
with open(path, "rb") as fh:
data = fh.read()
cb = CodeBlock.load(data)
if cb.version != DSO_VERSION and not quiet:
print("warning: %s is DSO version %d (expected %d)"
% (path, cb.version, DSO_VERSION), file=sys.stderr)
text = format_disasm(cb) if disasm else decompile(cb)
if to_stdout:
print(text)
return
if out is None:
out = default_output_path(path, disasm)
# Don't silently clobber an existing file (e.g. the original .cs source
# sitting next to a foo.cs.dso) unless asked to.
if os.path.exists(out) and not force:
raise RuntimeError("refusing to overwrite existing %s (use --force, "
"-o OTHER, or --stdout)" % out)
with open(out, "w") as fh:
fh.write(text + "\n")
if not quiet:
print("wrote %s" % out)
# ===========================================================================
# Decompiler
#
# TorqueScript's VM is a stack machine. We reconstruct source by symbolically
# executing the bytecode over expression "stacks" (mirroring the engine's
# string / numeric stacks), and by recovering control flow from the jump
# targets. The compiler's exact emission patterns (engine astNodes.cc) are the
# inverse map we invert here.
# ===========================================================================
# Source-operator precedence (higher binds tighter). Used to add parens.
P_ATOM = 100
P_UNARY = 90
P_MULT = 80 # * / %
P_ADD = 70 # + -
P_SHIFT = 60 # << >>
P_REL = 50 # < > <= >=
P_EQ = 45 # == != $= !$=
P_BAND = 42; P_BXOR = 40; P_BOR = 38
P_AND = 30 # &&
P_OR = 25 # ||
P_TERNARY = 20 # ?:
P_CAT = 15 # @ SPC TAB NL
P_ASSIGN = 12 # = += ...
P_COMMA = 8 # , (array comma-cat)
class Expr:
__slots__ = ("t", "p", "ident", "array")
def __init__(self, text, prec=P_ATOM, ident=False, array=None):
self.t = text
self.p = prec
self.ident = ident # produced by OP_LOADIMMED_IDENT (bareword)
self.array = array # (base_text, index_expr) when this is an array var
def paren(self, min_prec):
return self.t if self.p >= min_prec else "(" + self.t + ")"
def __repr__(self):
return "Expr(%r)" % self.t
def fmt_float(v):
if v == int(v) and abs(v) < 1e15:
return str(int(v))
return repr(v)
def quote_str(s):
out = ['"']
for ch in s:
if ch == '"':
out.append('\\"')
elif ch == "\\":
out.append("\\\\")
elif ch == "\n":
out.append("\\n")
elif ch == "\t":
out.append("\\t")
elif ch == "\r":
out.append("\\r")
else:
out.append(ch)
out.append('"')
return "".join(out)
def quote_tagged(s):
return "'" + s.replace("'", "\\'") + "'"
class Scope:
"""Decompiles one code region (top-level or a single function body)."""
def __init__(self, cb, instrs, idx_of_ip, lo, hi, in_function):
self.cb = cb
self.I = instrs
self.idx_of_ip = idx_of_ip
self.lo = lo
self.hi = hi # exclusive instruction index
self.strings = cb.function_strings if in_function else cb.global_strings
self.floats = cb.function_floats if in_function else cb.global_floats
# symbolic stacks
self.nstack = [] # numeric (uint+float unified)
self.cur = None # current string value (Expr or None)
self.saved = [] # [(Expr_or_None, sep)] string start stack
self.frames = [] # [[arg Expr,...]] open call frames
self.cur_var = None
self.cur_obj = None # Expr or "NEW"
self.cur_field = None
self.cur_field_array = None
# control flow
self.loops = {} # guard/do-head idx -> loop info
self.do_heads = {}
self.break_ip = [] # stack of loop break ips
self.continue_ip = []
self._find_loops()
# -- helpers ----------------------------------------------------------
def ste(self, ip):
return self.cb.idents.get(ip, "")
def target_idx(self, ins):
return self.idx_of_ip[ins.operands[0]]
def _find_loops(self):
I = self.I
for j in range(self.lo, self.hi):
ins = I[j]
if ins.op in ("OP_JMPIF", "OP_JMPIFF"):
tgt = ins.operands[0]
if tgt < ins.ip: # backward => loop bottom
top_idx = self.idx_of_ip.get(tgt)
if top_idx is None:
continue
break_ip = ins.ip + 2
guard_idx = top_idx - 1
info = {"bottom": j, "L": top_idx, "break_ip": break_ip}
if (guard_idx >= self.lo
and I[guard_idx].op in ("OP_JMPIFNOT", "OP_JMPIFFNOT")
and I[guard_idx].operands[0] == break_ip):
info["kind"] = "while"
self.loops[guard_idx] = info
else:
info["kind"] = "do"
self.do_heads[top_idx] = info
# -- statement emission ----------------------------------------------
def decompile(self, base_indent=0):
lines = []
self._block(self.lo, self.hi, lines, base_indent)
return lines
def _emit(self, lines, indent, text):
lines.append(" " * indent + text)
def at_boundary(self):
return (not self.nstack and self.cur is None and not self.saved
and not self.frames)
def pop_value(self):
"""Pop the single pending value (numeric or string)."""
if self.cur is not None:
v = self.cur
self.cur = None
return v
if self.nstack:
return self.nstack.pop()
return Expr("")
# -- core block walker ------------------------------------------------
def _block(self, lo, hi, lines, indent):
i = lo
while i < hi:
ins = self.I[i]
op = ins.op
# implicit trailing RETURN of the whole scope: drop it
if op == "OP_RETURN" and i == self.hi - 1 and self.at_boundary():
i += 1
continue
if i in self.loops: # while / for head
i = self._do_while_loop(i, lines, indent)
continue
if i in self.do_heads: # do { } while
i = self._do_do_loop(i, lines, indent)
continue
if op in ("OP_JMPIFNOT", "OP_JMPIFFNOT"):
i = self._do_if(i, lines, indent)
continue
if op == "OP_JMP":
tgt = ins.operands[0]
if self.break_ip and tgt == self.break_ip[-1]:
self._emit(lines, indent, "break;")
elif self.continue_ip and tgt == self.continue_ip[-1]:
self._emit(lines, indent, "continue;")
# else: an internal jump we don't model; ignore
i += 1
continue
if op == "OP_RETURN":
val = self.pop_value()
if val is not None and val.t != "":
self._emit(lines, indent, "return %s;" % val.t)
else:
self._emit(lines, indent, "return;")
i += 1
continue
# otherwise a normal value/statement instruction
i = self._step(i, lines, indent)
return i
def _do_if(self, i, lines, indent):
ins = self.I[i]
cond = self.pop_value()
tdx = self.target_idx(ins)
# else? instruction just before the target is JMP -> E (E > target).
# But a trailing JMP that targets an enclosing loop's break/continue
# point is a break/continue inside the if-block, not an else-skip.
prev = self.I[tdx - 1]
loop_targets = set(self.break_ip) | set(self.continue_ip)
if (prev.op == "OP_JMP" and prev.operands[0] > ins.operands[0]
and prev.operands[0] not in loop_targets):
edx = self.idx_of_ip[prev.operands[0]]
true_lo, true_hi = i + 1, tdx - 1
false_lo, false_hi = tdx, edx
# ternary? true branch yields a value (no statements)
sub = []
save = self._snapshot()
self._block(true_lo, true_hi, sub, indent + 1)
if not sub and not self.at_boundary():
# ternary expression; the branch value lives in the string
# register (cur) or on the numeric stack - keep it there.
string_typed = self.cur is not None
tval = self.pop_value()
self._block(false_lo, false_hi, [], indent + 1)
fval = self.pop_value()
self._restore(save)
tern = Expr("%s ? %s : %s" % (cond.paren(P_TERNARY + 1),
tval.paren(P_TERNARY + 1),
fval.paren(P_TERNARY)), P_TERNARY)
if string_typed:
self.cur = tern
else:
self.nstack.append(tern)
return edx
# if / else statement
self._emit(lines, indent, "if (%s)" % cond.t)
self._emit(lines, indent, "{")
lines.extend(sub)
self._emit(lines, indent, "}")
self._emit(lines, indent, "else")
self._emit(lines, indent, "{")
self._block(false_lo, false_hi, lines, indent + 1)
self._emit(lines, indent, "}")
return edx
else:
# plain if (no else); could still be ternary w/o else (rare) -> treat as if
sub = []
self._block(i + 1, tdx, sub, indent + 1)
self._emit(lines, indent, "if (%s)" % cond.t)
self._emit(lines, indent, "{")
lines.extend(sub)
self._emit(lines, indent, "}")
return tdx
def _do_while_loop(self, i, lines, indent):
info = self.loops[i]
cond = self.pop_value()
L = info["L"]
bottom = info["bottom"]
break_ip = info["break_ip"]
# The continue point is where the re-test (a byte-for-byte copy of the
# loop test) begins; for a `for` loop the increment sits just before
# it. Find it by matching the test opcodes backward from the guard and
# from the bottom jump.
j1, j2 = i - 1, bottom - 1
while j1 >= L and j2 >= L and self.I[j1].op == self.I[j2].op:
j1 -= 1
j2 -= 1
cont_ip = self.I[j2 + 1].ip if j2 + 1 <= bottom else self.I[bottom].ip
self.break_ip.append(break_ip)
self.continue_ip.append(cont_ip)
body = []
self._block(L, bottom, body, indent + 1)
# the bottom retest leaves a dangling value; discard it
self.pop_value()
self.break_ip.pop()
self.continue_ip.pop()
self._emit(lines, indent, "while (%s)" % cond.t)
self._emit(lines, indent, "{")
lines.extend(body)
self._emit(lines, indent, "}")
return self.idx_of_ip[break_ip]
def _do_do_loop(self, i, lines, indent):
info = self.do_heads[i]
bottom = info["bottom"]
break_ip = info["break_ip"]
self.break_ip.append(break_ip)
self.continue_ip.append(self.I[i].ip)
body = []
self._block(i, bottom, body, indent + 1)
cond = self.pop_value() # trailing retest value == loop condition
self.break_ip.pop()
self.continue_ip.pop()
self._emit(lines, indent, "do")
self._emit(lines, indent, "{")
lines.extend(body)
self._emit(lines, indent, "} while (%s);" % cond.t)
return self.idx_of_ip[break_ip]
def _snapshot(self):
return (list(self.nstack), self.cur, list(self.saved),
[list(f) for f in self.frames])
def _restore(self, snap):
self.nstack, self.cur, self.saved, self.frames = (
list(snap[0]), snap[1], list(snap[2]), [list(f) for f in snap[3]])
# -- single-instruction symbolic execution ---------------------------
def _step(self, i, lines, indent):
ins = self.I[i]
op = ins.op
o = ins.operands
if op == "OP_LOADIMMED_UINT":
self.nstack.append(Expr(str(o[0])))
elif op == "OP_LOADIMMED_FLT":
self.nstack.append(Expr(fmt_float(self.floats[o[0]])))
elif op == "OP_LOADIMMED_STR":
self.cur = Expr(quote_str(self.strings.get_str(o[0])))
elif op == "OP_TAG_TO_STR":
self.cur = Expr(quote_tagged(self.strings.get_str(o[0])))
elif op == "OP_LOADIMMED_IDENT":
self.cur = Expr(self.ste(ins.ip + 1), ident=True)
elif op == "OP_SETCURVAR" or op == "OP_SETCURVAR_CREATE":
self.cur_var = Expr(self.ste(ins.ip + 1))
elif op in ("OP_SETCURVAR_ARRAY", "OP_SETCURVAR_ARRAY_CREATE"):
self.cur_var = self._array_var(self.cur)
elif op in ("OP_LOADVAR_UINT", "OP_LOADVAR_FLT"):
self.nstack.append(self.cur_var)
elif op == "OP_LOADVAR_STR":
self.cur = self.cur_var
elif op in ("OP_SAVEVAR_UINT", "OP_SAVEVAR_FLT"):
rhs = self.nstack[-1] if self.nstack else Expr("")
self.nstack[-1:] = [Expr("%s = %s" % (self.cur_var.t,
rhs.paren(P_ASSIGN)), P_ASSIGN)]
elif op == "OP_SAVEVAR_STR":
rhs = self.cur if self.cur is not None else Expr("")
self.cur = Expr("%s = %s" % (self.cur_var.t, rhs.paren(P_ASSIGN)),
P_ASSIGN)
elif op == "OP_SETCUROBJECT":
self.cur_obj = self.cur
elif op == "OP_SETCUROBJECT_NEW":
self.cur_obj = "NEW"
elif op == "OP_SETCURFIELD":
self.cur_field = self.ste(ins.ip + 1)
self.cur_field_array = None
elif op == "OP_SETCURFIELD_ARRAY":
self.cur_field_array = self.cur
elif op in ("OP_LOADFIELD_UINT", "OP_LOADFIELD_FLT"):
self.nstack.append(self._field_expr())
elif op == "OP_LOADFIELD_STR":
self.cur = self._field_expr()
elif op in ("OP_SAVEFIELD_UINT", "OP_SAVEFIELD_FLT"):
rhs = self.nstack[-1] if self.nstack else Expr("")
self.nstack[-1:] = [Expr("%s = %s" % (self._field_expr().t,
rhs.paren(P_ASSIGN)), P_ASSIGN)]
elif op == "OP_SAVEFIELD_STR":
rhs = self.cur if self.cur is not None else Expr("")
self.cur = Expr("%s = %s" % (self._field_expr().t,
rhs.paren(P_ASSIGN)), P_ASSIGN)
# conversions: source text unchanged; just move between stacks
elif op in ("OP_STR_TO_UINT", "OP_STR_TO_FLT"):
self.nstack.append(self.cur if self.cur is not None else Expr(""))
self.cur = None
elif op in ("OP_UINT_TO_STR", "OP_FLT_TO_STR"):
self.cur = self.nstack.pop() if self.nstack else Expr("")
elif op in ("OP_UINT_TO_FLT", "OP_FLT_TO_UINT"):
pass
elif op == "OP_STR_TO_NONE":
self._emit_stmt(lines, indent, self.cur)
self.cur = None
elif op in ("OP_UINT_TO_NONE", "OP_FLT_TO_NONE"):
self._emit_stmt(lines, indent, self.nstack.pop() if self.nstack
else None)
# arithmetic / comparison / logic (binary; left was compiled last = top)
elif op == "OP_ADD":
self._binop("+", P_ADD)
elif op == "OP_SUB":
self._binop("-", P_ADD)
elif op == "OP_MUL":
self._binop("*", P_MULT)
elif op == "OP_DIV":
self._binop("/", P_MULT)
elif op == "OP_MOD":
self._binop("%", P_MULT)
elif op == "OP_CMPEQ":
self._binop("==", P_EQ)
elif op == "OP_CMPNE":
self._binop("!=", P_EQ)
elif op == "OP_CMPGR":
self._binop(">", P_REL)
elif op == "OP_CMPGE":
self._binop(">=", P_REL)
elif op == "OP_CMPLT":
self._binop("<", P_REL)
elif op == "OP_CMPLE":
self._binop("<=", P_REL)
elif op == "OP_BITAND":
self._binop("&", P_BAND)
elif op == "OP_BITOR":
self._binop("|", P_BOR)
elif op == "OP_XOR":
self._binop("^", P_BXOR)
elif op == "OP_SHL":
self._binop("<<", P_SHIFT)
elif op == "OP_SHR":
self._binop(">>", P_SHIFT)
elif op == "OP_AND":
self._binop("&&", P_AND)
elif op == "OP_OR":
self._binop("||", P_OR)
elif op == "OP_NEG":
a = self.nstack.pop() if self.nstack else Expr("")
self.nstack.append(Expr("-" + a.paren(P_UNARY), P_UNARY))
elif op == "OP_NOT" or op == "OP_NOTF":
a = self.nstack.pop() if self.nstack else Expr("")
self.nstack.append(Expr("!" + a.paren(P_UNARY), P_UNARY))
elif op == "OP_ONESCOMPLEMENT":
a = self.nstack.pop() if self.nstack else Expr("")
self.nstack.append(Expr("~" + a.paren(P_UNARY), P_UNARY))
# short-circuit && / || via NP jumps
elif op in ("OP_JMPIF_NP", "OP_JMPIFNOT_NP"):
return self._logical(i)
# string building
elif op == "OP_ADVANCE_STR":
self.saved.append((self.cur, "@"))
self.cur = None
elif op == "OP_ADVANCE_STR_APPENDCHAR":
sep = {" ": "SPC", "\t": "TAB", "\n": "NL"}.get(chr(o[0]), "@")
self.saved.append((self.cur, sep))
self.cur = None
elif op == "OP_ADVANCE_STR_COMMA":
self.saved.append((self.cur, "COMMA"))
self.cur = None
elif op == "OP_ADVANCE_STR_NUL":
self.saved.append((self.cur, "NUL"))
self.cur = None
elif op == "OP_REWIND_STR":
saved, sep = self.saved.pop()
self.cur = self._combine(saved, sep, self.cur)
elif op == "OP_TERMINATE_REWIND_STR":
saved, sep = self.saved.pop()
self.cur = saved # discard current, restore saved
elif op == "OP_COMPARE_STR":
saved, sep = self.saved.pop()
left = saved if saved is not None else Expr("")
right = self.cur if self.cur is not None else Expr("")
self.cur = None
self.nstack.append(Expr("%s $= %s" % (left.paren(P_EQ + 1),
right.paren(P_EQ + 1)), P_EQ))
# calls
elif op == "OP_PUSH_FRAME":
self.frames.append([])
elif op == "OP_PUSH":
self.frames[-1].append(self.cur if self.cur is not None else Expr(""))
self.cur = None
elif op in ("OP_CALLFUNC", "OP_CALLFUNC_RESOLVE"):
self._call(ins)
# object creation
elif op == "OP_CREATE_OBJECT":
return self._object(i, lines, indent)
elif op in ("OP_ADD_OBJECT", "OP_END_OBJECT"):
pass
elif op in ("OP_UNUSED_2", "OP_UNUSED_3", "OP_UNUSED_72", "OP_BREAK"):
pass
else:
self._emit(lines, indent, "// ?? unhandled %s" % op)
return i + 1
# -- expression helpers ----------------------------------------------
def _binop(self, sym, prec):
a = self.nstack.pop() if self.nstack else Expr("") # left (top)
b = self.nstack.pop() if self.nstack else Expr("") # right
self.nstack.append(Expr("%s %s %s" % (a.paren(prec), sym,
b.paren(prec + 1)), prec))
def _combine(self, left, sep, right):
left = left if left is not None else Expr("")
right = right if right is not None else Expr("")
if sep == "COMMA":
return Expr("%s, %s" % (left.paren(P_COMMA + 1),
right.paren(P_COMMA + 1)), P_COMMA)
sym = {"@": "@", "SPC": "SPC", "TAB": "TAB", "NL": "NL"}.get(sep, "@")
e = Expr("%s %s %s" % (left.paren(P_CAT), sym, right.paren(P_CAT + 1)),
P_CAT)
if left.ident:
e.array = (left.t, right)
return e
def _array_var(self, e):
if e is not None and e.array is not None:
base, idx = e.array
return Expr("%s[%s]" % (base, idx.t))
# fallback
return Expr((e.t if e else ""))
def _field_expr(self):
fld = self.cur_field
if self.cur_field_array is not None:
fld = "%s[%s]" % (fld, self.cur_field_array.t)
if self.cur_obj == "NEW" or self.cur_obj is None:
return Expr(fld)
return Expr("%s.%s" % (self.cur_obj.paren(P_ATOM), fld))
def _logical(self, i):
ins = self.I[i]
sym = "||" if ins.op == "OP_JMPIF_NP" else "&&"
prec = P_OR if sym == "||" else P_AND
left = self.nstack.pop() if self.nstack else Expr("")
tdx = self.target_idx(ins)
self._block(i + 1, tdx, [], 0) # evaluate right operand
right = self.nstack.pop() if self.nstack else Expr("")
self.nstack.append(Expr("%s %s %s" % (left.paren(prec),
sym, right.paren(prec + 1)), prec))
return tdx
def _call(self, ins):
fn = self.ste(ins.ip + 1)
ns = self.ste(ins.ip + 2)
call_type = ins.operands[2]
args = self.frames.pop() if self.frames else []
argtxt = [a.t for a in args]
if call_type == CALL_METHOD:
obj = argtxt[0] if argtxt else ""
rest = ", ".join(argtxt[1:])
self.cur = Expr("%s.%s(%s)" % (obj, fn, rest))
elif call_type == CALL_PARENT:
self.cur = Expr("Parent::%s(%s)" % (fn, ", ".join(argtxt)))
else:
name = "%s::%s" % (ns, fn) if ns else fn
self.cur = Expr("%s(%s)" % (name, ", ".join(argtxt)))
def _emit_stmt(self, lines, indent, val):
if val is not None and val.t != "":
self._emit(lines, indent, val.t + ";")
# -- object declarations ---------------------------------------------
def _object(self, i, lines, indent):
"""Parse a root OP_CREATE_OBJECT .. OP_END_OBJECT into `new`/`datablock`."""
text, end_idx = self._object_text(i, indent)
# A root object decl is preceded by `OP_LOADIMMED_UINT 0` (the group id
# placeholder); OP_ADD_OBJECT overwrites it with the new object's id.
if self.nstack and self.nstack[-1].t == "0":
self.nstack.pop()
self.nstack.append(Expr(text, P_ATOM))
return end_idx
def _object_text(self, i, indent):
ins = self.I[i]
parent = self.ste(ins.ip + 1)
is_db = ins.operands[1]
frame = self.frames.pop() if self.frames else []
argtxt = [a.t for a in frame]
class_name = argtxt[0] if argtxt else ""
obj_name = argtxt[1] if len(argtxt) > 1 else ""
ctor = argtxt[2:]
kw = "datablock" if is_db else "new"
name_part = "" if obj_name in ("", '""') else obj_name
decl = name_part
if parent:
decl = "%s : %s" % (name_part, parent) if name_part else ": " + parent
if ctor:
decl = (decl + ", " if decl else "") + ", ".join(ctor)
header = "%s %s(%s)" % (kw, class_name, decl)
# find this object's OP_ADD_OBJECT (depth 0; a field value could itself
# create a nested object, which would have its own ADD/END pair)
j = i + 1
depth = 0
while j < self.hi:
op = self.I[j].op
if op == "OP_CREATE_OBJECT":
depth += 1
elif op == "OP_END_OBJECT":
depth -= 1
elif op == "OP_ADD_OBJECT" and depth == 0:
break
j += 1
add_idx = j
# field assignments may contain ternary/logical expressions, so run the
# full control-flow-aware walker over the field region.
field_lines = []
child_texts = []
self._block(i + 1, add_idx, field_lines, 0)
if j < self.hi and self.I[j].op == "OP_ADD_OBJECT":
j += 1
# sub-objects: each is a full nested decl whose preamble (PUSH_FRAME,
# class/name/arg pushes) must be simulated before its CREATE_OBJECT.
throwaway = []
while j < self.hi and self.I[j].op != "OP_END_OBJECT":
if self.I[j].op == "OP_CREATE_OBJECT":
ctext, j = self._object_text(j, indent + 1)
child_texts.append(ctext)
else:
j = self._step(j, throwaway, 0)
# OP_END_OBJECT for this object
if j < self.hi and self.I[j].op == "OP_END_OBJECT":
j += 1
ind = " " * indent
body = ""
inner = " " * (indent + 1)
parts = []
for fl in field_lines:
parts.append(inner + fl)
if child_texts:
if parts:
parts.append("")
for ct in child_texts:
parts.append(inner + ct + ";")
if parts:
body = "\n%s{\n%s\n%s}" % (ind, "\n".join(parts), ind)
else:
body = ""
return header + body, j
def function_ranges(instrs, idx_of_ip):
"""Return list of (decl_instr_idx, body_lo_idx, body_hi_idx)."""
ranges = []
for k, ins in enumerate(instrs):
if ins.op == "OP_FUNC_DECL":
argc = ins.operands[5]
end_ip = ins.operands[4]
body_lo = k + 1
body_hi = idx_of_ip[end_ip]
ranges.append((k, body_lo, body_hi))
return ranges
def decompile(cb):
instrs, by_ip = disassemble(cb)
idx_of_ip = {ins.ip: k for k, ins in enumerate(instrs)}
# also map code_size (one past end) for jump targets to the end
idx_of_ip[cb.code_size] = len(instrs)
fns = function_ranges(instrs, idx_of_ip)
fn_body = set()
fn_decl_idx = {}
for decl, lo, hi in fns:
fn_decl_idx[decl] = (lo, hi)
for k in range(decl, hi):
fn_body.add(k)
out = []
out.append("// Decompiled from DSO (TorqueScript v%d) by dsodecomp.py"
% cb.version)
out.append("")
# Walk top-level instructions in order; when we hit a FUNC_DECL, emit the
# function (using the function string/float tables); otherwise accumulate
# top-level statements and flush them as a group.
i = 0
n = len(instrs)
top_chunk_lo = None
def flush_top(lo, hi):
if lo is None or lo >= hi:
return
scope = Scope(cb, instrs, idx_of_ip, lo, hi, in_function=False)
for line in scope.decompile():
out.append(line)
while i < n:
ins = instrs[i]
if ins.op == "OP_FUNC_DECL":
flush_top(top_chunk_lo, i)
top_chunk_lo = None
lo, hi = fn_decl_idx[i]
name = cb.idents.get(ins.ip + 1, "")
ns = cb.idents.get(ins.ip + 2, "")
pkg = cb.idents.get(ins.ip + 3, "")
argc = ins.operands[5]
args = []
for a in range(argc):
nm = cb.idents.get(ins.ip + 7 + a, "").lstrip("%")
args.append("%" + nm if nm else "%%junk%d" % a)
full = "%s::%s" % (ns, name) if ns else name
if pkg:
out.append("// package: %s" % pkg)
out.append("function %s(%s)" % (full, ", ".join(args)))
out.append("{")
scope = Scope(cb, instrs, idx_of_ip, lo, hi, in_function=True)
for line in scope.decompile(base_indent=1):
out.append(line)
out.append("}")
out.append("")
i = hi
else:
if top_chunk_lo is None:
top_chunk_lo = i
i += 1
flush_top(top_chunk_lo, n)
return "\n".join(out)
def main(argv=None):
ap = argparse.ArgumentParser(description="Decompile Torque/AoT .dso files.")
ap.add_argument("path", help="a .dso file, or a directory to recurse")
ap.add_argument("-o", "--output", help="output path (single file only)")
ap.add_argument("--stdout", action="store_true",
help="print result to the terminal instead of a file")
ap.add_argument("--disasm", action="store_true",
help="emit raw disassembly instead of reconstructed source")
ap.add_argument("-f", "--force", action="store_true",
help="overwrite the output file if it already exists")
ap.add_argument("--quiet", action="store_true")
args = ap.parse_args(argv)
if os.path.isdir(args.path):
targets = []
for root, _dirs, files in os.walk(args.path):
for f in files:
if f.lower().endswith(".dso"):
targets.append(os.path.join(root, f))
for t in sorted(targets):
try:
decompile_file(t, disasm=args.disasm, quiet=args.quiet,
force=args.force)
except Exception as exc: # noqa: BLE001
print("ERROR %s: %s" % (t, exc), file=sys.stderr)
return 0
decompile_file(args.path, out=args.output, to_stdout=args.stdout,
disasm=args.disasm, quiet=args.quiet, force=args.force)
return 0
if __name__ == "__main__":
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment