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 +78 -0
- package/bin/cli.js +54 -0
- package/dist/base64.d.ts +1 -0
- package/dist/base64.js +38 -0
- package/dist/decompile_internal.d.ts +57 -0
- package/dist/decompile_internal.js +42 -0
- package/dist/decompiler.d.ts +4 -0
- package/dist/decompiler.js +2677 -0
- package/dist/disasm.d.ts +3 -0
- package/dist/disasm.js +321 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +109 -0
- package/dist/parser.d.ts +4 -0
- package/dist/parser.js +582 -0
- package/dist/types.d.ts +174 -0
- package/dist/types.js +299 -0
- package/package.json +25 -0
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();
|
package/dist/base64.d.ts
ADDED
|
@@ -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;
|