clientscript compiler with decent round-trip results

This commit is contained in:
Skillbert
2024-01-10 02:39:39 +01:00
parent 07523f11ec
commit f1dfd8cd45
13 changed files with 266 additions and 91 deletions

View File

@@ -19,6 +19,6 @@ export type clientscript = {
opcodedata: {
opcode:number,
imm:number,
imm_obj:number|string|[number,number]|null,
imm_obj:number|string|[number,number]|{value:number,label:number}[]|null,
}[],
};

17
generated/dbrows.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
// GENERATED DO NOT EDIT
// This source data is located at '..\src\opcodes\dbrows.jsonc'
// run `npm run filetypes` to rebuild
export type dbrows = {
unk01?: {
cols: number,
columndata: {
id: number,
columns: {
type: number,
value: (string|number)[],
}[],
}[],
} | null
table?: number | null
};

17
generated/dbtables.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
// GENERATED DO NOT EDIT
// This source data is located at '..\src\opcodes\dbtables.jsonc'
// run `npm run filetypes` to rebuild
export type dbtables = {
unk01?: {
cols: number,
columndata: {
id: number,
columns: {
type: number,
unk: number | null,
default: (string|number) | null,
}[],
}[],
} | null
};

View File

@@ -3,6 +3,7 @@ import { clientscriptdata } from "../../generated/clientscriptdata";
import { CacheFileSource } from "../cache";
import { parse } from "../opdecoder";
import { ClientscriptObfuscation, OpcodeInfo, getArgType, getReturnType, prepareClientScript, typeToPrimitive } from "./callibrator";
import { clientscriptParser } from "./codeparser";
import { binaryOpIds, binaryOpSymbols, branchInstructions, branchInstructionsOrJump, dynamicOps, knownClientScriptOpNames, namedClientScriptOps, variableSources, StackDiff, StackInOut, StackList, StackTypeExt, ClientScriptOp, StackConst, StackType, StackConstants } from "./definitions";
/**
@@ -15,8 +16,20 @@ import { binaryOpIds, binaryOpSymbols, branchInstructions, branchInstructionsOrJ
*/
//get script names from https://api.runewiki.org/hashes?rev=930
/**
* 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
*/
function getSingleChild<T extends AstNode>(op: CodeBlockNode | null | undefined, type: { new(...args: any[]): T }) {
if (!op || op.children.length != 1 || !(op.children[0] instanceof type)) { return null; }
return op.children[0] as T;
}
function codeIndent(amount: number, linenr = -1, hasquestionmark = false) {
// linenr = -1;
return (linenr == -1 ? "" : linenr + ":").padEnd(5 + amount * 4, " ") + (hasquestionmark ? "?? " : " ");
@@ -27,6 +40,8 @@ function getOpcodeName(calli: ClientscriptObfuscation, op: ClientScriptOp) {
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.popvar || op.opcode == namedClientScriptOps.pushvar) {
let varmeta = calli.getClientVarMeta(op.imm);
if (varmeta) {
@@ -44,7 +59,7 @@ function getOpcodeCallCode(calli: ClientscriptObfuscation, op: ClientScriptOp, c
if (children.length == 2) {
return `(${children[0].getCode(calli, indent)} ${binarysymbol} ${children[1].getCode(calli, indent)})`;
} else {
return `(${binarysymbol} ${children.map(q => q.getCode(calli, indent))})`;
return `(${binarysymbol} ${children.map(q => q.getCode(calli, indent)).join(" ")})`;
}
}
let metastr = "";
@@ -202,11 +217,16 @@ export class CodeBlockNode extends AstNode {
}
getCode(calli: ClientscriptObfuscation, indent: number) {
let code = "";
if (this.parent) { code += `{\n`; indent++; }
if (this.parent && !(this.parent instanceof ClientScriptFunction)) {
code += `{\n`;
indent++;
}
for (let child of this.children) {
code += `${codeIndent(indent, child.originalindex)}${child.getCode(calli, indent)}\n`;
}
if (this.parent) { code += `${codeIndent(indent - 1)}}`; }
if (this.parent && !(this.parent instanceof ClientScriptFunction)) {
code += `${codeIndent(indent - 1)}}`;
}
return code;
}
getOpcodes(calli: ClientscriptObfuscation) {
@@ -244,7 +264,7 @@ function retargetJumps(calli: ClientscriptObfuscation, code: ClientScriptOp[], f
}
}
export class BinaryConditionalOpStatement extends AstNode {
export class BranchingStatement extends AstNode {
op: ClientScriptOp;
knownStackDiff = new StackInOut(new StackList(["int", "int"]), new StackList(["int"]));//TODO not correct, we also use this for longs
constructor(opcodeinfo: ClientScriptOp, originalindex: number) {
@@ -257,24 +277,24 @@ export class BinaryConditionalOpStatement extends AstNode {
}
getOpcodes(calli: ClientscriptObfuscation) {
if (this.children.length != 2) { throw new Error("unexpected"); }
let left = this.children[0].getOpcodes(calli);
let right = this.children[1].getOpcodes(calli);
let ops: ClientScriptOp[] = [];
if (this.op.opcode == namedClientScriptOps.shorting_or) {
//retarget true jumps to true outcome of combined statement
retargetJumps(calli, left, 1, right.length + 1);
//index 0 [false] will already point to start of right condition
} else if (this.op.opcode == namedClientScriptOps.shorting_and) {
//retarget the false jumps to one past end [false] of combined statement
retargetJumps(calli, left, 0, right.length);
//retarget true jumps to start of right statement
retargetJumps(calli, left, 1, 0);
} else {
ops.push({ opcode: this.op.opcode, imm: 1, imm_obj: null });
if (this.op.opcode == namedClientScriptOps.shorting_or || this.op.opcode == namedClientScriptOps.shorting_and) {
if (this.children.length != 2) { throw new Error("unexpected"); }
let left = this.children[0].getOpcodes(calli);
let right = this.children[1].getOpcodes(calli);
if (this.op.opcode == namedClientScriptOps.shorting_or) {
//retarget true jumps to true outcome of combined statement
retargetJumps(calli, left, 1, right.length + 1);
//index 0 [false] will already point to start of right condition
} else {
//retarget the false jumps to one past end [false] of combined statement
retargetJumps(calli, left, 0, right.length);
//retarget true jumps to start of right statement
retargetJumps(calli, left, 1, 0);
}
return [...left, ...right];
}
return [...left, ...right, ...ops];
let op: ClientScriptOp = { opcode: this.op.opcode, imm: 1, imm_obj: null };
return this.children.flatMap(q => q.getOpcodes(calli)).concat(op);
}
}
@@ -345,7 +365,8 @@ export class SwitchStatementNode extends AstNode {
}
let defaultblock: CodeBlockNode | null = nodes.find(q => q.originalindex == switchop.originalindex + 1) ?? null;
if (defaultblock && defaultblock.children.length == 1 && defaultblock.children[0] instanceof RawOpcodeNode && defaultblock.children[0].opinfo.id == namedClientScriptOps.jump) {
let defaultblockjump = getSingleChild(defaultblock, RawOpcodeNode);
if (defaultblock && defaultblockjump && defaultblockjump.opinfo.id == namedClientScriptOps.jump) {
if (defaultblock.possibleSuccessors.length != 1) { throw new Error("jump successor branch expected"); }
defaultblock = defaultblock.possibleSuccessors[0];
if (defaultblock.originalindex == endindex) {
@@ -374,7 +395,6 @@ export class SwitchStatementNode extends AstNode {
res += `${codeIndent(indent + 1)}default:`;
res += this.defaultbranch.getCode(calli, indent + 1);
}
res += `\n`;
res += `${codeIndent(indent)}}`;
return res;
}
@@ -382,34 +402,43 @@ export class SwitchStatementNode extends AstNode {
let body: ClientScriptOp[] = [];
if (this.valueop) { body.push(...this.valueop.getOpcodes(calli)); }
let jump = calli.getNamedOp(namedClientScriptOps.jump);
let switchop = calli.getNamedOp(namedClientScriptOps.switch);
body.push({ opcode: switchop.id, imm: -1, imm_obj: null });//TODO switch map id
let switchopinfo = calli.getNamedOp(namedClientScriptOps.switch);
let switchop: ClientScriptOp = { opcode: switchopinfo.id, imm: -1, imm_obj: [] };
let defaultjmp: ClientScriptOp = { opcode: jump.id, imm: -1, imm_obj: null };
body.push(switchop);//TODO switch map id
let jumpstart = body.length;
body.push(defaultjmp);
//body.push switch
let endops: ClientScriptOp[] = [];
let jumptable: { value: number, label: number }[] = [];
let lastblock: CodeBlockNode | null = null;
let lastblockindex = 0;
for (let i = 0; i < this.branches.length; i++) {
let branch = this.branches[i];
//TODO write jump table
if (branch.block == lastblock) { continue; }
lastblock = branch.block;
lastblockindex = body.length;
body.push(...branch.block.getOpcodes(calli));
//no jump at last branch
if (this.defaultbranch || i != this.branches.length - 1) {
//add a jump so the previous branch skips to end (and last branch doesn't)
if (branch.block == lastblock) {
jumptable.push({ value: branch.value, label: lastblockindex });
continue;
}
if (lastblock) {
let jmp: ClientScriptOp = { opcode: jump.id, imm: -1, imm_obj: null };
body.push(jmp);
endops.push(jmp);
}
lastblock = branch.block;
lastblockindex = body.length - jumpstart;
jumptable.push({ value: branch.value, label: lastblockindex });
body.push(...branch.block.getOpcodes(calli));
}
if (this.defaultbranch) {
if (lastblock) {
let jmp: ClientScriptOp = { opcode: jump.id, imm: -1, imm_obj: null };
body.push(jmp);
endops.push(jmp);
}
defaultjmp.imm = body.length - body.indexOf(defaultjmp) - 1;
body.push(...this.defaultbranch.getOpcodes(calli));
} else {
@@ -422,6 +451,8 @@ export class SwitchStatementNode extends AstNode {
op.imm = body.length - index - 1;
}
switchop.imm_obj = jumptable;
return body;
}
}
@@ -467,7 +498,8 @@ export class IfStatementNode extends AstNode {
if (this.falsebranch) {
res += `else`;
//skip brackets for else if construct
if (this.falsebranch instanceof CodeBlockNode && this.falsebranch.children.length == 1 && this.falsebranch.children[0] instanceof IfStatementNode) {
let subif = getSingleChild(this.falsebranch, IfStatementNode);
if (subif) {
res += " " + this.falsebranch.children[0].getCode(calli, indent);
} else {
res += this.falsebranch.getCode(calli, indent);
@@ -481,7 +513,8 @@ export class IfStatementNode extends AstNode {
let falsebranch: ClientScriptOp[] = [];
if (this.falsebranch) {
falsebranch = this.falsebranch.getOpcodes(calli);
retargetJumps(calli, truebranch, 0, falsebranch.length)
truebranch.push({ opcode: calli.getNamedOp(namedClientScriptOps.jump).id, imm: falsebranch.length, imm_obj: null });
// retargetJumps(calli, truebranch, 0, falsebranch.length)
}
//TODO rerouting true jumps past 2 in order to switch them with false at 1, this is stupid
retargetJumps(calli, cond, 0, truebranch.length == 1 ? 2 : truebranch.length);
@@ -530,7 +563,7 @@ export class RawOpcodeNode extends AstNode {
if (typeof this.op.imm_obj == "string") { return `"${this.op.imm_obj.replace(/(["\\])/g, "\\$1")}"`; }
else { return "" + this.op.imm_obj; }
}
if (this.op.opcode == namedClientScriptOps.pushlocalint || this.op.opcode == namedClientScriptOps.pushlocalstring || this.op.opcode == namedClientScriptOps.pushvar) {
if (this.op.opcode == namedClientScriptOps.pushlocalint || this.op.opcode == namedClientScriptOps.poplocallong || this.op.opcode == namedClientScriptOps.pushlocalstring || this.op.opcode == namedClientScriptOps.pushvar) {
return getOpcodeName(calli, this.op);
}
if (this.op.opcode == namedClientScriptOps.joinstring) {
@@ -694,7 +727,7 @@ export function translateAst(ast: CodeBlockNode) {
//merge variable assign nodes
let currentassignnode: VarAssignNode | null = null;
for (let node = cursor.goToStart(); node; node = cursor.next()) {
let isassign = node instanceof RawOpcodeNode && (node.op.opcode == namedClientScriptOps.poplocalint || node.op.opcode == namedClientScriptOps.poplocalstring || node.op.opcode == namedClientScriptOps.popvar)
let isassign = node instanceof RawOpcodeNode && (node.op.opcode == namedClientScriptOps.poplocalint || node.op.opcode == namedClientScriptOps.poplocallong || node.op.opcode == namedClientScriptOps.poplocalstring || node.op.opcode == namedClientScriptOps.popvar)
if (isassign) {
if (currentassignnode && currentassignnode.parent != node.parent) {
throw new Error("ast is expected to be flat at this stage");
@@ -766,6 +799,16 @@ function fixControlFlow(ast: AstNode, scriptjson: clientscript) {
let cursor = new RewriteCursor(ast);
//find if statements
for (let node = cursor.goToStart(); node; node = cursor.next()) {
if (node instanceof IfStatementNode) {
//detect an or statement that wasn't caught before (a bit late, there should be a better way to do this)
let subif = getSingleChild(node.falsebranch, IfStatementNode);
if (subif && subif.truebranch == node.truebranch) {
let combined = new BranchingStatement({ opcode: namedClientScriptOps.shorting_or, imm: 0, imm_obj: null }, node.statement.originalindex);
combined.push(node.statement);
combined.push(subif.statement);
node.setBranches(combined, node.truebranch, subif.falsebranch, subif.ifEndIndex);
}
}
if (node instanceof RawOpcodeNode && branchInstructions.includes(node.opinfo.id)) {
let parent = node.parent;
if (!(parent instanceof CodeBlockNode) || parent.possibleSuccessors.length != 2) { throw new Error("if op parent is not compatible"); }
@@ -774,7 +817,8 @@ function fixControlFlow(ast: AstNode, scriptjson: clientscript) {
let trueblock = parent.possibleSuccessors[1];
let falseblock: CodeBlockNode | null = parent.possibleSuccessors[0];
if (falseblock.children.length == 1 && falseblock.children[0] instanceof RawOpcodeNode && falseblock.children[0].opinfo.id == namedClientScriptOps.jump) {
let falseblockjump = getSingleChild(falseblock, RawOpcodeNode);
if (falseblockjump && falseblockjump.opinfo.id == namedClientScriptOps.jump) {
if (falseblock.possibleSuccessors.length != 1) { throw new Error("jump successor branch expected"); }
falseblock = falseblock.possibleSuccessors[0];
if (falseblock == parent.branchEndNode) {
@@ -802,22 +846,22 @@ function fixControlFlow(ast: AstNode, scriptjson: clientscript) {
falseblock = newblock;
}
let condnode = new BinaryConditionalOpStatement(node.op, node.originalindex);
let condnode = new BranchingStatement(node.op, node.originalindex);
condnode.pushList(node.children);
let grandparent = parent?.parent;
if (parent instanceof CodeBlockNode && grandparent instanceof IfStatementNode && grandparent.ifEndIndex == parent.branchEndNode.originalindex) {
let isor = grandparent.truebranch == trueblock && grandparent.falsebranch == parent;
let isand = grandparent.falsebranch == falseblock && grandparent.truebranch == parent;
let isand = condnode.children.length <= 2 && grandparent.falsebranch == falseblock && grandparent.truebranch == parent;
if (isor || isand) {
if (parent.children.length != 1) {
parent.remove(node);
condnode.pushList(parent.children);
//TODO make some sort of in-line codeblock node for this
// console.log("merging if statements while 2nd if wasn't parsed completely, stack will be invalid");
parent.remove(node);
//TODO make some sort of in-line codeblock node for this
// console.log("merging if statements while 2nd if wasn't parsed completely, stack will be invalid");
while (parent.children.length != 0) {
condnode.unshift(parent.children[0]);
}
let fakeop: ClientScriptOp = { opcode: isor ? namedClientScriptOps.shorting_or : namedClientScriptOps.shorting_and, imm: 0, imm_obj: null };
let combinedcond = new BinaryConditionalOpStatement(fakeop, grandparent.originalindex);
let combinedcond = new BranchingStatement(fakeop, grandparent.originalindex);
combinedcond.push(grandparent.statement);
combinedcond.push(condnode);
if (isor) {
@@ -855,6 +899,11 @@ function fixControlFlow(ast: AstNode, scriptjson: clientscript) {
if (codeblock.originalindex != target) { continue; }
if (codeblock.children.at(-1) != ifnode) { throw new Error("unexpected"); }
//TODO this is silly, there might be more instructions in the enclosing block, make sure these aren't lost
//mostly seems to affect expansions of var++ and ++var constructs which are currently not supported
for (let i = codeblock.children.length - 2; i >= 0; i--) {
ifnode.statement.unshift(codeblock.children[i]);
}
let originalparent = codeblock.parent;
let loopstatement = WhileLoopStatementNode.fromIfStatement(codeblock.originalindex, ifnode);
originalparent.replaceChild(codeblock, loopstatement);
@@ -1147,6 +1196,46 @@ export function parseClientScriptIm(calli: ClientscriptObfuscation, script: clie
}
globalThis.parseClientScriptIm = parseClientScriptIm;
export function astToImJson(calli: ClientscriptObfuscation, func: ClientScriptFunction) {
let opdata = func.getOpcodes(calli);
let allargs = func.argtype.getStackdiff();
let script: clientscript = {
byte0: 0,
switchsize: -1,
switches: [],
longargcount: allargs.long,
stringargcount: allargs.string,
intargcount: allargs.int,
locallongcount: allargs.long,
localstringcount: allargs.string,
localintcount: allargs.int,
instructioncount: opdata.length,
opcodedata: opdata,
}
for (let op of opdata) {
if (op.opcode == namedClientScriptOps.poplocalint || op.opcode == namedClientScriptOps.pushlocalint) { script.localintcount = Math.max(script.localintcount, op.imm + 1); }
if (op.opcode == namedClientScriptOps.poplocallong || op.opcode == namedClientScriptOps.pushlocallong) { script.locallongcount = Math.max(script.locallongcount, op.imm + 1); }
if (op.opcode == namedClientScriptOps.poplocalstring || op.opcode == namedClientScriptOps.pushlocalstring) { script.localstringcount = Math.max(script.localstringcount, op.imm + 1); }
if (op.opcode == namedClientScriptOps.switch) {
op.imm = script.switches.push(op.imm_obj as any) - 1;
op.imm_obj = null;
}
}
//1+foreach(2+sublen*(4+4))
script.switchsize = 1 + script.switches.reduce((a, v) => a + 2 + v.length * (4 + 4), 0);
return script;
}
export async function compileClientScript(source: CacheFileSource, code: string) {
let calli = await prepareClientScript(source);
let parseresult = clientscriptParser(calli).runparse(code);
if (!parseresult.success) { throw new Error("failed to parse clientscript", { cause: parseresult.failedOn }); }
if (parseresult.remaining != "") { throw new Error("failed to parse clientscript, left over: " + parseresult.remaining.slice(0, 100)); }
return astToImJson(calli, parseresult.result);
}
export async function renderClientScript(source: CacheFileSource, buf: Buffer, fileid: number) {
let calli = await prepareClientScript(source);
let script = parse.clientscript.read(buf, source);

View File

@@ -516,6 +516,9 @@ export class ClientscriptObfuscation {
} else if (op.type == "int") {
state.buffer.writeInt32BE(v.imm, state.scan);
state.scan += 4;
} else if (op.type == "tribyte") {
state.buffer.writeUIntBE(v.imm, state.scan, 3);
state.scan += 3;
} else if (op.type == "switch") {
if (!("imm_obj" in v)) { throw new Error("imm_obj prop expected"); }
state.buffer.writeUInt8(v.imm, state.scan);
@@ -531,7 +534,7 @@ export class ClientscriptObfuscation {
state.scan += 8;
} else if (v.imm == 2) {
if (typeof v.imm_obj != "string") { throw new Error("string expected"); }
state.buffer.write(v.imm_obj, "latin1");
state.buffer.write(v.imm_obj, state.scan, "latin1");
state.scan += v.imm_obj.length;
state.buffer.writeUint8(0, state.scan);
state.scan++;

View File

@@ -1,10 +1,15 @@
import { has, hasMore, parse, optional, invert, isEnd } from "../libs/yieldparser";
import { AstNode, BinaryConditionalOpStatement, CodeBlockNode, FunctionBindNode, IfStatementNode, RawOpcodeNode, VarAssignNode, WhileLoopStatementNode, SwitchStatementNode, ClientScriptFunction } from "./ast";
import { AstNode, BranchingStatement, CodeBlockNode, FunctionBindNode, IfStatementNode, RawOpcodeNode, VarAssignNode, WhileLoopStatementNode, SwitchStatementNode, ClientScriptFunction, astToImJson } from "./ast";
import { ClientscriptObfuscation } from "./callibrator";
import { binaryOpIds, binaryOpSymbols, knownClientScriptOpNames, namedClientScriptOps, variableSources, StackDiff, StackInOut, StackList, StackTypeExt } from "./definitions";
import prettyJson from "json-stringify-pretty-compact";
const whitespace = /^\s*/;
function* whitespace() {
while (true) {
let match = yield [/^\/\/.*$/m, /^\/\*[\s\S]*\*\//, /^\s+/, ""];
if (match === "") { break; }
}
}
const newline = /^\s*?\n/;
const unmatchable = /$./;
const reserverd = "if,while,break,continue,else,switch,strcat,script".split(",");
@@ -37,13 +42,14 @@ export function clientscriptParser(deob: ClientscriptObfuscation) {
}
function* stackdiff() {
let [match, int, long, string, vararg] = yield (/^\((\d+),(\d+),(\d+),(\d+)\)/);
let [match, int, long, string, vararg] = yield (/^\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
return new StackDiff(+int, +long, +string, +vararg);
}
function* stacklist() {
let items: StackTypeExt[] = [];
while (items.length == 0 || (yield has(","))) {
yield whitespace;
let match = yield [stackdiff, "int", "long", "string", "vararg", ""];
if (match == "") {
if (items.length == 0) { break; }
@@ -308,9 +314,9 @@ export function clientscriptParser(deob: ClientscriptObfuscation) {
} else if (m[1] == "int") {
let popintop = getopinfo(namedClientScriptOps.pushlocalint);
return new RawOpcodeNode(-1, { opcode: popintop.id, imm: varid, imm_obj: null }, popintop);
// } else if (m[1] == "long") {
// let popintop=getopinfo(namedClientScriptOps.poplocalint);
// return new RawOpcodeNode(-1, { opcode: poplongop.id, imm: varid, imm_obj: null }, poplongop);
} else if (m[1] == "long") {
let poplongop = getopinfo(namedClientScriptOps.pushlocallong);
return new RawOpcodeNode(-1, { opcode: poplongop.id, imm: varid, imm_obj: null }, poplongop);
} else if (m[1] == "string") {
let popstringop = getopinfo(namedClientScriptOps.pushlocalstring);
return new RawOpcodeNode(-1, { opcode: popstringop.id, imm: varid, imm_obj: null }, popstringop);
@@ -336,6 +342,7 @@ export function clientscriptParser(deob: ClientscriptObfuscation) {
let condition = yield valueStatement;
yield whitespace;
yield ")";
yield whitespace;
let code = yield codeBlock;
return new WhileLoopStatementNode(-1, condition, code);
}
@@ -352,7 +359,7 @@ export function clientscriptParser(deob: ClientscriptObfuscation) {
if (!opid) { throw new Error("unexpected"); }
let node: AstNode;
if (binaryconditionals.includes(op)) {
node = new BinaryConditionalOpStatement({ opcode: opid, imm: 0, imm_obj: null }, -1);
node = new BranchingStatement({ opcode: opid, imm: 0, imm_obj: null }, -1);
} else {
node = new RawOpcodeNode(-1, { opcode: opid, imm: 0, imm_obj: null }, deob.getNamedOp(opid));
}
@@ -370,7 +377,14 @@ export function clientscriptParser(deob: ClientscriptObfuscation) {
}
let statements = yield statementlist;
yield ")";
let node = new BinaryConditionalOpStatement(op, -1);
let opid = binaryOpIds.get(op);
if (!opid) { throw new Error("unexpected"); }
let node: AstNode;
if (binaryconditionals.includes(op)) {
node = new BranchingStatement({ opcode: opid, imm: 0, imm_obj: null }, -1);
} else {
node = new RawOpcodeNode(-1, { opcode: opid, imm: 0, imm_obj: null }, deob.getNamedOp(opid));
}
node.pushList(statements);
return node;
}
@@ -432,23 +446,32 @@ export function clientscriptParser(deob: ClientscriptObfuscation) {
//TODO remove
globalThis.testy = async () => {
const fs = require("fs");
const fs = require("fs") as typeof import("fs");
let codefs = await globalThis.cli("extract -m clientscripttext -i 0-1999");
let codefiles = [...codefs.extract.filesMap.values()].map(q => q.data.replace(/^\d+:/gm, "")); 1;
let codefiles = [...codefs.extract.filesMap.values()].map(q => q.data.replace(/^\d+:/gm, m => " ".repeat(m.length))); 1;
let jsonfs = await globalThis.cli("extract -m clientscript -i 0-1999");
jsonfs.extract.filesMap.delete(".schema-clientscript.json");
let jsonfiles = [...jsonfs.extract.filesMap.values()];
let subtest = (index: number) => {
let parseresult = clientscriptParser(globalThis.deob).runparse(codefiles[index]);
const deob = globalThis.deob as ClientscriptObfuscation;
let parseresult = clientscriptParser(deob).runparse(codefiles[index]);
if (!parseresult.success) { return parseresult; }
let roundtripped = prettyJson(parseresult.result.getOpcodes(globalThis.deob));
let jsondata = JSON.parse(jsonfiles[index].data).opcodedata;
jsondata.forEach(q => { delete q.opname });
let original = prettyJson(jsondata);
let roundtripped = astToImJson(deob, parseresult.result);
let jsondata = JSON.parse(jsonfiles[index].data);
delete jsondata.$schema;
jsondata.opcodedata.forEach(q => { delete q.opname });
let original = prettyJson(jsondata.opcodedata);
fs.writeFileSync("C:/Users/wilbe/tmp/clinetscript/json1.json", original);
fs.writeFileSync("C:/Users/wilbe/tmp/clinetscript/json2.json", roundtripped);
return { roundtripped, original, exact: original == roundtripped };
let rawinput = prettyJson(jsondata);
let rawroundtrip = prettyJson(roundtripped);
fs.writeFileSync("C:/Users/wilbe/tmp/clinetscript/raw1.json", rawinput);
fs.writeFileSync("C:/Users/wilbe/tmp/clinetscript/raw2.json", rawroundtrip);
fs.writeFileSync("C:/Users/wilbe/tmp/clinetscript/json1.json", prettyJson(jsondata.opcodedata));
fs.writeFileSync("C:/Users/wilbe/tmp/clinetscript/json2.json", prettyJson(roundtripped.opcodedata));
fs.writeFileSync("C:/Users/wilbe/tmp/clinetscript/js1.js", codefiles[index]);
fs.writeFileSync("C:/Users/wilbe/tmp/clinetscript/js2.js", parseresult.result.getCode(deob, 0));
return { roundtripped, original, exact: rawinput == rawroundtrip };
}
return { subtest, codefiles, codefs, jsonfs, jsonfiles };
}

View File

@@ -23,6 +23,8 @@ export const namedClientScriptOps = {
poplocalint: 34,
pushlocalstring: 35,
poplocalstring: 36,
pushlocallong: 9102,//TODO find this one
poplocallong: 9103,//TODO find this one
//variable number of args
joinstring: 37,
@@ -157,7 +159,7 @@ export type ImmediateType = "byte" | "int" | "tribyte" | "switch" | "long" | "st
export type ClientScriptOp = {
opcode: number,
imm: number,
imm_obj: string | number | [number, number] | null,
imm_obj: string | number | [number, number] | { value: number, label: number }[] | null,
opname?: string
}

View File

@@ -43,7 +43,8 @@ export type DecodeState = {
}
export type EncodeState = {
scan: number
scan: number,
endoffset: number,
buffer: Buffer,
args: Record<string, unknown>
}
@@ -311,11 +312,13 @@ function structParser(args: unknown[], parent: ChunkParentCallback, typedef: Typ
if (typeof value != "object" || !value) { throw new Error("object expected"); }
for (let key of keys) {
let propvalue = value[key as string];
let refarray = refs[key];
if (refarray) {
propvalue = propvalue ?? 0;
for (let ref of refarray) {
propvalue = ref.resolve(value, propvalue);
if (propvalue == null) {
let refarray = refs[key];
propvalue ??= 0;
if (refarray) {
for (let ref of refarray) {
propvalue = ref.resolve(value, propvalue);
}
}
}
let prop = props[key];
@@ -1180,7 +1183,12 @@ const hardcodes: Record<string, (args: unknown[], parent: ChunkParentCallback, t
return res;
},
write(state, v) {
throw new Error("not implemented");
let oldscan = state.scan;
subtype.write(state, v);
let len = state.scan - oldscan;
state.buffer.copyWithin(state.endoffset - len, oldscan, state.scan);
state.scan = oldscan;
state.endoffset -= len;
},
getTypescriptType(indent) {
return subtype.getTypescriptType(indent);
@@ -1318,7 +1326,7 @@ const hardcodes: Record<string, (args: unknown[], parent: ChunkParentCallback, t
return `{\n`
+ `${newindent}opcode:number,\n`
+ `${newindent}imm:number,\n`
+ `${newindent}imm_obj:number|string|[number,number]|null,\n`
+ `${newindent}imm_obj:number|string|[number,number]|{value:number,label:number}[]|null,\n`
+ `${indent}}`;
}
}

View File

@@ -1,16 +1,21 @@
["struct",
["byte0",["match","buildnr",{">458":"ubyte","other":0}]],
["switchsize",["match","buildnr",{">=495":["footer",2,"ushort"],"other":0}]],
// ["byte0",["match","buildnr",{">458":"ubyte","other":0}]],
["byte0","ubyte"],
// ["switchsize",["match","buildnr",{">=495":["footer",2,"ushort"],"other":0}]],
["switchsize",["footer",2,"ushort"]],
["switches",["footer",["ref","switchsize"],
["array",["match","buildnr",{">=495":"ubyte","other":0}],["array","ushort",["struct",
// ["array",["match","buildnr",{">=495":"ubyte","other":0}],["array","ushort",["struct",
["array","ubyte",["array","ushort",["struct",
["value","int"],
["label","uint"]
]]]
]],
["longargcount",["match","buildnr",{">641":["footer",2,"ushort"],"other":0}]],
// ["longargcount",["match","buildnr",{">641":["footer",2,"ushort"],"other":0}]],
["longargcount",["footer",2,"ushort"]],
["stringargcount",["footer",2,"ushort"]],
["intargcount",["footer",2,"ushort"]],
["locallongcount",["match","buildnr",{">641":["footer",2,"ushort"],"other":0}]],
// ["locallongcount",["match","buildnr",{">641":["footer",2,"ushort"],"other":0}]],
["locallongcount",["footer",2,"ushort"]],
["localstringcount",["footer",2,"ushort"]],
["localintcount",["footer",2,"ushort"]],
["instructioncount",["footer",4,"uint"]],

View File

@@ -62,16 +62,21 @@ export class FileParser<T> {
return this.readInternal(state) as T;
}
write(obj: T) {
write(obj: T, args?: Record<string, any>) {
let state: opcode_reader.EncodeState = {
buffer: scratchbuf,
scan: 0,
endoffset: scratchbuf.byteLength,
args: {
clientVersion: 1000//TODO
clientVersion: 1000,//TODO
...args
}
};
this.parser.write(state, obj);
if (state.scan > scratchbuf.byteLength) { throw new Error("tried to write file larger than scratchbuffer size"); }
if (state.scan > state.endoffset) { throw new Error("tried to write file larger than scratchbuffer size"); }
//append footer data to end of normal data
state.buffer.copyWithin(state.scan, state.endoffset, scratchbuf.byteLength);
state.scan += scratchbuf.byteLength - state.endoffset;
//do the weird prototype slice since we need a copy, not a ref
let r: Buffer = Uint8Array.prototype.slice.call(scratchbuf, 0, state.scan);
//clear it for next use

View File

@@ -119,7 +119,7 @@ export async function extractCacheFiles(output: ScriptOutput, outdir: ScriptFS,
}
let logicalid = mode.fileToLogical(source, fileid.index.major, fileid.index.minor, arch[fileid.subindex].fileid);
let newfile = await outdir.readFileBuffer(`${args.mode}-${logicalid.join("_")}.${mode.ext}`);
arch[fileid.subindex].buffer = mode.write(newfile);
arch[fileid.subindex].buffer = await mode.write(newfile, logicalid, source);
}
await archedited();
}
@@ -164,7 +164,7 @@ export async function writeCacheFiles(output: ScriptOutput, source: CacheFileSou
let arch = getarch(archid.major, archid.minor, mode);
let raw = await diffdir.readFileBuffer(file);
let buf = mode.write(raw);
let buf = await mode.write(raw, logicalid, source);
arch.files.push({ subid: archid.subid, file: buf });
continue;

View File

@@ -16,7 +16,7 @@ import { legacyGroups, legacyMajors } from "../cache/legacycache";
import { classicGroups } from "../cache/classicloader";
import { renderCutscene } from "./rendercutscene";
import { prepareClientScript } from "../clientscript/callibrator";
import { renderClientScript } from "../clientscript/ast";
import { compileClientScript, renderClientScript } from "../clientscript/ast";
import { renderRsInterface } from "./renderrsinterface";
@@ -340,7 +340,7 @@ export type DecodeMode<T = Buffer | string> = {
parser?: FileParser<any>,
read(buf: Buffer, fileid: LogicalIndex, source: CacheFileSource): T | Promise<T>,
prepareDump(output: ScriptFS, source: CacheFileSource): Promise<void> | void,
write(file: Buffer): Buffer,
write(file: Buffer, fileid: LogicalIndex, source: CacheFileSource): Buffer | Promise<Buffer>,
combineSubs(files: T[]): T
} & DecodeLookup;
@@ -445,12 +445,18 @@ const decodeInterface2: DecodeModeFactory = () => {
const decodeClientScriptText: DecodeModeFactory = () => {
return {
ext: "txt",
ext: "js",
...noArchiveIndex(cacheMajors.clientscript),
...throwOnNonSimple,
async prepareDump(out, source) { await prepareClientScript(source) },
read(buf, fileid, source) {
return renderClientScript(source, buf, fileid[0]);
},
async write(file, fileid, source) {
let obj = await compileClientScript(source, file.toString("utf8"));
let res = parse.clientscript.write(obj, source.getDecodeArgs());
// throw new Error("exit dryrun");
return res;
}
}
}

View File

@@ -640,7 +640,7 @@ export function FileDisplay(p: { file: UIScriptFile }) {
el = <iframe srcDoc={filedata} sandbox="allow-scripts" style={{ width: "95%", height: "95%" }} />;
} else {
//TODO make this not depend on wether file is Buffer or string
//string types so far: txt, json, batch.json
//string types so far: js, txt, json, batch.json
el = <SimpleTextViewer file={filedata} />;
}
} else {