mirror of
https://github.com/skillbert/rsmv.git
synced 2026-01-23 20:58:10 -05:00
360 lines
15 KiB
TypeScript
360 lines
15 KiB
TypeScript
import { boundMethod } from "autobind-decorator";
|
|
import { AstNode, BranchingStatement, ClientScriptFunction, CodeBlockNode, ComposedOp, FunctionBindNode, IfStatementNode, RawOpcodeNode, SwitchStatementNode, VarAssignNode, WhileLoopStatementNode, getSingleChild, SubcallNode, ComposedopType } from "./ast";
|
|
import { ClientscriptObfuscation } from "./callibrator";
|
|
import { ClientScriptSubtypeSolver } from "./subtypedetector";
|
|
import { ClientScriptOp, PrimitiveType, binaryOpSymbols, branchInstructionsOrJump, getOpName, longJsonToBigInt, namedClientScriptOps, popDiscardOps, popLocalOps, subtypeToTs, subtypes } from "./definitions";
|
|
|
|
/**
|
|
* known compiler differences
|
|
* - in some situations bunny hop jumps in nested ifs are merged while the jagex compiler doesn't
|
|
* - default return values for int can be -1 for some specialisations while this compiler doesn't know about those
|
|
* - this ast tree automatically strips dead code so round trips won't be identical if there dead code
|
|
* - when a script has no return values but the original code had an explicit return then this compiler won't output that
|
|
* - the jagex compiler uses some unknown logic to put the default branch of a switch statement either at the start or end of the block
|
|
*/
|
|
|
|
/**
|
|
* decompiler TODO
|
|
* - fix default return of -1 for int specialisations
|
|
* - fix function bind arrays
|
|
*/
|
|
|
|
export function debugAst(node: AstNode) {
|
|
let writer = new TsWriterContext(globalThis.deob, new ClientScriptSubtypeSolver())
|
|
let res = "";
|
|
if (node instanceof CodeBlockNode) { res += `//[${node.scriptid},${node.originalindex}]\n`; }
|
|
res += writer.getCode(node);
|
|
console.log(res);
|
|
}
|
|
globalThis.debugAst = debugAst;
|
|
|
|
export class TsWriterContext {
|
|
calli: ClientscriptObfuscation;
|
|
typectx: ClientScriptSubtypeSolver;
|
|
indents: boolean[] = [];
|
|
declaredVars: Set<string>[] = [];
|
|
constructor(calli: ClientscriptObfuscation, typectx: ClientScriptSubtypeSolver) {
|
|
this.calli = calli;
|
|
this.typectx = typectx;
|
|
}
|
|
codeIndent(linenr = -1, hasquestionmark = false) {
|
|
// return (linenr == -1 ? "" : linenr + ":").padEnd(5 + amount * 4, " ") + (hasquestionmark ? "?? " : " ");
|
|
return " ".repeat(this.indents.length);
|
|
}
|
|
pushIndent(hasScope: boolean) {
|
|
this.indents.push(hasScope);
|
|
if (hasScope) {
|
|
this.declaredVars.push(new Set());
|
|
}
|
|
}
|
|
popIndent() {
|
|
let hadscope = this.indents.pop();
|
|
if (hadscope == undefined) { throw new Error("negative indent"); }
|
|
if (hadscope) {
|
|
this.declaredVars.pop();
|
|
}
|
|
}
|
|
declareLocal(varname: string) {
|
|
let set = this.declaredVars.at(-1);
|
|
if (!set) { throw new Error("no scope"); }
|
|
if (set.has(varname)) {
|
|
return true;
|
|
} else {
|
|
set.add(varname);
|
|
return false;
|
|
}
|
|
}
|
|
@boundMethod
|
|
getCode(node: AstNode) {
|
|
let writer = writermap.get(node.constructor);
|
|
if (!writer) { throw new Error(`no writer defined for ${node.constructor.name} node`); }
|
|
return writer(node, this);
|
|
}
|
|
}
|
|
|
|
function getOpcodeName(calli: ClientscriptObfuscation, op: ClientScriptOp) {
|
|
if (op.opcode == namedClientScriptOps.poplocalint || op.opcode == namedClientScriptOps.pushlocalint) {
|
|
return `int${op.imm}`;
|
|
} else if (op.opcode == namedClientScriptOps.poplocalstring || op.opcode == namedClientScriptOps.pushlocalstring) {
|
|
return `string${op.imm}`;
|
|
} else if (op.opcode == namedClientScriptOps.poplocallong || op.opcode == namedClientScriptOps.pushlocallong) {
|
|
return `long${op.imm}`;
|
|
} else if (op.opcode == namedClientScriptOps.popdiscardint || op.opcode == namedClientScriptOps.popdiscardlong || op.opcode == namedClientScriptOps.popdiscardstring) {
|
|
return "";
|
|
} else if (op.opcode == namedClientScriptOps.popvar || op.opcode == namedClientScriptOps.pushvar) {
|
|
let varmeta = calli.getClientVarMeta(op.imm);
|
|
if (varmeta) {
|
|
return `var${varmeta.name}_${varmeta.varid}`;
|
|
} else {
|
|
return `varunk_${op.imm}`;
|
|
}
|
|
} else if (op.opcode == namedClientScriptOps.popvarbit || op.opcode == namedClientScriptOps.pushvarbit) {
|
|
let id = op.imm >> 8;
|
|
let optarget = (op.imm & 0xff);
|
|
let varbitmeta = calli.varbitmeta.get(id);
|
|
if (typeof varbitmeta?.varid != "number") {
|
|
return `varbitunk_${op.imm}`;
|
|
} else {
|
|
let groupmeta = calli.varmeta.get(varbitmeta.varid >> 16);
|
|
return `varbit${groupmeta?.name ?? "unk"}_${id}${optarget == 0 ? "" : `[${optarget}]`}`;//TODO this is currently not supported in the parser
|
|
}
|
|
}
|
|
return getOpName(op.opcode);
|
|
}
|
|
|
|
function valueList(ctx: TsWriterContext, nodes: AstNode[]) {
|
|
if (nodes.length == 1) { return ctx.getCode(nodes[0]); }
|
|
return `[${nodes.map(ctx.getCode).join(", ")}]`;
|
|
}
|
|
|
|
function escapeStringLiteral(source: string, quotetype: "template" | "double" | "single") {
|
|
return source.replace(/[`"'\\\n\r\t\b\f\x00-\x1F]|\$\{/g, m => {
|
|
switch (m) {
|
|
case '"': return (quotetype == "double" ? '\\"' : "\"");
|
|
case "'": return (quotetype == "single" ? "\\'" : "'");
|
|
case "\\": return "\\\\";
|
|
case "\n": return "\\n";
|
|
case "\r": return "\\r";
|
|
case "\t": return "\\t";
|
|
case "\b": return "\\b";
|
|
case "\f": return "\\f";
|
|
case "${": return (quotetype == "template" ? "\\${" : "${");
|
|
case "`": return (quotetype == "template" ? "\\`" : "`");
|
|
default: return `\\x${m.charCodeAt(0).toString(16).padStart(2, "0")}`;
|
|
}
|
|
});
|
|
}
|
|
|
|
function writeCall(ctx: TsWriterContext, funcstring: string, children: AstNode[]) {
|
|
return `${funcstring}(${children.map(ctx.getCode).join(", ")})`;
|
|
}
|
|
function getOpcodeCallCode(ctx: TsWriterContext, op: ClientScriptOp, children: AstNode[], originalindex: number) {
|
|
let binarysymbol = binaryOpSymbols.get(op.opcode);
|
|
if (binarysymbol) {
|
|
if (children.length == 2) {
|
|
return `(${ctx.getCode(children[0])} ${binarysymbol} ${ctx.getCode(children[1])})`;
|
|
} else {
|
|
return `(${binarysymbol} ${children.map(ctx.getCode).join(" ")})`;
|
|
}
|
|
}
|
|
if (op.opcode == namedClientScriptOps.return) {
|
|
if (children.length == 0) { return `return`; }
|
|
return `return ${valueList(ctx, children)}`;
|
|
}
|
|
if (op.opcode == namedClientScriptOps.gosub) {
|
|
return writeCall(ctx, `script${op.imm}`, children);
|
|
}
|
|
let metastr = "";
|
|
if (branchInstructionsOrJump.includes(op.opcode)) {
|
|
metastr = `[${op.imm + originalindex + 1}]`;
|
|
} else if (op.opcode == namedClientScriptOps.gosub) {
|
|
metastr = `[${op.imm}]`;
|
|
} else if (op.imm != 0) {
|
|
metastr = `[${op.imm}]`;
|
|
}
|
|
return writeCall(ctx, `${getOpcodeName(ctx.calli, op)}${metastr}`, children);
|
|
}
|
|
|
|
const writermap = new Map<AstNode["constructor"], (node: AstNode, ctx: TsWriterContext) => string>();
|
|
|
|
function addWriter<T extends new (...args: any[]) => AstNode>(type: T, writer: (node: InstanceType<T>, ctx: TsWriterContext) => string) {
|
|
writermap.set(type, writer as any);
|
|
}
|
|
|
|
addWriter(ComposedOp, (node, ctx) => {
|
|
if ((["++x", "--x", "x++", "x--"] as ComposedopType[]).includes(node.type)) {
|
|
if (node.children.length != 0) { throw new Error("no children expected on composednode"); }
|
|
let varname = getOpcodeName(ctx.calli, (node.internalOps[0] as RawOpcodeNode).op);
|
|
if (node.type == "++x") { return `++${varname}`; }
|
|
if (node.type == "--x") { return `--${varname}`; }
|
|
if (node.type == "x++") { return `${varname}++`; }
|
|
if (node.type == "x--") { return `${varname}--`; }
|
|
}
|
|
if (node.type == "stack") {
|
|
return writeCall(ctx, "stack", node.children);
|
|
}
|
|
throw new Error("unknown composed op type");
|
|
});
|
|
addWriter(VarAssignNode, (node, ctx) => {
|
|
let res = "";
|
|
let fulldiscard = node.varops.every(q => popDiscardOps.includes(q.op.opcode));
|
|
if (!fulldiscard) {
|
|
let hasglobal = false;
|
|
let hasundeclared = false;
|
|
let varnames: string[] = [];
|
|
let exacttypes: number[] = [];
|
|
let vardeclared: boolean[] = [];
|
|
for (let sub of node.varops) {
|
|
let name = getOpcodeName(ctx.calli, sub.op);
|
|
|
|
let exacttype = -1;
|
|
if (node.knownStackDiff?.exactin) {
|
|
let all = node.knownStackDiff.exactin.all();
|
|
if (all.length != 1) { throw new Error("unexpected"); }
|
|
let type = ctx.typectx.knowntypes.get(all[0]);
|
|
if (typeof type == "number") {
|
|
exacttype = type;
|
|
}
|
|
}
|
|
exacttypes.push(exacttype);
|
|
if (popLocalOps.includes(sub.op.opcode)) {
|
|
let isdeclared = ctx.declareLocal(name);
|
|
hasundeclared ||= !isdeclared;
|
|
vardeclared.push(isdeclared);
|
|
} else {
|
|
hasglobal = true;
|
|
}
|
|
varnames.push(name);
|
|
}
|
|
if (hasundeclared) {
|
|
if (hasglobal) {
|
|
//we need a "var" expression, but can't add var to the entire destructor operation, add seperate var declarations
|
|
for (let [index, name] of varnames.entries()) {
|
|
if (vardeclared[index]) { continue; }
|
|
res += `var ${name}:${subtypeToTs(exacttypes[index])};`;
|
|
res += ctx.codeIndent();
|
|
}
|
|
} else {
|
|
res += "var ";
|
|
}
|
|
}
|
|
if (node.varops.length != 1) { res += "["; }
|
|
res += `${varnames.join(", ")}`;
|
|
if (node.varops.length != 1) { res += "]"; }
|
|
res += " = ";
|
|
}
|
|
res += valueList(ctx, node.children);
|
|
return res;
|
|
});
|
|
addWriter(CodeBlockNode, (node, ctx) => {
|
|
let code = "";
|
|
if (node.parent) {
|
|
code += `{\n`;
|
|
ctx.pushIndent(node.parent instanceof ClientScriptFunction);
|
|
}
|
|
// code += `${codeIndent(indent, node.originalindex)}//[${node.scriptid},${node.originalindex}]\n`;
|
|
for (let child of node.children) {
|
|
code += `${ctx.codeIndent(child.originalindex)}${ctx.getCode(child)};\n`;
|
|
}
|
|
if (node.parent) {
|
|
ctx.popIndent();
|
|
code += `${ctx.codeIndent()}}`;
|
|
}
|
|
return code;
|
|
});
|
|
addWriter(BranchingStatement, (node, ctx) => {
|
|
return getOpcodeCallCode(ctx, node.op, node.children, node.originalindex);
|
|
});
|
|
addWriter(WhileLoopStatementNode, (node, ctx) => {
|
|
let res = `while (${ctx.getCode(node.statement)}) `;
|
|
res += ctx.getCode(node.body);
|
|
return res;
|
|
});
|
|
addWriter(SwitchStatementNode, (node, ctx) => {
|
|
let res = "";
|
|
res += `switch (${node.valueop ? ctx.getCode(node.valueop) : ""}) {\n`;
|
|
ctx.pushIndent(false);
|
|
for (let [i, branch] of node.branches.entries()) {
|
|
res += `${ctx.codeIndent(branch.block.originalindex)}case ${branch.value}:`;
|
|
if (i + 1 < node.branches.length && node.branches[i + 1].block == branch.block) {
|
|
res += `\n`;
|
|
} else {
|
|
res += " " + ctx.getCode(branch.block);
|
|
res += `\n`;
|
|
}
|
|
}
|
|
if (node.defaultbranch) {
|
|
res += `${ctx.codeIndent()}default: `;
|
|
res += ctx.getCode(node.defaultbranch);
|
|
res += `\n`;
|
|
}
|
|
ctx.popIndent();
|
|
res += `${ctx.codeIndent()}}`;
|
|
return res;
|
|
});
|
|
addWriter(IfStatementNode, (node, ctx) => {
|
|
let res = `if (${ctx.getCode(node.statement)}) `;
|
|
res += ctx.getCode(node.truebranch);
|
|
if (node.falsebranch) {
|
|
res += ` else `;
|
|
//skip brackets for else if construct
|
|
let subif = getSingleChild(node.falsebranch, IfStatementNode);
|
|
if (subif) {
|
|
res += ctx.getCode(subif);
|
|
} else {
|
|
res += ctx.getCode(node.falsebranch);
|
|
}
|
|
}
|
|
return res;
|
|
});
|
|
addWriter(RawOpcodeNode, (node, ctx) => {
|
|
if (node.op.opcode == namedClientScriptOps.pushconst) {
|
|
let exacttype = -1;
|
|
if (node.knownStackDiff?.exactout) {
|
|
let all = node.knownStackDiff.exactout.all();
|
|
if (all.length != 1) { throw new Error("unexpected"); }
|
|
let type = ctx.typectx.knowntypes.get(all[0]);
|
|
if (typeof type == "number") {
|
|
exacttype = type;
|
|
}
|
|
}
|
|
let gettypecast = (subt: PrimitiveType) => {
|
|
if (exacttype == -1) { return ""; }
|
|
if (exacttype == subtypes.int || exacttype == subtypes.string || exacttype == subtypes.long) { return ""; }
|
|
if (exacttype == subtypes.unknown_int || exacttype == subtypes.unknown_string || exacttype == subtypes.unknown_long) { return ""; }
|
|
return ` as ${subtypeToTs(exacttype)}`;
|
|
}
|
|
if (typeof node.op.imm_obj == "string") {
|
|
return `"${escapeStringLiteral(node.op.imm_obj, "double")}"${gettypecast("string")}`;
|
|
} else if (Array.isArray(node.op.imm_obj)) {
|
|
return `${longJsonToBigInt(node.op.imm_obj)}n${gettypecast("long")}`;
|
|
} else if (typeof node.op.imm_obj == "number") {
|
|
if (exacttype == subtypes.component) {
|
|
return `comp(${node.op.imm_obj >> 16}, ${node.op.imm_obj & 0xffff})`;
|
|
}
|
|
if (exacttype == subtypes.boolean) {
|
|
return (node.op.imm_obj == 1 ? "true" : "false");
|
|
}
|
|
return `${node.op.imm_obj}${gettypecast("int")}`;
|
|
} else {
|
|
throw new Error("unexpected");
|
|
}
|
|
}
|
|
if (node.op.opcode == namedClientScriptOps.pushlocalint
|
|
|| node.op.opcode == namedClientScriptOps.pushlocallong
|
|
|| node.op.opcode == namedClientScriptOps.pushlocalstring
|
|
|| node.op.opcode == namedClientScriptOps.pushvar
|
|
|| node.op.opcode == namedClientScriptOps.pushvarbit) {
|
|
return getOpcodeName(ctx.calli, node.op);
|
|
}
|
|
if (node.op.opcode == namedClientScriptOps.joinstring) {
|
|
let res = "`";
|
|
for (let child of node.children) {
|
|
if (child instanceof RawOpcodeNode && child.opinfo.id == namedClientScriptOps.pushconst && typeof child.op.imm_obj == "string") {
|
|
res += escapeStringLiteral(child.op.imm_obj, "template");
|
|
} else {
|
|
res += `\${${ctx.getCode(child)}}`;
|
|
}
|
|
}
|
|
res += "`";
|
|
return res;
|
|
}
|
|
return getOpcodeCallCode(ctx, node.op, node.children, node.originalindex);
|
|
});
|
|
addWriter(ClientScriptFunction, (node, ctx) => {
|
|
let scriptidmatch = node.scriptname.match(/^script(\d+)$/);
|
|
let meta = (scriptidmatch ? ctx.calli.scriptargs.get(+scriptidmatch[1]) : null);
|
|
let res = "";
|
|
res += `//${meta?.scriptname ?? "unknown name"}\n`;
|
|
res += `${ctx.codeIndent()}function ${node.scriptname}(${node.argtype.toTypeScriptVarlist(true, meta?.stack.exactin)}): ${node.returntype.toTypeScriptReturnType(meta?.stack.exactout)} `;
|
|
res += ctx.getCode(node.children[0]);
|
|
return res;
|
|
});
|
|
addWriter(FunctionBindNode, (node, ctx) => {
|
|
let scriptid = node.children[0]?.knownStackDiff?.constout ?? -1;
|
|
if (scriptid == -1 && node.children.length == 1) { return `callback()`; }
|
|
return `callback(script${scriptid}${node.children.length > 1 ? ", " : ""}${node.children.slice(1).map(ctx.getCode).join(", ")})`;
|
|
});
|
|
addWriter(SubcallNode, (node, ctx) => {
|
|
return writeCall(ctx, node.funcname, node.children.slice(0, -1));
|
|
}); |