r2papi
Version:
r2api on top of r2pipe for typescript and js
526 lines (515 loc) • 18 kB
text/typescript
import { R2PipeSync } from "./r2pipe.js";
declare let console: any;
declare let r2: R2PipeSync;
// ("this is just a comment"), -- comments are also part of the runtime
/*
=("//", {
=(obj, {}())
=([obj, comment], 32)
if(eq([obj,comment], 32),
ret()
)
ret(obj)
})
*/
export class EsilToken {
label: string = "";
comment: string = "";
text: string = "";
addr: string = "0"; // for ut64 we use strings for numbers :<
position: number = 0;
constructor(text: string = "", position: number = 0) {
this.text = text;
this.position = position;
}
toString(): string {
return this.text;
}
}
export type EsilNodeType =
| "number"
| "flag"
| "register"
| "operation"
| "none"
| "block"
| "goto"
| "label";
export class EsilNode {
lhs: EsilNode | undefined;
rhs: EsilNode | undefined;
children: EsilNode[];
token: EsilToken;
type: EsilNodeType = "none";
constructor(
token: EsilToken = new EsilToken(),
type: EsilNodeType = "none"
) {
this.token = token;
this.children = [];
}
setSides(lhs: EsilNode, rhs: EsilNode): void {
this.lhs = lhs;
this.rhs = rhs;
}
addChildren(ths: EsilNode, fhs: EsilNode): void {
if (ths !== undefined) {
this.children.push(ths);
}
if (fhs !== undefined) {
this.children.push(fhs);
}
}
toEsil(): string {
if (this.lhs !== undefined && this.rhs !== undefined) {
// XXX handle ?{ }{ }
let left = this.lhs.toEsil();
if (left !== "") {
left += ",";
}
const right = this.rhs.toEsil();
return `${right},${left}${this.token}`;
}
return ""; // this.token.text;
}
toString(): string {
let str = "";
if (this.token.label !== "") {
str += this.token.label + ":\n";
}
if (this.token.addr !== "0") {
// str += "// @ " + this.token.addr + "\n";
}
if (this.token.comment !== "") {
str += "/*" + this.token.comment + "*/\n";
}
if (this.token.toString() === "GOTO") {
if (this.children.length > 0) {
const children = this.children[0];
str += "goto label_" + children.token.position + ";\n";
} else {
// console.log(JSON.stringify(this,null, 2));
const pos = 0;
str += `goto label_${pos};\n`;
}
}
if (this.children.length > 0) {
str += ` (if (${this.rhs})\n`;
for (const children of this.children) {
if (children !== null) {
const x = children.toString();
if (x != "") {
str += ` ${x}\n`;
}
}
}
str += " )\n";
}
if (this.lhs !== undefined && this.rhs !== undefined) {
return str + ` ( ${this.lhs} ${this.token} ${this.rhs} )`;
// return str + `${this.lhs} ${this.token} ${this.rhs}`;
}
return str + this.token.toString();
}
}
export class EsilParser {
r2: R2PipeSync;
stack: EsilNode[]; // must be a stack or a list.. to parse sub expressions we must reset
nodes: EsilNode[];
root: EsilNode;
tokens: EsilToken[];
cur: number = 0;
constructor(r2: R2PipeSync) {
this.r2 = r2;
this.cur = 0;
this.stack = [];
this.nodes = [];
this.tokens = [];
this.root = new EsilNode(new EsilToken("function", 0), "block");
}
toJSON(): string {
if (this.stack.length > 0) {
// return JSON.stringify (this.stack, null, 2);
throw new Error("The ESIL stack is not empty");
}
return JSON.stringify(this.root, null, 2);
}
toEsil(): string {
return this.nodes.map((x) => x.toEsil()).join(",");
}
private optimizeFlags(node: EsilNode) {
if (node.rhs !== undefined) {
this.optimizeFlags(node.rhs);
}
if (node.lhs !== undefined) {
this.optimizeFlags(node.lhs);
}
for (let i = 0; i < node.children.length; i++) {
this.optimizeFlags(node.children[i]);
}
const addr: string = node.toString();
if (+addr > 4096) {
const cname = r2.cmd(`fd.@ ${addr}`);
const fname = cname.trim().split("\n")[0].trim();
if (fname != "" && fname.indexOf("+") === -1) {
node.token.text = fname;
}
}
}
optimize(options: string): void {
if (options.indexOf("flag") != -1) {
this.optimizeFlags(this.root);
}
}
toString(): string {
return this.root.children.map((x) => x.toString()).join(";\n");
}
reset(): void {
this.nodes = [];
this.stack = [];
this.tokens = [];
this.cur = 0;
this.root = new EsilNode(new EsilToken("function", 0), "block");
}
parseRange(from: number, to: number) {
let pos = from;
while (pos < this.tokens.length && pos < to) {
const token = this.peek(pos);
if (!token) {
// console.log("BREAK");
break;
}
// console.log(pos, token);
this.cur = pos;
this.pushToken(token);
pos = this.cur;
pos++;
}
// console.log("done");
}
parseFunction(addr?: string): void {
const ep = this;
function parseAmount(n: number): void {
// console.log("PDQ "+n);
const output = r2.cmd("pie " + n + " @e:scr.color=0");
const lines = output.trim().split("\n");
for (const line of lines) {
if (line.length === 0) {
console.log("Empty");
continue;
}
// console.log("parse", r2.cmd("?v:$$"));
const kv = line.split(" ");
if (kv.length > 1) {
// line != "") {
// console.log("// @ " + kv[0]);
//ep.reset ();
r2.cmd(`s ${kv[0]}`);
ep.parse(kv[1], kv[0]);
ep.optimize("flags,labels");
//console.log(ep.toString());
}
}
// console.log(ep.toString());
}
const oaddr = (r2.cmd("?v $$")).trim();
// const func = r2.cmdj("pdrj"); // XXX this command changes the current seek
if (addr === undefined) {
addr = oaddr;
}
const bbs = r2.cmdj(`afbj@${addr}`); // XXX this command changes the current seek
for (const bb of bbs) {
// console.log("bb_" + bb.addr + ":");
r2.cmd(`s ${bb.addr}`);
parseAmount(bb.ninstr);
}
r2.cmd(`s ${oaddr}`);
}
parse(expr: string, addr?: string): void | never {
const tokens = expr
.trim()
.split(",")
.map((x) => x.trim());
const from = this.tokens.length;
for (const tok of tokens) {
const token = new EsilToken(tok, this.tokens.length);
if (addr !== undefined) {
token.addr = addr;
}
this.tokens.push(token);
}
const to = this.tokens.length;
this.parseRange(from, to);
}
peek(a: number): EsilToken | undefined {
return this.tokens[a];
}
pushToken(tok: EsilToken): void | never {
if (this.isNumber(tok)) {
const node = new EsilNode(tok, "number");
this.stack.push(node);
this.nodes.push(node);
} else if (this.isInternal(tok)) {
const node = new EsilNode(tok, "flag");
this.stack.push(node);
this.nodes.push(node);
} else if (this.isOperation(tok)) {
// run the operation login
} else {
// assume it's a register, so just push the string
const node = new EsilNode(tok, "register");
this.stack.push(node);
this.nodes.push(node);
}
// we need a list of register names to do this check properly
// throw new Error ("Unknown token");
}
private isNumber(expr: EsilToken): boolean {
if (expr.toString().startsWith("0")) {
return true;
}
return +expr > 0;
}
private isInternal(expr: EsilToken): boolean {
const text = expr.toString();
return text.startsWith("$") && text.length > 1;
}
private parseUntil(start: number): EsilNode | null {
const from = start + 1;
let pos = from;
const origStack: any[] = [];
const this_nodes_length = this.nodes.length;
this.stack.forEach((x) => origStack.push(x));
while (pos < this.tokens.length) {
const token = this.peek(pos);
if (!token) {
break;
}
if (token.toString() === "}") {
break;
}
if (token.toString() === "}{") {
// return token;
break;
}
// console.log("peek ", this.tokens[pos]);
pos++;
}
this.stack = origStack;
const to = pos;
this.parseRange(from, to);
const same = this.nodes.length == this_nodes_length;
// console.log("BLOCK ("+ ep.toString());
if (same) {
return null;
}
return this.nodes[this.nodes.length - 1]; // this.tokens.length - 1];
}
private getNodeFor(index: number): null | EsilNode {
const tok = this.peek(index);
if (tok === undefined) {
return null;
}
for (const node of this.nodes) {
if (node.token.position === index) {
return node;
}
}
this.nodes.push(new EsilNode(new EsilToken("label", index), "label"));
return null;
}
private findNodeFor(index: number): null | EsilNode {
for (const node of this.nodes) {
if (node.token.position === index) {
return node;
}
}
return null;
}
private isOperation(expr: EsilToken): never | boolean {
switch (expr.toString()) {
// 1pop1push
case "[1]":
case "[2]":
case "[4]":
case "[8]":
if (this.stack.length >= 1) {
const i1 = this.stack.pop()!;
// TODO: MemoryReferenceNode(i1));
const mn = new EsilNode(i1.token, "operation"); // expr.toString());
this.stack.push(i1); // mn);
} else {
throw new Error("Stack needs more items");
}
return true;
// 1pop1push
case "!":
if (this.stack.length >= 1) {
const i0 = new EsilNode(
new EsilToken("", expr.position),
"none"
);
const i1 = this.stack.pop()!;
const nn = new EsilNode(expr, "operation");
nn.setSides(i0, i1);
this.stack.push(nn);
} else {
throw new Error("Stack needs more items");
}
return true;
case "":
case "}":
case "}{":
// no pops or nothing, just does nothing
return true;
case "DUP":
if (this.stack.length < 1) {
throw new Error("goto cant pop");
} else {
const destNode = this.stack.pop()!;
this.stack.push(destNode);
this.stack.push(destNode);
}
return true;
case "GOTO":
// take previous statement which should be const and add a label
{
const prev = this.peek(expr.position - 1);
if (prev !== null) {
// TODO: check stack
if (this.stack.length < 1) {
throw new Error("goto cant pop");
}
const destNode = this.stack.pop()!;
if (destNode !== null) {
const value: number = 0 | +destNode.toString();
if (value > 0) {
const destToken = this.peek(value);
if (destToken !== undefined) {
destToken.label = "label_" + value;
destToken.comment = "hehe";
const nn = new EsilNode(expr, "goto");
const gn = this.getNodeFor(
destToken.position
);
if (gn != null) {
nn.children.push(gn);
}
this.root.children.push(nn);
} else {
console.error("Cannot find goto node");
}
} else {
console.error("Cannot find dest node for goto");
}
}
}
}
return true;
// controlflow
case "?{": // ESIL_TOKEN_IF
if (this.stack.length >= 1) {
const i0 = new EsilNode(
new EsilToken("if", expr.position),
"none"
);
const i1 = this.stack.pop()!;
const nn = new EsilNode(expr, "operation");
nn.setSides(i0, i1); // left side can be ignored for now.. but we can express this somehow
const trueBlock = this.parseUntil(expr.position);
let falseBlock = null;
// nn.addChildren(trueBlock, falseBlock);
if (trueBlock !== null) {
nn.children.push(trueBlock);
this.nodes.push(trueBlock);
falseBlock = this.parseUntil(
trueBlock.token.position + 1
);
if (falseBlock !== null) {
nn.children.push(falseBlock);
this.nodes.push(falseBlock);
}
}
// console.log("true", trueBlock);
// console.log("false", falseBlock);
// this.stack.push(nn);
this.nodes.push(nn);
this.root.children.push(nn);
if (falseBlock !== null) {
this.cur = falseBlock.token.position;
}
} else {
throw new Error("Stack needs more items");
}
return true;
case "-":
if (this.stack.length >= 2) {
const i0 = this.stack.pop()!;
const i1 = this.stack.pop()!;
const nn = new EsilNode(expr, "operation");
nn.setSides(i0, i1);
if (this.stack.length === 0) {
// this.root.children.push(nn);
}
this.stack.push(nn);
this.nodes.push(nn);
} else {
throw new Error("Stack needs more items");
}
return true;
// 2pop1push
case "<":
case ">":
case "^":
case "&":
case "|":
case "+":
case "*":
case "/":
case ">>=":
case "<<=":
case ">>>=":
case "<<<=":
case ">>>>=":
case "<<<<=":
if (this.stack.length >= 2) {
const i0 = this.stack.pop()!;
const i1 = this.stack.pop()!;
const nn = new EsilNode(expr, "operation");
nn.setSides(i0, i1);
if (this.stack.length === 0) {
// this.root.children.push(nn);
}
this.stack.push(nn);
this.nodes.push(nn);
} else {
throw new Error("Stack needs more items");
}
return true;
// 2pop0push
case "=":
case ":=":
case "-=":
case "+=":
case "==":
case "=[1]":
case "=[2]":
case "=[4]":
case "=[8]":
if (this.stack.length >= 2) {
const i0 = this.stack.pop()!;
const i1 = this.stack.pop()!;
const nn = new EsilNode(expr, "operation");
nn.setSides(i0, i1);
if (this.stack.length === 0) {
this.root.children.push(nn);
}
this.nodes.push(nn);
} else {
throw new Error("Stack needs more items");
}
return true;
}
return false;
}
}