iris-decompiler 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # iris-decompiler
2
+
3
+ A Luau / Roblox bytecode decompiler. Parses compiled Luau bytecode and
4
+ reconstructs readable Luau source. Ships as a **library** plus a small
5
+ **CLI** — no server, no runtime dependencies (Node's standard library only).
6
+
7
+ ## Install / build
8
+
9
+ ```bash
10
+ npm install
11
+ npm run build # compiles src -> dist (CommonJS + .d.ts types)
12
+ ```
13
+
14
+ ## Library usage
15
+
16
+ ```ts
17
+ import { Decompiler } from "iris-decompiler";
18
+
19
+ // From raw bytes (Uint8Array / Buffer)
20
+ const result = Decompiler.decompile(uint8Array); // or { bytes }
21
+ console.log(result.source); // reconstructed Luau
22
+
23
+ // From a base64 string
24
+ const r2 = Decompiler.decompile({ base64: b64String }); // r2.decodedFromBase64 === true
25
+
26
+ // Disassembly instead of decompilation
27
+ const asm = Decompiler.disassemble(uint8Array); // all protos
28
+ const one = Decompiler.disassembleProto({ bytes }, 0); // single proto by index
29
+
30
+ // Low-level parse only (inspect the IR)
31
+ const { program } = Decompiler.parse(uint8Array);
32
+ ```
33
+
34
+ `decompile` accepts either a `Uint8Array` or an object `{ bytes?, base64?, name? }`.
35
+ It returns `{ source, program, decodedFromBase64 }`. Malformed input throws
36
+ `DecompilerError`.
37
+
38
+ ### Direct function exports
39
+
40
+ ```ts
41
+ import { decompile, disassemble, parseBytecode, base64Decode, Op, ConstKind } from "iris-decompiler";
42
+ ```
43
+
44
+ | Export | Purpose |
45
+ |---|---|
46
+ | `Decompiler` | Object API (methods above) |
47
+ | `decompile(program)` / `decompileProto(...)` | Core decompiler |
48
+ | `disassemble(program)` / `disassembleProto(program, i)` | Disassembly |
49
+ | `parseBytecode(bytes, program, err)` | Low-level parser |
50
+ | `base64Decode(str)` | Base64 → `Uint8Array` |
51
+ | `Op`, `ConstKind`, `Program`, `Proto`, `Constant` | Types / enums |
52
+ | `DecompilerError` | Error thrown on invalid input |
53
+
54
+ ## CLI
55
+
56
+ ```bash
57
+ node bin/cli.js <file> # decompile to stdout
58
+ node bin/cli.js --disasm <file> # disassemble to stdout
59
+ ```
60
+
61
+ The CLI also accepts a base64 file and auto-decodes it.
62
+ After `npm link` or global install it's available as `iris-decompiler`.
63
+
64
+ ## Bytecode version support
65
+
66
+ Luau bytecode **versions 3–13** (and the version-100 "classes" format). Anything
67
+ outside that range is rejected with a clear error, e.g. current Roblox client
68
+ bytecode is version 14 → `bytecode version mismatch (expected [3..13], got 14)`.
69
+
70
+ ## Test fixtures
71
+
72
+ `test-fixtures/` contains sample bytecode. `animate.bin` and `last_raw.bin`
73
+ decompile to ~568 lines of Luau; `health.bin` is version 14 and is expected to
74
+ be rejected.
75
+
76
+ ```bash
77
+ node bin/cli.js test-fixtures/animate.bin
78
+ ```
package/bin/cli.js ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ // Usage:
3
+ // node bin/cli.js <file> -> decompile
4
+ // node bin/cli.js --disasm <file> -> disassemble
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ function loadDecompiler() {
10
+ const distPath = path.join(__dirname, "..", "dist", "index.js");
11
+ if (fs.existsSync(distPath)) {
12
+ return require(distPath);
13
+ }
14
+ throw new Error("Could not load decompiler: dist/ is missing. Run `npm run build` first.");
15
+ }
16
+
17
+ function readFileBytes(p) {
18
+ return new Uint8Array(fs.readFileSync(p));
19
+ }
20
+
21
+ function main() {
22
+ const args = process.argv.slice(2);
23
+ if (args.length < 1) {
24
+ process.stderr.write("usage: iris-decompiler <file>\n");
25
+ process.stderr.write(" iris-decompiler --disasm <file>\n");
26
+ process.exit(1);
27
+ }
28
+
29
+ const D = loadDecompiler();
30
+
31
+ if (args[0] === "--disasm" && args.length >= 2) {
32
+ const data = readFileBytes(args[1]);
33
+ process.stdout.write(D.Decompiler.disassemble({ bytes: data }));
34
+ return;
35
+ }
36
+
37
+ const data = readFileBytes(args[0]);
38
+ try {
39
+ const result = D.Decompiler.decompile({ bytes: data });
40
+ process.stdout.write(result.source);
41
+ } catch (err) {
42
+ // If raw-byte parsing failed, try as base64.
43
+ const b64 = Buffer.from(data).toString("utf8");
44
+ try {
45
+ const result = D.Decompiler.decompile({ base64: b64 });
46
+ process.stdout.write(result.source);
47
+ } catch (e2) {
48
+ process.stderr.write("error: failed to parse bytecode: " + (err.message || err) + "\n");
49
+ process.exit(1);
50
+ }
51
+ }
52
+ }
53
+
54
+ main();
@@ -0,0 +1 @@
1
+ export declare function base64Decode(input: string): Uint8Array | null;
package/dist/base64.js ADDED
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.base64Decode = base64Decode;
4
+ function base64Decode(input) {
5
+ const out = [];
6
+ let buffer = 0;
7
+ let bits = 0;
8
+ const b64val = (c) => {
9
+ const code = c.charCodeAt(0);
10
+ if (code >= 0x41 && code <= 0x5a)
11
+ return code - 0x41; // A-Z
12
+ if (code >= 0x61 && code <= 0x7a)
13
+ return code - 0x61 + 26; // a-z
14
+ if (code >= 0x30 && code <= 0x39)
15
+ return code - 0x30 + 52; // 0-9
16
+ if (code === 0x2b)
17
+ return 62; // +
18
+ if (code === 0x2f)
19
+ return 63; // /
20
+ return -1;
21
+ };
22
+ for (const c of input) {
23
+ if (c === "\r" || c === "\n" || c === " " || c === "\t")
24
+ continue;
25
+ if (c === "=")
26
+ break;
27
+ const v = b64val(c);
28
+ if (v < 0)
29
+ return null;
30
+ buffer = (buffer << 6) | v;
31
+ bits += 6;
32
+ if (bits >= 8) {
33
+ bits -= 8;
34
+ out.push((buffer >> bits) & 0xff);
35
+ }
36
+ }
37
+ return new Uint8Array(out);
38
+ }
@@ -0,0 +1,57 @@
1
+ export declare enum ExprK {
2
+ Leaf = 0,
3
+ Bin = 1,
4
+ Un = 2,
5
+ Index = 3,
6
+ Dot = 4,
7
+ Call = 5,
8
+ MethodCall = 6,
9
+ Table = 7,
10
+ Func = 8,
11
+ Vararg = 9,
12
+ IfElse = 10
13
+ }
14
+ export interface Expr {
15
+ kind: ExprK;
16
+ prec: number;
17
+ rightAssoc: boolean;
18
+ multi: boolean;
19
+ built: boolean;
20
+ parenWrap: boolean;
21
+ text: string;
22
+ op: string;
23
+ a?: Expr;
24
+ b?: Expr;
25
+ c?: Expr;
26
+ args: Expr[];
27
+ name: string;
28
+ keys: Expr[];
29
+ vals: Expr[];
30
+ funcHeader: string;
31
+ funcComment: string;
32
+ funcBodyLines: Array<[number, string]>;
33
+ }
34
+ export declare function makeExpr(kind: ExprK): Expr;
35
+ export interface Line {
36
+ level: number;
37
+ text: string;
38
+ }
39
+ export declare class Sink {
40
+ indent: number;
41
+ lines: Line[];
42
+ push(level: number, text: string): void;
43
+ emit(text: string): void;
44
+ }
45
+ export interface UpvalBind {
46
+ name: string;
47
+ byRef: boolean;
48
+ isParentUpvalue: boolean;
49
+ }
50
+ export interface ClosureResult {
51
+ header: string;
52
+ comment: string;
53
+ body: Sink;
54
+ upvalues: UpvalBind[];
55
+ selfRef: boolean;
56
+ }
57
+ export declare function makeClosureResult(): ClosureResult;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Sink = exports.ExprK = void 0;
4
+ exports.makeExpr = makeExpr;
5
+ exports.makeClosureResult = makeClosureResult;
6
+ var ExprK;
7
+ (function (ExprK) {
8
+ ExprK[ExprK["Leaf"] = 0] = "Leaf";
9
+ ExprK[ExprK["Bin"] = 1] = "Bin";
10
+ ExprK[ExprK["Un"] = 2] = "Un";
11
+ ExprK[ExprK["Index"] = 3] = "Index";
12
+ ExprK[ExprK["Dot"] = 4] = "Dot";
13
+ ExprK[ExprK["Call"] = 5] = "Call";
14
+ ExprK[ExprK["MethodCall"] = 6] = "MethodCall";
15
+ ExprK[ExprK["Table"] = 7] = "Table";
16
+ ExprK[ExprK["Func"] = 8] = "Func";
17
+ ExprK[ExprK["Vararg"] = 9] = "Vararg";
18
+ ExprK[ExprK["IfElse"] = 10] = "IfElse";
19
+ })(ExprK || (exports.ExprK = ExprK = {}));
20
+ function makeExpr(kind) {
21
+ return {
22
+ kind, prec: 0, rightAssoc: false, multi: false, built: false, parenWrap: false,
23
+ text: "", op: "", args: [], name: "", keys: [], vals: [],
24
+ funcHeader: "", funcComment: "", funcBodyLines: [],
25
+ };
26
+ }
27
+ class Sink {
28
+ constructor() {
29
+ this.indent = 0;
30
+ this.lines = [];
31
+ }
32
+ push(level, text) {
33
+ this.lines.push({ level, text });
34
+ }
35
+ emit(text) {
36
+ this.lines.push({ level: this.indent, text });
37
+ }
38
+ }
39
+ exports.Sink = Sink;
40
+ function makeClosureResult() {
41
+ return { header: "", comment: "", body: new Sink(), upvalues: [], selfRef: false };
42
+ }
@@ -0,0 +1,4 @@
1
+ import { Program, Proto } from "./types";
2
+ import { UpvalBind, ClosureResult } from "./decompile_internal";
3
+ export declare function decompileProto(program: Program, proto: Proto, upvals: UpvalBind[]): ClosureResult;
4
+ export declare function decompile(program: Program): string;