From: Brendan Hansen Date: Sun, 1 Mar 2020 03:39:40 +0000 (-0600) Subject: Initial commit with a substantial amount of code X-Git-Url: https://git.brendanfh.com/?a=commitdiff_plain;h=6e063dc95562219945dbcb1826c657af4a9e70c3;p=wasm-analyzer.git Initial commit with a substantial amount of code --- 6e063dc95562219945dbcb1826c657af4a9e70c3 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4218884 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +clib/%.o: clib/%.c clib/*.h + gcc -DLUA51_TARGET -O2 -o $@ -fPIC -c $< -llua + +clib/%.so: clib/%.o + gcc -shared -o $@ $< -llua + +all: clib/parse_helper.so diff --git a/clib/buffered_file.h b/clib/buffered_file.h new file mode 100644 index 0000000..91ee50c --- /dev/null +++ b/clib/buffered_file.h @@ -0,0 +1,56 @@ +#ifndef BUFFERED_FILE_H +#define BUFFERED_FILE_H + +#include +#include +#include + +#define BUFFERED_FILE_BUFF_SIZE 4096 +typedef struct { + FILE* fp; + int buff_ptr; + char buff[BUFFERED_FILE_BUFF_SIZE]; +} buffered_file; + +buffered_file* buffered_file_open(const char* filepath) { + buffered_file* bf = (buffered_file*) malloc(sizeof(buffered_file)); + + bf->fp = fopen(filepath, "r"); + if (bf->fp == NULL) { + return NULL; + } + bf->buff_ptr = BUFFERED_FILE_BUFF_SIZE; + + return bf; +} + +bool buffered_file_empty(buffered_file* bf) { + return bf->buff_ptr >= BUFFERED_FILE_BUFF_SIZE && feof(bf->fp); +} + +unsigned char buffered_file_read_byte(buffered_file* bf) { + if (buffered_file_empty(bf)) return 0; + + if (bf->buff_ptr >= BUFFERED_FILE_BUFF_SIZE) { + // Need to read more data + fread(bf->buff, BUFFERED_FILE_BUFF_SIZE, 1, bf->fp); + bf->buff_ptr = 0; + } + + return bf->buff[bf->buff_ptr++]; +} + +unsigned char buffered_file_peek_byte(buffered_file* bf) { + if (buffered_file_empty(bf)) return 0; + + if (bf->buff_ptr >= BUFFERED_FILE_BUFF_SIZE) { + // Need to read more data + fread(bf->buff, BUFFERED_FILE_BUFF_SIZE, 1, bf->fp); + bf->buff_ptr = 0; + } + + return bf->buff[bf->buff_ptr]; +} + + +#endif diff --git a/clib/parse_helper.c b/clib/parse_helper.c new file mode 100644 index 0000000..63cd19f --- /dev/null +++ b/clib/parse_helper.c @@ -0,0 +1,115 @@ +#include +#ifdef LUA51_TARGET + #include + #include + #include +#else + #include + #include + #include +#endif + +#include "buffered_file.h" + +#define new(class, var) class* var = (class*) malloc(sizeof(class)); +#define LUA_FUNCTION(fname) int fname(lua_State *L) + +typedef struct { + buffered_file* file; +} Parser; + +LUA_FUNCTION(open_file) { + const char *filepath = lua_tostring(L, 1); + + new(Parser, parser); + parser->file = buffered_file_open(filepath); + + lua_pushlightuserdata(L, parser); + return 1; +} + +LUA_FUNCTION(is_file_empty) { + Parser* parser = (Parser*) lua_touserdata(L, 1); + + lua_pushboolean(L, buffered_file_empty(parser->file) == 1); + return 1; +} + +LUA_FUNCTION(read_byte) { + Parser* parser = (Parser*) lua_touserdata(L, 1); + + unsigned char byte = buffered_file_read_byte(parser->file); + + lua_pushinteger(L, byte); + return 1; +} + +LUA_FUNCTION(peek_byte) { + Parser* parser = (Parser*) lua_touserdata(L, 1); + + unsigned char byte = buffered_file_peek_byte(parser->file); + + lua_pushinteger(L, byte); + return 1; +} + +LUA_FUNCTION(parse_uleb128) { + Parser* parser = (Parser*) lua_touserdata(L, 1); + + unsigned long long result = 0; + int shift = 0; + unsigned char byte; + + while (1) { + byte = buffered_file_read_byte(parser->file); + result |= (byte & 0x7f) << shift; + if ((byte & 0x80) == 0) + break; + + shift += 7; + } + lua_pushnumber(L, result); + return 1; +} + +LUA_FUNCTION(parse_sleb128) { + Parser* parser = (Parser*) lua_touserdata(L, 1); + int size = lua_tointeger(L, 2); + + long long result = 0; + int shift = 0; + + unsigned char byte; + + do { + byte = buffered_file_read_byte(parser->file); + result |= (byte & 0x7f) << shift; + shift += 7; + } while ((byte & 0x80) != 0); + + if ((shift < size) && (byte & 0x40) > 0) + result |= (~0ll << shift); + + lua_pushnumber(L, (double) result); + return 1; +} + +int luaopen_parse_helper(lua_State* L) { + const luaL_Reg functions[] = { + { "open_file", open_file }, + { "read_byte", read_byte }, + { "peek_byte", peek_byte }, + { "is_file_empty", is_file_empty }, + + { "parse_uleb128", parse_uleb128 }, + { "parse_sleb128", parse_sleb128 }, + { NULL, NULL }, + }; + +#ifdef LUA51_TARGET + luaL_register(L, "parse_helper", functions); +#else + luaL_newlib(L, functions); +#endif + return 1; +} diff --git a/clib/parse_helper.so b/clib/parse_helper.so new file mode 100755 index 0000000..50ce0d0 Binary files /dev/null and b/clib/parse_helper.so differ diff --git a/conf.lua b/conf.lua new file mode 100644 index 0000000..3521b8e --- /dev/null +++ b/conf.lua @@ -0,0 +1,6 @@ +function love.conf(t) + t.window.title = "TBD" + t.window.width = 1200 + t.window.height = 900 + t.window.resizable = false +end diff --git a/data/add.wasm b/data/add.wasm new file mode 100644 index 0000000..67b8441 Binary files /dev/null and b/data/add.wasm differ diff --git a/data/main.wasm b/data/main.wasm new file mode 100755 index 0000000..1c41dc8 Binary files /dev/null and b/data/main.wasm differ diff --git a/data/test.bin b/data/test.bin new file mode 100644 index 0000000..39e9efe --- /dev/null +++ b/data/test.bin @@ -0,0 +1 @@ +À»x \ No newline at end of file diff --git a/decompile.lua b/decompile.lua new file mode 100644 index 0000000..fee1557 --- /dev/null +++ b/decompile.lua @@ -0,0 +1,714 @@ +-- Recursive decent parser for the WASM v1.0 binary format +-- Brendan Hansen 2020 + +-- Look in clib folder for C-based libraries +package.cpath = package.cpath .. [[;./clib/?.so]] + +import { + parse_helper = "parse_helper"; + pprint = "lualib.pprint"; + + build_str = "utils:build_str"; +} + +VAL_TYPES = { + i32 = "i32", + i64 = "i64", + f32 = "f32", + f64 = "f64" +} + +function parse_valtype(r) + local val = r:read_byte() + + local valtypes_map = { + [0x7f] = VAL_TYPES.i32; + [0x7E] = VAL_TYPES.i64; + [0x7D] = VAL_TYPES.f32; + [0x7C] = VAL_TYPES.f64; + } + + return valtypes_map[val] +end + +function parse_vector(tp) + return function(r) + local n = r:read_uint(32) + + local v = {} + for i=1, n do + table.insert(v, tp(r)) + end + + return v + end +end + +function parse_byte(r) + return r:read_byte() +end + +function parse_name(r) + local name = parse_vector(parse_byte)(r) + return name +end + +function parse_blocktype(r) + if r:peek_byte() == 0x40 then + r:read_byte() + return {} + else + return { parse_valtype(r) } + end +end + +function parse_functype(r) + assert(r:read_byte() == 0x60, "functype expected 0x60 as first byte") + + local params = parse_vector(parse_valtype)(r) + local results = parse_vector(parse_valtype)(r) + + return { param_types = params, result_types = results } +end + +function parse_limits(r) + local t = r:read_byte() + + local min = r:read_uint(32) + local max = nil + if t == 0x01 then + max = r:read_uint(32) + end + + return { min = min, max = max } +end + +function parse_memtype(r) + local lims = parse_limits(r) + + return lims +end + +function parse_tabletype(r) + local elemtype = parse_elemtype(r) + local limits = parse_limits(r) + + return { lim = limits, et = elemtype } +end + +function parse_elemtype(r) + assert(r:read_byte() == 0x70, "elemtype should be 0x70") + return "funcref" +end + +function parse_globaltype(r) + local valtype = parse_valtype(r) + local ismut = parse_mut(r) == 0x01 + + return { t = valtype, m = ismut } +end + +function parse_mut(r) + local v = r:read_byte() + assert(v == 0x00 or v == 0x01, "mut should be 0x00 or 0x01") + return v +end + +function parse_instr(r) + local instr = r:read_byte() + + return match(instr) { + [0x00] = function() return { "unreachable" } end; + [0x01] = function() return { "nop" } end; + [0x02] = function() + local rt = parse_blocktype(r) + local instrs = {} + + while true do + if r:peek_byte() == 0x0B then + r:read_byte() + break + else + table.insert(instrs, parse_instr(r)) + end + end + + return { "block", rt = rt, instrs = instrs } + end; + [0x03] = function() + local rt = parse_blocktype(r) + local instrs = {} + + while true do + if r:peek_byte() == 0x0B then + r:read_byte() + break + else + table.insert(instrs, parse_instr(r)) + end + end + + return { "loop", rt = rt, instrs = instrs } + end; + [0x04] = function() + local rt = parse_blocktype(r) + local instrs = {} + local else_instrs = {} + local inelse = false + + while true do + local peek = r:peek_byte() + if peek == 0x0B then + r:read_byte() + break + elseif peek == 0x05 then + r:read_byte() + inelse = true + else + if not inelse then + table.insert(instrs, parse_instr(r)) + else + table.insert(else_instrs, parse_instr(r)) + end + end + end + + return { "if", rt = rt, instrs = instrs, else_instrs = else_instrs } + end; + + [0x0C] = function() return { "br", x = parse_labelidx(r) } end; + [0x0D] = function() return { "br_if", x = parse_labelidx(r) } end; + [0x0E] = function() + local labels = parse_vector(parse_labelidx)(r) + local labeln = parse_labelidx(r) + + return { "br_table", labels = labels, labeln = labeln } + end; + + [0x0F] = function() return { "return" } end; + + [0x10] = function() return { "call", x = parse_funcidx(r) } end; + [0x11] = function() + local x = parse_typeidx(r) + assert(r:read_byte() ~= 0x00, "call_indirect expects 0x00 at end") + return { "call_indirect", x = x } + end; + + [0x1A] = function() return { "drop" } end; + [0x1B] = function() return { "select" } end; + + [0x20] = function() return { "local.get", x = parse_localidx(r) } end; + [0x21] = function() return { "local.set", x = parse_localidx(r) } end; + [0x22] = function() return { "local.tee", x = parse_localidx(r) } end; + [0x23] = function() return { "global.get", x = parse_globalidx(r) } end; + [0x24] = function() return { "global.set", x = parse_globalidx(r) } end; + + [0x28] = function() return { "i32.load", x = parse_memarg(r) } end; + [0x29] = function() return { "i64.load", x = parse_memarg(r) } end; + [0x2A] = function() return { "f32.load", x = parse_memarg(r) } end; + [0x2B] = function() return { "f64.load", x = parse_memarg(r) } end; + [0x2C] = function() return { "i32.load8_s", x = parse_memarg(r) } end; + [0x2D] = function() return { "i32.load8_u", x = parse_memarg(r) } end; + [0x2E] = function() return { "i32.load16_s", x = parse_memarg(r) } end; + [0x2F] = function() return { "i32.load16_u", x = parse_memarg(r) } end; + [0x30] = function() return { "i64.load8_s", x = parse_memarg(r) } end; + [0x31] = function() return { "i64.load8_u", x = parse_memarg(r) } end; + [0x32] = function() return { "i64.load16_s", x = parse_memarg(r) } end; + [0x33] = function() return { "i64.load16_u", x = parse_memarg(r) } end; + [0x34] = function() return { "i64.load32_s", x = parse_memarg(r) } end; + [0x35] = function() return { "i64.load32_u", x = parse_memarg(r) } end; + [0x36] = function() return { "i32.store", x = parse_memarg(r) } end; + [0x37] = function() return { "i64.store", x = parse_memarg(r) } end; + [0x38] = function() return { "f32.store", x = parse_memarg(r) } end; + [0x39] = function() return { "f64.store", x = parse_memarg(r) } end; + [0x3A] = function() return { "i32.store8", x = parse_memarg(r) } end; + [0x3B] = function() return { "i32.store16", x = parse_memarg(r) } end; + [0x3C] = function() return { "i64.store8", x = parse_memarg(r) } end; + [0x3D] = function() return { "i64.store16", x = parse_memarg(r) } end; + [0x3E] = function() return { "i64.store32", x = parse_memarg(r) } end; + [0x3F] = function() + assert(r:read_byte() == 0x00, "memory.size expects 0x00") + return { "memory.size" } + end; + [0x40] = function() + assert(r:read_byte() == 0x00, "memory.grow expects 0x00") + return { "memory.grow" } + end; + + [0x41] = function() return { "i32.const", x = r:read_sint(32) } end; + [0x42] = function() return { "i64.const", x = r:read_sint(64) } end; + [0x43] = function() return { "f32.const", x = r:read_float(32) } end; + [0x44] = function() return { "f64.const", x = r:read_float(64) } end; + + [0x45] = function() return { "i32.eqz" } end; + [0x46] = function() return { "i32.eq" } end; + [0x47] = function() return { "i32.ne" } end; + [0x48] = function() return { "i32.lt_s" } end; + [0x49] = function() return { "i32.lt_u" } end; + [0x4A] = function() return { "i32.gt_s" } end; + [0x4B] = function() return { "i32.gt_u" } end; + [0x4C] = function() return { "i32.le_s" } end; + [0x4D] = function() return { "i32.le_u" } end; + [0x4E] = function() return { "i32.ge_s" } end; + [0x4F] = function() return { "i32.ge_u" } end; + [0x50] = function() return { "i64.eqz" } end; + [0x51] = function() return { "i64.eq" } end; + [0x52] = function() return { "i64.ne" } end; + [0x53] = function() return { "i64.lt_s" } end; + [0x54] = function() return { "i64.lt_u" } end; + [0x55] = function() return { "i64.gt_s" } end; + [0x56] = function() return { "i64.gt_u" } end; + [0x57] = function() return { "i64.le_s" } end; + [0x58] = function() return { "i64.le_u" } end; + [0x59] = function() return { "i64.ge_s" } end; + [0x5A] = function() return { "i64.ge_u" } end; + [0x5B] = function() return { "f32.eq" } end; + [0x5C] = function() return { "f32.ne" } end; + [0x5D] = function() return { "f32.lt" } end; + [0x5E] = function() return { "f32.gt" } end; + [0x5F] = function() return { "f32.le" } end; + [0x60] = function() return { "f32.ge" } end; + [0x61] = function() return { "f64.eq" } end; + [0x62] = function() return { "f64.ne" } end; + [0x63] = function() return { "f64.lt" } end; + [0x64] = function() return { "f64.gt" } end; + [0x65] = function() return { "f64.le" } end; + [0x66] = function() return { "f64.ge" } end; + [0x67] = function() return { "i32.clz" } end; + [0x68] = function() return { "i32.ctz" } end; + [0x69] = function() return { "i32.popcnt" } end; + [0x6A] = function() return { "i32.add" } end; + [0x6B] = function() return { "i32.sub" } end; + [0x6C] = function() return { "i32.mul" } end; + [0x6D] = function() return { "i32.div_s" } end; + [0x6E] = function() return { "i32.div_u" } end; + [0x6F] = function() return { "i32.rem_s" } end; + [0x70] = function() return { "i32.rem_u" } end; + [0x71] = function() return { "i32.and" } end; + [0x72] = function() return { "i32.or" } end; + [0x73] = function() return { "i32.xor" } end; + [0x74] = function() return { "i32.shl" } end; + [0x75] = function() return { "i32.shr_s" } end; + [0x76] = function() return { "i32.shr_u" } end; + [0x77] = function() return { "i32.rotl" } end; + [0x78] = function() return { "i32.rotr" } end; + [0x79] = function() return { "i64.clz" } end; + [0x7A] = function() return { "i64.ctz" } end; + [0x7B] = function() return { "i64.popcnt" } end; + [0x7C] = function() return { "i64.add" } end; + [0x7D] = function() return { "i64.sub" } end; + [0x7E] = function() return { "i64.mul" } end; + [0x7F] = function() return { "i64.div_s" } end; + [0x80] = function() return { "i64.div_u" } end; + [0x81] = function() return { "i64.rem_s" } end; + [0x82] = function() return { "i64.rem_u" } end; + [0x83] = function() return { "i64.and" } end; + [0x84] = function() return { "i64.or" } end; + [0x85] = function() return { "i64.xor" } end; + [0x86] = function() return { "i64.shl" } end; + [0x87] = function() return { "i64.shr_s" } end; + [0x88] = function() return { "i64.shr_u" } end; + [0x89] = function() return { "i64.rotl" } end; + [0x8A] = function() return { "i64.rotr" } end; + [0x8B] = function() return { "f32.abs" } end; + [0x8C] = function() return { "f32.neg" } end; + [0x8D] = function() return { "f32.ceil" } end; + [0x8E] = function() return { "f32.floor" } end; + [0x8F] = function() return { "f32.trunc" } end; + [0x90] = function() return { "f32.nearest" } end; + [0x91] = function() return { "f32.sqrt" } end; + [0x92] = function() return { "f32.add" } end; + [0x93] = function() return { "f32.sub" } end; + [0x94] = function() return { "f32.mul" } end; + [0x95] = function() return { "f32.div" } end; + [0x96] = function() return { "f32.min" } end; + [0x97] = function() return { "f32.max" } end; + [0x98] = function() return { "f32.copysign" } end; + [0x99] = function() return { "f64.abs" } end; + [0x9A] = function() return { "f64.neg" } end; + [0x9B] = function() return { "f64.ceil" } end; + [0x9C] = function() return { "f64.floor" } end; + [0x9D] = function() return { "f64.trunc" } end; + [0x9E] = function() return { "f64.nearest" } end; + [0x9F] = function() return { "f64.sqrt" } end; + [0xA0] = function() return { "f64.add" } end; + [0xA1] = function() return { "f64.sub" } end; + [0xA2] = function() return { "f64.mul" } end; + [0xA3] = function() return { "f64.div" } end; + [0xA4] = function() return { "f64.min" } end; + [0xA5] = function() return { "f64.max" } end; + [0xA6] = function() return { "f64.copysign" } end; + + [0xA7] = function() return { "i32.wrap_i64" } end; + [0xA8] = function() return { "i32.trunc_f32_s" } end; + [0xA9] = function() return { "i32.trunc_f32_u" } end; + [0xAA] = function() return { "i32.trunc_f64_s" } end; + [0xAB] = function() return { "i32.trunc_f64_u" } end; + [0xAC] = function() return { "i64.extend_i32_s" } end; + [0xAD] = function() return { "i64.extend_i32_u" } end; + [0xAE] = function() return { "i64.trunc_f32_s" } end; + [0xAF] = function() return { "i64.trunc_f32_u" } end; + [0xB0] = function() return { "i64.trunc_f64_s" } end; + [0xB1] = function() return { "i64.trunc_f64_u" } end; + [0xB2] = function() return { "f32.convert_i32_s" } end; + [0xB3] = function() return { "f32.convert_i32_u" } end; + [0xB4] = function() return { "f32.convert_i64_s" } end; + [0xB5] = function() return { "f32.convert_i64_u" } end; + [0xB6] = function() return { "f32.demote_f64" } end; + [0xB7] = function() return { "f64.convert_i32_s" } end; + [0xB8] = function() return { "f64.convert_i32_u" } end; + [0xB9] = function() return { "f64.convert_i64_s" } end; + [0xBA] = function() return { "f64.convert_i64_u" } end; + [0xBB] = function() return { "f64.promote_f32" } end; + [0xBC] = function() return { "i32.reinterpret_f32" } end; + [0xBD] = function() return { "i64.reinterpret_f64" } end; + [0xBE] = function() return { "f32.reinterpret_i32" } end; + [0xBF] = function() return { "f64.reinterpret_i64" } end; + } +end + +function parse_memarg(r) + local a = r:read_uint(32) + local o = r:read_uint(32) + + return { "memarg"; align = a; offset = o } +end + +function parse_expr(r) + local instrs = {} + while true do + if r:peek_byte() == 0x0B then + r:read_byte() + break + else + table.insert(instrs, parse_instr(r)) + end + end + + return instrs +end + +function parse_typeidx(r) return r:read_uint(32) end +function parse_funcidx(r) return r:read_uint(32) end +function parse_tableidx(r) return r:read_uint(32) end +function parse_memidx(r) return r:read_uint(32) end +function parse_globalidx(r) return r:read_uint(32) end +function parse_localidx(r) return r:read_uint(32) end +function parse_labelidx(r) return r:read_uint(32) end + +function parse_section(r, expectN, B) + if r:peek_byte() ~= expectN then return end + + local N = r:read_byte() + local size = r:read_uint(32) + local cont = B(r) + + return { "section", contents = cont, size = size } +end + +function parse_customsec(r) + local csect = parse_section(r, 0, parse_custom) + if not csect then return nil end + + for i=1, csect.size do + -- Discard csect.size bytes + r:read_byte() + end + return csect +end + +function parse_custom(r) + local name = parse_name(r) + + return { "custom", name = name } +end + +function parse_typesec(r) + return parse_section(r, 1, parse_vector(parse_functype)) +end + +function parse_importsec(r) + return parse_section(r, 2, parse_vector(parse_import)) +end + +function parse_import(r) + local mod = parse_name(r) + local nm = parse_name(r) + local d = parse_importdesc(r) + + return { "import", mod = mod, name = nm, desc = d } +end + +function parse_importdesc(r) + local t = r:read_byte() + if t == 0x00 then return { "func", x = parse_typeidx(r) } + elseif t == 0x01 then return { "table", tt = parse_tabletype(r) } + elseif t == 0x02 then return { "mem", mt = parse_memtype(r) } + elseif t == 0x03 then return { "global", gt = parse_globaltype(r) } + end + + error("bad importdesc") +end + +function parse_funcsec(r) + return parse_section(r, 3, parse_vector(parse_typeidx)) +end + +function parse_tablesec(r) + return parse_section(r, 4, parse_vector(parse_table)) +end + +function parse_table(r) + local tt = parse_tabletype(r) + return { "table", type_ = tt } +end + +function parse_memsec(r) + return parse_section(r, 5, parse_vector(parse_mem)) +end + +function parse_mem(r) + local mt = parse_memtype(r) + return { "mem", type_ = mt } +end + +function parse_globalsec(r) + return parse_section(r, 6, parse_vector(parse_global)) +end + +function parse_global(r) + local gt = parse_globaltype(r) + local e = parse_expr(r) + return { "global", type_ = gt, init = e } +end + +function parse_exportsec(r) + return parse_section(r, 7, parse_vector(parse_export)) +end + +function parse_export(r) + local nm = parse_name(r) + local d = parse_exportdesc(r) + return { "export", name = nm, desc = d } +end + +function parse_exportdesc(r) + local t = r:read_byte() + if t == 0x00 then return { "func", x = parse_typeidx(r) } + elseif t == 0x01 then return { "table", tt = parse_tableidx(r) } + elseif t == 0x02 then return { "mem", mt = parse_memidx(r) } + elseif t == 0x03 then return { "global", gt = parse_globalidx(r) } + end + + error("bad exportdesc: ", t) +end + +function parse_startsec(r) + return parse_section(r, 8, parse_start) +end + +function parse_start(r) + local x = parse_funcidx(r) + return { "start", func = x } +end + +function parse_elemsec(r) + return parse_section(r, 9, parse_vector(parse_elem)) +end + +function parse_elem(r) + local x = parse_tableidx(r) + local e = parse_expr(r) + local y = parse_vector(parse_funcidx)(r) + + return { "elem", table = x, offset = e, init = y } +end + +function parse_codesec(r) + return parse_section(r, 10, parse_vector(parse_code)) +end + +function parse_code(r) + local size = r:read_uint(32) + local code = parse_func(r) + return code +end + +function parse_func(r) + local t = parse_vector(parse_locals)(r) + local e = parse_expr(r) + + local localidx = 0 + local locals = {} + for _, v in ipairs(t) do + for _, l in ipairs(v) do + l.localidx = localidx + table.insert(locals, l) + localidx = localidx + 1 + end + end + + return { "func", locals = locals, body = e } +end + +function parse_locals(r) + local n = r:read_uint(32) + local t = parse_valtype(r) + + --TODO: Make a list of values with names like local0, local1, ... + + local locals = {} + for i = 0, n - 1 do + table.insert(locals, { + name = "local" .. i, + type_ = t, + localidx = i + }) + end + + return locals +end + +function parse_datasec(r) + return parse_section(r, 11, parse_vector(parse_data)) +end + +function parse_data(r) + local x = parse_memidx(r) + local e = parse_expr(r) + local b = parse_vector(parse_byte)(r) + + return { "data", data = x, offset = e, init = b } +end + +function parse_magic(r) + assert(r:read_byte() == 0x00, "magic string is wrong") + assert(r:read_byte() == 0x61, "magic string is wrong") + assert(r:read_byte() == 0x73, "magic string is wrong") + assert(r:read_byte() == 0x6D, "magic string is wrong") +end + +function parse_version(r) + assert(r:read_byte() == 0x01, "version is wrong") + assert(r:read_byte() == 0x00, "version is wrong") + assert(r:read_byte() == 0x00, "version is wrong") + assert(r:read_byte() == 0x00, "version is wrong") +end + +function parse_module(r) + parse_magic(r) + parse_version(r) + + local functypes = parse_typesec(r) + local imports = parse_importsec(r) + local typeidxs = parse_funcsec(r) + local tables = parse_tablesec(r) + local mems = parse_memsec(r) + local globals = parse_globalsec(r) + local exports = parse_exportsec(r) + local start = parse_startsec(r) + local elems = parse_elemsec(r) + local codes = parse_codesec(r) + local data = parse_datasec(r) + + local funcs = {} + local funcidx = 0 + + if imports then + for k, v in ipairs(imports.contents) do + if v.desc[1] == "func" then + funcs[funcidx] = { + name = build_str(v.mod) .. "." .. build_str(v.name); + funcidx = funcidx; + type_ = functypes.contents[v.desc.x + 1]; + } + funcidx = funcidx + 1 + end + end + end + + if codes then + for i=1, #codes.contents do + local locals = codes.contents[i].locals; + local type_ = functypes.contents[typeidxs.contents[i] + 1]; + local param_types = type_.param_types; + + local new_locals = {} + local new_local_idx = 0 + for _, p in ipairs(param_types) do + table.insert(new_locals, { + name = "param" .. new_local_idx, + type_ = p; + localidx = new_local_idx + }) + new_local_idx = new_local_idx + 1 + end + for _, l in ipairs(locals) do + l.localidx = new_local_idx + table.insert(new_locals, l) + new_local_idx = new_local_idx + 1 + end + + funcs[funcidx] = { + funcidx = funcidx; + name = "func" .. funcidx; + type_ = functypes.contents[typeidxs.contents[i] + 1]; + locals = new_locals; + body = codes.contents[i].body + } + funcidx = funcidx + 1 + end + end + + return { + "module"; + types = functypes; + tables = tables; + mems = mems; + globals = globas; + exports = exports; + start = start; + elems = elems; + data = data; + imports = imports; + funcs = funcs; + } +end + + +-- Reader util class used for interfacing +-- with the parse_helper c library +class "Reader" { + init = function(self, filename) + self.wasm_file = parse_helper.open_file(filename) + end; + + read_byte = function(self) + return parse_helper.read_byte(self.wasm_file) + end; + + peek_byte = function(self) + return parse_helper.peek_byte(self.wasm_file) + end; + + -- NOTE: N is unused + read_uint = function(self, N) + return parse_helper.parse_uleb128(self.wasm_file) + end; + + read_sint = function(self, N) + return parse_helper.parse_sleb128(self.wasm_file, N) + end; + + read_float = function(self, N) + return 0.0; + end; +} + +function decompile(filepath) + local reader = Reader(filepath) + + return parse_module(reader) +end + +return module { decompile; VAL_TYPES = VAL_TYPES } diff --git a/docs/todo b/docs/todo new file mode 100644 index 0000000..f3fb67e --- /dev/null +++ b/docs/todo @@ -0,0 +1,2 @@ +After experimenting with using Lua for the parser, I think it will be better to write the parser in C / C++ +and then have a function that converts all the data to a lua table that will be used for visualization / examination. diff --git a/lualib/oop.lua b/lualib/oop.lua new file mode 100755 index 0000000..bf4a9e8 --- /dev/null +++ b/lualib/oop.lua @@ -0,0 +1,316 @@ +--Under MIT License +--Originally developed by Brendan Hansen +--Copyright 2017 + +--[[ found on http://lua-users.org/lists/lua-l/2010-06/msg00314.html ]] +setfenv = setfenv or function(f, t) + f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func) + local name + local up = 0 + repeat + up = up + 1 + name = debug.getupvalue(f, up) + until name == '_ENV' or name == nil + if name then + debug.upvaluejoin(f, up, function() return t end, 1) -- use unique upvalue, set it to f + end +end + +getfenv = getfenv or function(fn) + local i = 1 + while true do + local name, val = debug.getupvalue(fn, i) + if name == "_ENV" then + return val + elseif not name then + break + end + i = i + 1 + end +end + +local function helper(obj, super, f) + return function(...) + local a = { ... } + local data = a[#a] + local env = getfenv(2) + + data.extends = super + env[obj] = f(data) + end +end + + +--[[ + Module System + "imports" are defined by: + import { + NAME = "path.to.lua.file"; + NAME = "path.to.lua.file:specific_import"; + NAME = "path.to.lua.file:"; --this uses the default export (see below) + } + + "exports" are defined by: + return module { + the_default_export_variable; + other_export=some_value; + } +]] + +local ALL_DEPS = {} +function import(reqs) + local function convertPath(req) + local r = req + if req:find(":") ~= nil then + r = req:sub(1, req:find(":") - 1) + end + r = r:gsub("%.", "/") + return r + end + + local function getField(req) + if req:find(":") ~= nil then + if req:sub(-1) == ":" then + return "default" + else + return req:sub(req:find(":")+1) + end + else + return nil + end + end + + local dep = {} + for name, req in pairs(reqs) do + if type(req) ~= "string" then + error "Please use strings for referencing modules" + end + + local mod = require(convertPath(req)) + dep[name] = mod + + if type(mod) == "table" then + local field = getField(req) + if field ~= nil then + if mod[field] ~= nil then + dep[name] = mod[field] + else + dep[name] = nil + end + end + end + end + + local newenv = setmetatable({}, { + __index = function(_, k) + local v = dep[k] + if v == nil then v = _G[k] end + return v + end; + __newindex = function(_, k, v) + dep[k] = v; + end; + }) + setfenv(2, newenv) +end + +function module(exp) + exp["default"] = exp[1] + exp[1] = nil + return exp +end + +--[[ + Data - basically tables that can only hold numbers, strings, bools, tables, or nil (no functions) +]] +function data(d) + for k, v in pairs(d) do + if type(v) == "function" then + d[k] = nil + end + end + return d +end + +--[[ + Factories - basically tables that can only hold functions, normally static +]] +function factory(f) + for k, v in pairs(f) do + if type(v) ~= "function" then + f[k] = nil + end + end + return f +end + +--[[ + Classes - factories with a initializer and a self-reference, and are allowed to hold variables + + Initialize with "init" + Create new instance by calling name of class + local ClassName = class { ... } + local ClassName = Base.extend { ... } + local instance_a = ClassName(args) + + Can also declare classes as + class "ClassName" { ... } + or + class "ClassName extends Base" { ... } + or + class "ClassName" ["Base"] { ... } +]] +function class(obj) + if type(obj) == "string" then + local sub, super = obj:match "(%w+) +extends +(%w+)" + if sub then obj = sub end + + if super then + return helper(obj, super, class) + else + local o = {} + o = setmetatable(o, { + __call = helper(obj, nil, class); + + __index = function(_, super) + return helper(obj, super, class) + end; + }) + return o + end + end + + local cls = {} + if obj.extends ~= nil then + for k, v in pairs(obj.extends) do + cls[k] = v + end + end + + for k, v in pairs(obj) do + if k ~= "extends" then + cls[k] = v + end + end + + cls = setmetatable(cls, { + __call = function(_, ...) + local o = {} + local mt = {} + + for k, v in pairs(cls) do + if k:sub(0, 2) == "__" then + mt[k] = v + end + end + + mt.__index = cls + o = setmetatable(o, mt) + + if cls.init then + cls.init(o, ...) + end + + return o + end + }) + + cls.extend = function(d) + d.extends = cls + return class(d) + end + + return cls; +end + +--[[ + Singleton - a single instance of a class +]] +function singleton(obj) + return (class(obj))() +end + +function enum(obj) + if type(obj) == "string" then + return helper(obj, nil, enum) + end + + local i = 0 + for k, v in pairs(obj) do + obj[k] = nil + if type(v) == "string" then + obj[v] = i + i = i + 1 + end + end + return obj +end + +function interface(fns) + if type(fns) == "string" then + local sub, super = fns:match "(%w+) +extends +(%w+)" + if sub then fns = sub end + + if super then + return helper(fns, super, interface) + else + local int = {} + int = setmetatable(int, { + __call = helper(fns, nil, interface); + + __index = function(_, super) + return helper(fns, super, interface) + end; + }) + return int + end + end + + local int = {} + for k, v in pairs(fns) do + if type(v) == "string" then + int[v] = function(self) end + end + end + + if fns.extends then + for k, v in pairs(fns.extends) do + if type(v) == "function" then + int[k] = function(self) end + end + end + end + + local mt = { + __call = function(_, ...) + error "Interfaces cannot be instatiated with a constructor" + end; + } + int = setmetatable(int, mt) + + return int +end + +function match(var) + return function(tab) + local ran = false + for k, v in pairs(tab) do + if k ~= "default" and var == k then + return v() + end + end + + if tab["default"] then + tab["default"]() + end + end +end + +function with(o) + return function(tab) + for k, v in pairs(tab) do + o[k] = v + end + return o + end +end diff --git a/lualib/pprint.lua b/lualib/pprint.lua new file mode 100644 index 0000000..2245484 --- /dev/null +++ b/lualib/pprint.lua @@ -0,0 +1,475 @@ +local pprint = { VERSION = '0.1' } + +local depth = 1 + +pprint.defaults = { + -- If set to number N, then limit table recursion to N deep. + depth_limit = false, + -- type display trigger, hide not useful datatypes by default + -- custom types are treated as table + show_nil = true, + show_boolean = true, + show_number = true, + show_string = true, + show_table = true, + show_function = false, + show_thread = false, + show_userdata = false, + -- additional display trigger + show_metatable = false, -- show metatable + show_all = false, -- override other show settings and show everything + use_tostring = false, -- use __tostring to print table if available + filter_function = nil, -- called like callback(value[,key, parent]), return truty value to hide + object_cache = 'local', -- cache blob and table to give it a id, 'local' cache per print, 'global' cache + -- per process, falsy value to disable (might cause infinite loop) + -- format settings + indent_size = 2, -- indent for each nested table level + level_width = 80, -- max width per indent level + wrap_string = true, -- wrap string when it's longer than level_width + wrap_array = false, -- wrap every array elements + sort_keys = true, -- sort table keys +} + +local TYPES = { + ['nil'] = 1, ['boolean'] = 2, ['number'] = 3, ['string'] = 4, + ['table'] = 5, ['function'] = 6, ['thread'] = 7, ['userdata'] = 8 +} + +-- seems this is the only way to escape these, as lua don't know how to map char '\a' to 'a' +local ESCAPE_MAP = { + ['\a'] = '\\a', ['\b'] = '\\b', ['\f'] = '\\f', ['\n'] = '\\n', ['\r'] = '\\r', + ['\t'] = '\\t', ['\v'] = '\\v', ['\\'] = '\\\\', +} + +-- generic utilities +local function escape(s) + s = s:gsub('([%c\\])', ESCAPE_MAP) + local dq = s:find('"') + local sq = s:find("'") + if dq and sq then + return s:gsub('"', '\\"'), '"' + elseif sq then + return s, '"' + else + return s, "'" + end +end + +local function is_plain_key(key) + return type(key) == 'string' and key:match('^[%a_][%a%d_]*$') +end + +local CACHE_TYPES = { + ['table'] = true, ['function'] = true, ['thread'] = true, ['userdata'] = true +} + +-- cache would be populated to be like: +-- { +-- function = { `fun1` = 1, _cnt = 1 }, -- object id +-- table = { `table1` = 1, `table2` = 2, _cnt = 2 }, +-- visited_tables = { `table1` = 7, `table2` = 8 }, -- visit count +-- } +-- use weakrefs to avoid accidentall adding refcount +local function cache_apperance(obj, cache, option) + if not cache.visited_tables then + cache.visited_tables = setmetatable({}, {__mode = 'k'}) + end + local t = type(obj) + + -- TODO can't test filter_function here as we don't have the ix and key, + -- might cause different results? + -- respect show_xxx and filter_function to be consistent with print results + if (not TYPES[t] and not option.show_table) + or (TYPES[t] and not option['show_'..t]) then + return + end + + if CACHE_TYPES[t] or TYPES[t] == nil then + if not cache[t] then + cache[t] = setmetatable({}, {__mode = 'k'}) + cache[t]._cnt = 0 + end + if not cache[t][obj] then + cache[t]._cnt = cache[t]._cnt + 1 + cache[t][obj] = cache[t]._cnt + end + end + if t == 'table' or TYPES[t] == nil then + if cache.visited_tables[obj] == false then + -- already printed, no need to mark this and its children anymore + return + elseif cache.visited_tables[obj] == nil then + cache.visited_tables[obj] = 1 + else + -- visited already, increment and continue + cache.visited_tables[obj] = cache.visited_tables[obj] + 1 + return + end + for k, v in pairs(obj) do + cache_apperance(k, cache, option) + cache_apperance(v, cache, option) + end + local mt = getmetatable(obj) + if mt and option.show_metatable then + cache_apperance(mt, cache, option) + end + end +end + +-- makes 'foo2' < 'foo100000'. string.sub makes substring anyway, no need to use index based method +local function str_natural_cmp(lhs, rhs) + while #lhs > 0 and #rhs > 0 do + local lmid, lend = lhs:find('%d+') + local rmid, rend = rhs:find('%d+') + if not (lmid and rmid) then return lhs < rhs end + + local lsub = lhs:sub(1, lmid-1) + local rsub = rhs:sub(1, rmid-1) + if lsub ~= rsub then + return lsub < rsub + end + + local lnum = tonumber(lhs:sub(lmid, lend)) + local rnum = tonumber(rhs:sub(rmid, rend)) + if lnum ~= rnum then + return lnum < rnum + end + + lhs = lhs:sub(lend+1) + rhs = rhs:sub(rend+1) + end + return lhs < rhs +end + +local function cmp(lhs, rhs) + local tleft = type(lhs) + local tright = type(rhs) + if tleft == 'number' and tright == 'number' then return lhs < rhs end + if tleft == 'string' and tright == 'string' then return str_natural_cmp(lhs, rhs) end + if tleft == tright then return str_natural_cmp(tostring(lhs), tostring(rhs)) end + + -- allow custom types + local oleft = TYPES[tleft] or 9 + local oright = TYPES[tright] or 9 + return oleft < oright +end + +-- setup option with default +local function make_option(option) + if option == nil then + option = {} + end + for k, v in pairs(pprint.defaults) do + if option[k] == nil then + option[k] = v + end + if option.show_all then + for t, _ in pairs(TYPES) do + option['show_'..t] = true + end + option.show_metatable = true + end + end + return option +end + +-- override defaults and take effects for all following calls +function pprint.setup(option) + pprint.defaults = make_option(option) +end + +-- format lua object into a string +function pprint.pformat(obj, option, printer) + option = make_option(option) + local buf = {} + local function default_printer(s) + table.insert(buf, s) + end + printer = printer or default_printer + + local cache + if option.object_cache == 'global' then + -- steal the cache into a local var so it's not visible from _G or anywhere + -- still can't avoid user explicitly referentce pprint._cache but it shouldn't happen anyway + cache = pprint._cache or {} + pprint._cache = nil + elseif option.object_cache == 'local' then + cache = {} + end + + local last = '' -- used for look back and remove trailing comma + local status = { + indent = '', -- current indent + len = 0, -- current line length + } + + local wrapped_printer = function(s) + printer(last) + last = s + end + + local function _indent(d) + status.indent = string.rep(' ', d + #(status.indent)) + end + + local function _n(d) + wrapped_printer('\n') + wrapped_printer(status.indent) + if d then + _indent(d) + end + status.len = 0 + return true -- used to close bracket correctly + end + + local function _p(s, nowrap) + status.len = status.len + #s + if not nowrap and status.len > option.level_width then + _n() + wrapped_printer(s) + status.len = #s + else + wrapped_printer(s) + end + end + + local formatter = {} + local function format(v) + local f = formatter[type(v)] + f = f or formatter.table -- allow patched type() + if option.filter_function and option.filter_function(v, nil, nil) then + return '' + else + return f(v) + end + end + + local function tostring_formatter(v) + return tostring(v) + end + + local function number_formatter(n) + return n == math.huge and '[[math.huge]]' or tostring(n) + end + + local function nop_formatter(v) + return '' + end + + local function make_fixed_formatter(t, has_cache) + if has_cache then + return function (v) + return string.format('[[%s %d]]', t, cache[t][v]) + end + else + return function (v) + return '[['..t..']]' + end + end + end + + local function string_formatter(s, force_long_quote) + local s, quote = escape(s) + local quote_len = force_long_quote and 4 or 2 + if quote_len + #s + status.len > option.level_width then + _n() + -- only wrap string when is longer than level_width + if option.wrap_string and #s + quote_len > option.level_width then + -- keep the quotes together + _p('[[') + while #s + status.len >= option.level_width do + local seg = option.level_width - status.len + _p(string.sub(s, 1, seg), true) + _n() + s = string.sub(s, seg+1) + end + _p(s) -- print the remaining parts + return ']]' + end + end + + return force_long_quote and '[['..s..']]' or quote..s..quote + end + + local function table_formatter(t) + if option.use_tostring then + local mt = getmetatable(t) + if mt and mt.__tostring then + return string_formatter(tostring(t), true) + end + end + + local print_header_ix = nil + local ttype = type(t) + if option.object_cache then + local cache_state = cache.visited_tables[t] + local tix = cache[ttype][t] + -- FIXME should really handle `cache_state == nil` + -- as user might add things through filter_function + if cache_state == false then + -- already printed, just print the the number + return string_formatter(string.format('%s %d', ttype, tix), true) + elseif cache_state > 1 then + -- appeared more than once, print table header with number + print_header_ix = tix + cache.visited_tables[t] = false + else + -- appeared exactly once, print like a normal table + end + end + + local limit = tonumber(option.depth_limit) + if limit and depth > limit then + if print_header_ix then + return string.format('[[%s %d]]...', ttype, print_header_ix) + end + return string_formatter(tostring(t), true) + end + + local tlen = #t + local wrapped = false + _p('{') + _indent(option.indent_size) + _p(string.rep(' ', option.indent_size - 1)) + if print_header_ix then + _p(string.format('--[[%s %d]] ', ttype, print_header_ix)) + end + for ix = 1,tlen do + local v = t[ix] + if formatter[type(v)] == nop_formatter or + (option.filter_function and option.filter_function(v, ix, t)) then + -- pass + else + if option.wrap_array then + wrapped = _n() + end + depth = depth+1 + _p(format(v)..', ') + depth = depth-1 + end + end + + -- hashmap part of the table, in contrast to array part + local function is_hash_key(k) + if type(k) ~= 'number' then + return true + end + + local numkey = math.floor(tonumber(k)) + if numkey ~= k or numkey > tlen or numkey <= 0 then + return true + end + end + + local function print_kv(k, v, t) + -- can't use option.show_x as obj may contain custom type + if formatter[type(v)] == nop_formatter or + formatter[type(k)] == nop_formatter or + (option.filter_function and option.filter_function(v, k, t)) then + return + end + wrapped = _n() + if is_plain_key(k) then + _p(k, true) + else + _p('[') + -- [[]] type string in key is illegal, needs to add spaces inbetween + local k = format(k) + if string.match(k, '%[%[') then + _p(' '..k..' ', true) + else + _p(k, true) + end + _p(']') + end + _p(' = ', true) + depth = depth+1 + _p(format(v), true) + depth = depth-1 + _p(',', true) + end + + if option.sort_keys then + local keys = {} + for k, _ in pairs(t) do + if is_hash_key(k) then + table.insert(keys, k) + end + end + table.sort(keys, cmp) + for _, k in ipairs(keys) do + print_kv(k, t[k], t) + end + else + for k, v in pairs(t) do + if is_hash_key(k) then + print_kv(k, v, t) + end + end + end + + if option.show_metatable then + local mt = getmetatable(t) + if mt then + print_kv('__metatable', mt, t) + end + end + + _indent(-option.indent_size) + -- make { } into {} + last = string.gsub(last, '^ +$', '') + -- peek last to remove trailing comma + last = string.gsub(last, ',%s*$', ' ') + if wrapped then + _n() + end + _p('}') + + return '' + end + + -- set formatters + formatter['nil'] = option.show_nil and tostring_formatter or nop_formatter + formatter['boolean'] = option.show_boolean and tostring_formatter or nop_formatter + formatter['number'] = option.show_number and number_formatter or nop_formatter -- need to handle math.huge + formatter['function'] = option.show_function and make_fixed_formatter('function', option.object_cache) or nop_formatter + formatter['thread'] = option.show_thread and make_fixed_formatter('thread', option.object_cache) or nop_formatter + formatter['userdata'] = option.show_userdata and make_fixed_formatter('userdata', option.object_cache) or nop_formatter + formatter['string'] = option.show_string and string_formatter or nop_formatter + formatter['table'] = option.show_table and table_formatter or nop_formatter + + if option.object_cache then + -- needs to visit the table before start printing + cache_apperance(obj, cache, option) + end + + _p(format(obj)) + printer(last) -- close the buffered one + + -- put cache back if global + if option.object_cache == 'global' then + pprint._cache = cache + end + + return table.concat(buf) +end + +-- pprint all the arguments +function pprint.pprint( ... ) + local args = {...} + -- select will get an accurate count of array len, counting trailing nils + local len = select('#', ...) + for ix = 1,len do + pprint.pformat(args[ix], nil, io.write) + io.write('\n') + end +end + +setmetatable(pprint, { + __call = function (_, ...) + pprint.pprint(...) + end +}) + +return pprint + diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..1dc3e36 --- /dev/null +++ b/main.lua @@ -0,0 +1,271 @@ +-- Required to use the import and class statements +require "lualib.oop" + +import { + decompile = "decompile:"; + VAL_TYPES = "decompile:VAL_TYPES"; + + Ui = "ui:Ui"; + UiRegion = "ui:UiRegion"; + ScrollingUiRegion = "ui:ScrollingUiRegion"; + DraggableRect = "ui:DraggableRect"; + + render_text = "ui:render_text"; + + pprint = "lualib.pprint"; +} + +function build_func_header_text(func, mod) + local text = {} + + table.insert(text, { text = "[" .. func.funcidx .. "] ", color = { 0, 0, 0 } }) + table.insert(text, { text = func.name, color = { 0, 0, 1 } }) + table.insert(text, { text = ": ", color = { 0, 0, 0 } }) + + local type_ = func.type_ + for _, t in ipairs(type_.param_types) do + table.insert(text, { text = t .. " ", color = { 0, 0, 0 } }) + end + + if #type_.param_types == 0 then + table.insert(text, { text = "void ", color = { 0, 0, 0 } }) + end + + table.insert(text, { text = "-> ", color = { 0, 0, 0 } }) + + for _, t in ipairs(type_.result_types) do + table.insert(text, { text = t .. " ", color = { 0, 0, 0 } }) + end + + + if #type_.result_types == 0 then + table.insert(text, { text = "void", color = { 0, 0, 0 } }) + end + + table.insert(text, { newline = true }) + + return text +end + +local line = 1 + +function add_line_number(textlist, newline) + if newline == nil then newline = true end + table.insert(textlist, { newline = newline }) + table.insert(textlist, { text = ("%4d"):format(line), color = { 0.3, 0.3, 0.3 }, background = { 0.7, 0.7, 0.7 }, post_indent = true }) + table.insert(textlist, { text = " ", color = { 0, 0, 0 }, post_indent = true }) + line = line + 1 +end + +function instr_to_text(textlist, instr, func, mod) + if instr[1] == "block" then + table.insert(textlist, { text = "block", color = { 1, 0, 0 }, change_indent = 1 }) + for _, ins in ipairs(instr.instrs) do + add_line_number(textlist) + instr_to_text(textlist, ins, func, mod) + end + table.insert(textlist, { change_indent = -1 }) + + elseif instr[1] == "loop" then + table.insert(textlist, { text = "loop", color = { 1, 0, 0 }, change_indent = 1 }) + for _, ins in ipairs(instr.instrs) do + add_line_number(textlist) + instr_to_text(textlist, ins, func, mod) + end + table.insert(textlist, { change_indent = -1 }) + + elseif instr[1] == "if" then + table.insert(textlist, { text = "if", color = { 1, 0, 0 }, change_indent = 1 }) + for _, ins in ipairs(instr.instrs) do + add_line_number(textlist) + instr_to_text(textlist, ins, func, mod) + end + table.insert(textlist, { change_indent = -1 }) + + if #instr.else_instrs > 0 then + add_line_number(textlist) + table.insert(textlist, { text = "else", color = { 1, 0, 0 }, change_indent = 1 }) + for _, ins in ipairs(instr.instrs) do + add_line_number(textlist) + instr_to_text(textlist, ins, func, mod) + end + table.insert(textlist, { change_indent = -1 }) + end + + elseif instr[1] == "call" then + table.insert(textlist, { text = instr[1] .. " ", color = { 0, 0, 1 } }) + table.insert(textlist, { text = mod.funcs[instr.x].name, color = { 0, 0.5, 0 } }) + + elseif instr[1]:match("local") then + table.insert(textlist, { text = instr[1] .. " ", color = { 0, 0, 1 } }) + table.insert(textlist, { text = func.locals[instr.x + 1].name, color = { 0, 0.5, 0 } }) + + else + table.insert(textlist, { text = instr[1] .. " ", color = { 0, 0, 1 } }) + if instr.x then + if type(instr.x) == "table" then + if instr.x[1] == "memarg" then + table.insert(textlist, { + text = ("align=0x%03x offset=0x%08x"):format(instr.x.align, instr.x.offset); + color = { 0, 0.5, 0 } + }) + end + else + table.insert(textlist, { text = tostring(instr.x), color = { 0, 0.5, 0 } }) + end + end + end + + return line +end + +function build_func_body_text(func, mod) + local text = {} + line = 1 + + table.insert(text, { text = "Locals:", color = { 0, 0, 1 }, newline = true, change_indent = 2 }) + for _, loc in ipairs(func.locals) do + table.insert(text, { text = loc.type_ .. " " .. loc.name, color = { 0, 0, 0 }, newline = true }) + end + + if #func.locals == 0 then + table.insert(text, { text = "no locals", color = { 1, 0, 0 }, newline = true }) + end + + table.insert(text, { newline = true, change_indent = -2 }) + table.insert(text, { text = "Body:", color = { 0, 0, 1 } }) + + for _, instr in ipairs(func.body) do + add_line_number(text) + instr_to_text(text, instr, func, mod) + end + + -- table.remove(text, #text) + + return text +end + +class "FunctionBlock" [DraggableRect] { + init = function(self, func, x, y) + DraggableRect.init(self, x, y, 400, 400) + + self.header_text = build_func_header_text(func, mod) + self.body_text = build_func_body_text(func, mod) + self.scroll = 0 + end; + + onwheel = function(self, _, dy) + self.scroll = self.scroll - dy * 10; + if self.scroll < 0 then self.scroll = 0 end + end; + + ondrag = function(self, button, x, y, dx, dy) + DraggableRect.ondrag(self, button, x, y, dx, dy) + + if button == 2 then + self.rect.w = math.max(100, x) + self.rect.h = math.max(22, y) + end + end; + + draw = function(self) + love.graphics.setColor(0, 0, 0) + love.graphics.rectangle("fill", 0, 0, self.rect.w, self.rect.h) + love.graphics.setColor(1, 1, 1) + love.graphics.rectangle("fill", 2, 2, self.rect.w - 4, self.rect.h - 4) + love.graphics.setColor(0.8, 0.8, 0.8) + love.graphics.rectangle("fill", 2, 2, self.rect.w - 4, 18) + + local sx, sy = love.graphics.transformPoint(0, 0) + love.graphics.intersectScissor(sx, sy, self.rect.w, self.rect.h) + local x, y = render_text(2, 2, self.header_text) + + local sx, sy = love.graphics.transformPoint(2, y - 1) + love.graphics.intersectScissor(sx, sy, self.rect.w - 4, self.rect.h - 22) + render_text(2, y + 4 - self.scroll, self.body_text) + end; +} + +mod = nil +ui = nil +function love.load() + ui = Ui() + + mod = decompile(arg[2]) + + ui_region = ScrollingUiRegion() + ui_region.rect.x = 0 + ui_region.rect.y = 0 + ui_region.rect.w = 1200 + ui_region.rect.h = 900 + ui_region.background_color = { 0.1, 0.1, 0.1 } + ui_region.line_color = { 0.2, 0.2, 0.2 } + + -- ui_region:add_object(DraggableRect(100, 100, 100, 100)) + -- ui_region:add_object(DraggableRect(200, 100, 100, 100)) + -- ui_region:add_object(DraggableRect(300, 100, 100, 100)) + -- ui_region:add_object(DraggableRect(400, 100, 300, 100)) + + local x = 0 + local y = 0 + for _, func in pairs(mod.funcs) do + if func.body then + local block = FunctionBlock(func, x, y) + block.order = 600 - y + ui_region:add_object(block) + y = y + 20 + end + end + + ui:add_region(ui_region) + + -- bottom_bar_region = UiRegion() + -- bottom_bar_region.rect.x = 0 + -- bottom_bar_region.rect.y = 800 + -- bottom_bar_region.rect.w = 1200 + -- bottom_bar_region.rect.h = 100 + -- bottom_bar_region.layer = 1 + -- bottom_bar_region.background_color = { 1, 0, 0 } + + -- ui:add_region(bottom_bar_region) +end + +function love.update(dt) + if love.keyboard.isDown "escape" then + love.event.quit() + end + + ui:update(dt) +end + +function love.mousepressed(x, y, button) + ui:onmousedown(button, x, y) +end + +function love.mousereleased(x, y, button) + ui:onmouseup(button, x, y) +end + +function love.mousemoved(x, y, dx, dy) + ui:onmousemove(x, y, dx, dy) +end + +function love.wheelmoved(dx, dy) + ui:onmousewheel(dx, dy) +end + +function love.keypressed(key) + ui:onkeydown(key) +end + +function love.keyreleased(key) + ui:onkeyup(key) +end + +function love.resize(w, h) + ui:onresize(w, h) +end + +function love.draw() + ui:draw() +end diff --git a/res/FiraCode-Regular.ttf b/res/FiraCode-Regular.ttf new file mode 100644 index 0000000..97c1159 Binary files /dev/null and b/res/FiraCode-Regular.ttf differ diff --git a/testing.lua b/testing.lua new file mode 100644 index 0000000..6f76fe2 --- /dev/null +++ b/testing.lua @@ -0,0 +1,11 @@ +require "lualib.oop" + +import { + decompile = "decompile:"; + build_str = "utils:build_str"; + pprint = "lualib.pprint"; +} + +local mod = decompile("./data/main.wasm") +pprint(mod) + diff --git a/ui.lua b/ui.lua new file mode 100644 index 0000000..c9aa260 --- /dev/null +++ b/ui.lua @@ -0,0 +1,438 @@ +import { + Rectangle = "utils:Rectangle"; + uuid = "utils:uuidv4"; + revipairs = "utils:revipairs"; +} + +class "Ui" { + init = function(self) + self.regions = {} + self.active_region = nil + end; + + add_region = function(self, region) + table.insert(self.regions, region) + table.sort(self.regions) + end; + + onmousedown = function(self, button, x, y) + local region = nil + for _, r in revipairs(self.regions) do + if r.rect:contains(x, y) then + region = r + break + end + end + + if region ~= nil then + region:onmousedown(button, x - region.rect.x, y - region.rect.y) + end + end; + + onmouseup = function(self, button, x, y) + local region = nil + for _, r in revipairs(self.regions) do + if r.rect:contains(x, y) then + region = r + break + end + end + + if region ~= nil then + region:onmouseup(button, x - region.rect.x, y - region.rect.y) + end + end; + + onmousemove = function(self, x, y, dx, dy) + local region = nil + for _, r in revipairs(self.regions) do + if r.rect:contains(x, y) then + region = r + break + end + end + + if region ~= nil then + if region ~= self.active_region then + if self.active_region ~= nil then + self.active_region:onmouseleave() + end + + region:onmouseenter() + end + region:onmousemove(x - region.rect.x, y - region.rect.y, dx, dy) + end + + self.active_region = region + end; + + onmousewheel = function(self, dx, dy) + if not self.active_region then return end + self.active_region:onmousewheel(dx, dy) + end; + + onkeydown = function(self, key) + if not self.active_region then return end + self.active_region:onkeydown(key) + end; + + onkeyup = function(self, key) + if not self.active_region then return end + self.active_region:onkeyup(key) + end; + + onresize = function(self, new_width, new_height) + end; + + update = function(self, dt) + for _, region in ipairs(self.regions) do + region:update(dt); + end + end; + + draw = function(self) + for _, region in ipairs(self.regions) do + love.graphics.push() + love.graphics.setScissor(region.rect.x, region.rect.y, region.rect.w, region.rect.h) + love.graphics.translate(region.rect.x, region.rect.y) + region:draw() + love.graphics.pop() + end + end; +} + +class "UiRegion" { + init = function(self) + self.active = false + + self.selected_object = nil + self.objects = {} + + self.rect = Rectangle(0, 0, 0, 0) + self.background_color = { 0, 0, 0 } + self.layer = 0 + + -- Internals + self.is_mouse_down = false + self.fire_onclick = false + end; + + add_object = function(self, obj) + table.insert(self.objects, obj) + end; + + -- Used for sorting in the Ui + __lt = function(self, other) + return self.layer < other.layer + end; + + _obj_rect = function(self, obj) + return obj.rect + end; + + onmousedown = function(self, button, x, y) + self.is_mouse_down = button + self.fire_onclick = true + local new_selected = nil + + for _, obj in revipairs(self.objects) do + if self:_obj_rect(obj):contains(x, y) then + new_selected = obj + break + end + end + + if self.selected_object ~= new_selected then + if self.selected_object ~= nil then + self.selected_object:onunselect() + end + + self.selected_object = new_selected + + if self.selected_object ~= nil then + self.selected_object.order = 0 + + for _, obj in ipairs(self.objects) do + if obj ~= self.selected_obj then + obj.order = obj.order + 1 + end + end + + table.sort(self.objects, function(a, b) + return a.order > b.order + end) + + self.selected_object:onselect() + end + end + end; + + onmouseup = function(self, button, x, y) + self.is_mouse_down = false + + if self.selected_object ~= nil and self.fire_onclick then + local obj_rect = self:_obj_rect(self.selected_object) + self.selected_object:onclick(button, x - obj_rect.x, y - obj_rect.y) + end + end; + onmousemove = function(self, x, y, dx, dy) + if self.is_mouse_down and self.selected_object ~= nil then + local obj_rect = self:_obj_rect(self.selected_object) + self.selected_object:ondrag(self.is_mouse_down, x - obj_rect.x, y - obj_rect.y, dx, dy) + self.fire_onclick = false + end + end; + onmouseenter = function(self) + self.active = true + end; + + onmouseleave = function(self) + self.active = false; + self.is_mouse_down = false + self.fire_onclick = false + self.selected_object = nil + end; + + onmousewheel = function(self, dx, dy) + if self.selected_object ~= nil then + self.selected_object:onwheel(dx, dy) + end + end; + + onkeydown = function(self, key) end; + onkeyup = function(self, key) end; + + update = function(self, dt) + for _, obj in ipairs(self.objects) do + obj:update(dt) + end + end; + + draw = function(self) + if self.background_color ~= nil then + love.graphics.setColor(self.background_color) + love.graphics.rectangle("fill", 0, 0, self.rect.w, self.rect.h) + end + + for _, obj in ipairs(self.objects) do + love.graphics.push() + love.graphics.translate(obj.rect.x, obj.rect.y) + obj:draw() + love.graphics.setScissor() + love.graphics.pop() + end + end; +} + +class "ScrollingUiRegion" [UiRegion] { + init = function(self) + UiRegion.init(self) + + self.offset = { x = 0; y = 0; } + self.line_color = { 0, 0, 0 } + + self.zoom = 1 + end; + + _obj_rect = function(self, obj) + return Rectangle( + obj.rect.x + self.offset.x, + obj.rect.y + self.offset.y, + obj.rect.w, obj.rect.h) + end; + + onmousemove = function(self, x, y, dx, dy) + UiRegion.onmousemove(self, x, y, dx, dy) + + if self.is_mouse_down and self.selected_object == nil then + self.offset.x = self.offset.x + dx + self.offset.y = self.offset.y + dy + end + end; + + onmousewheel = function(self, dx, dy) + UiRegion.onmousewheel(self, dx, dy) + + if self.selected_object == nil then + self.zoom = self.zoom * (dy > 0 and 1.05 or (1 / 1.05)) + if self.zoom >= 1 then self.zoom = 1 end + end + end; + + update = function(self, dt) + UiRegion.update(self, dt) + + if self.active then + local speed = 300 + if love.keyboard.isDown "left" then + self.offset.x = self.offset.x + speed * dt + end + if love.keyboard.isDown "right" then + self.offset.x = self.offset.x - speed * dt + end + if love.keyboard.isDown "up" then + self.offset.y = self.offset.y + speed * dt + end + if love.keyboard.isDown "down" then + self.offset.y = self.offset.y - speed * dt + end + end + end; + + draw = function(self) + love.graphics.push() + + if self.background_color ~= nil then + love.graphics.setColor(self.background_color) + love.graphics.rectangle("fill", 0, 0, self.rect.w, self.rect.h) + end + local tmpbgcolor = self.background_color + self.background_color = nil + + local xoff = self.offset.x + local yoff = self.offset.y + local spacing = 64 + + while xoff >= spacing do xoff = xoff - spacing end + while yoff >= spacing do yoff = yoff - spacing end + while xoff <= -spacing do xoff = xoff + spacing end + while yoff <= -spacing do yoff = yoff + spacing end + + love.graphics.setColor(self.line_color) + for x = 0, 40 do + love.graphics.line(x * spacing + xoff, 0, x * spacing + xoff, self.rect.h) + end + for y = 0, 40 do + love.graphics.line(0, y * spacing + yoff, self.rect.w, y * spacing + yoff) + end + + love.graphics.translate(self.offset.x, self.offset.y) + UiRegion.draw(self) + love.graphics.pop() + + self.background_color = tmpbgcolor + end; +} + +class "UiObject" { + init = function(self) + self.rect = Rectangle(0, 0, 0, 0) + self.uuid = uuid() + self.selected = false + self.order = math.random(1, 128) + end; + + onclick = function(self, button, x, y) end; + ondrag = function(self, button, x, y, dx, dy) end; + onwheel = function(self, dx, dy) end; + + onselect = function(self) + self.selected = true + end; + onunselect = function(self) + self.selected = false + end; + + onkey = function(self, button) end; + + update = function(dt) end; + draw = function() end; +} + +class "DraggableRect" [UiObject] { + init = function(self, x, y, w, h) + UiObject.init(self) + + self.rect = Rectangle(x, y, w, h) + end; + + ondrag = function(self, button, x, y, dx, dy) + if button == 1 then + self.rect.x = self.rect.x + dx + self.rect.y = self.rect.y + dy + end + end; + + draw = function(self) + if self.selected then + love.graphics.setColor(0, 0, 1) + love.graphics.rectangle("fill", -2, -2, self.rect.w + 4, self.rect.h + 4) + end + love.graphics.setColor(1, 1, 1) + love.graphics.rectangle("fill", 0, 0, self.rect.w, self.rect.h) + + render_text(0, 0, { + { text = "1 Hello ", color = { 1, 0, 0 } }; + { text = "world", color = { 0, 0, 1 } }; + { newline = true }; + { text = "2 Nother line", color = { 0, 0.5, 0 } }; + { newline = true }; + { newline = true }; + { text = "3 nother Nother line", color = { 0, 0.5, 0 } }; + }) + end; +} + +programming_font = false +function render_text(x, y, text) + if not programming_font then + programming_font = love.graphics.newFont("res/FiraCode-Regular.ttf", 14) + love.graphics.setFont(programming_font) + end + + local origx = x + local fheight = programming_font:getHeight() + local indent_level = 0 + local indentation = "" + local fresh_newline = true + + for _, txt in ipairs(text) do + if txt.text then + local printed = txt.text + if fresh_newline then + if txt.post_indent then + printed = txt.text .. indentation + else + printed = indentation .. txt.text + end + fresh_newline = false + end + + if txt.background then + love.graphics.setColor(txt.background) + love.graphics.rectangle("fill", x, y, programming_font:getWidth(txt.text), fheight + 2); + end + + love.graphics.setColor(txt.color) + love.graphics.print(printed, x, y) + + x = x + programming_font:getWidth(printed) + end + + if txt.newline then + y = y + fheight + 2; + x = origx + fresh_newline = true + end + + if txt.change_indent then + indent_level = indent_level + txt.change_indent + indentation = "" + for i=1,indent_level do + indentation = indentation .. " " + end + end + end + + return x, y +end + +return module { + Ui = Ui; + UiRegion = UiRegion; + ScrollingUiRegion = ScrollingUiRegion; + UiObject = UiObject; + DraggableRect = DraggableRect; + + render_text = render_text; +} diff --git a/utils.lua b/utils.lua new file mode 100644 index 0000000..ec4b5e0 --- /dev/null +++ b/utils.lua @@ -0,0 +1,101 @@ +import {} + +-- This is unsued in favor of the C equivalent +function buffered_read(file, buffer_size) + buffer_size = buffer_size or 4 * 1024 + + local buffer = {} + local ptr = -1 + + return coroutine.wrap(function() + while true do + if ptr <= 0 or ptr >= buffer_size then + ptr = 0 + local str = file:read(buffer_size) + if not str then break end + + buffer = {} + for c in (str or ''):gmatch '.' do + buffer[#buffer + 1] = c + end + end + + ptr = ptr + 1 + if ptr > #buffer then break end + coroutine.yield(buffer[ptr]:byte()) + end + return + end) +end + +function build_str(tbl_int) + if #tbl_int < 30 then + return string.char(unpack(tbl_int)) + else + -- This is slower but may be needed for long strings + local str = "" + for _, c in ipairs(tbl_int) do + str = str .. string.char(c) + end + return str + end +end + +function uuidv4() + local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' + return string.gsub(template, '[xy]', function (c) + local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb) + return string.format('%x', v) + end) +end + +function revipairs(t) + local max = 1 + while t[max] ~= nil do + max = max + 1 + end + local function ripairs_it(t, i) + i = i - 1 + local v = t[i] + if v ~= nil then + return i, v + else + return nil + end + end + return ripairs_it, t, max +end + +class "Rectangle" { + init = function(self, x, y, w, h) + self.x = x + self.y = y + self.w = w + self.h = h + end; + + contains = function(self, x, y) + return x >= self.x and x <= self.x + self.w + and y >= self.y and y <= self.y + self.h + end; + + intersects = function(self, other) + return self.x <= other.x + other.w + and self.x + self.w >= other.x + and self.y <= other.y + other.h + and self.y + self.h >= other.y + end; + + __lt = function(self, other) return self:intersects(other) end +} + +return module { + buffered_read = buffered_read; + build_str = build_str; + + uuidv4 = uuidv4; + + revipairs = revipairs; + + Rectangle = Rectangle; +}