swf-toolkit 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +125 -2
- package/README.tr.md +126 -0
- package/dist/abc/abc-file.d.ts +168 -0
- package/dist/abc/abc-file.js +656 -0
- package/dist/abc/analysis.d.ts +36 -0
- package/dist/abc/analysis.js +213 -0
- package/dist/abc/code.d.ts +59 -0
- package/dist/abc/code.js +379 -0
- package/dist/abc/constants.d.ts +80 -0
- package/dist/abc/constants.js +93 -0
- package/dist/abc/interner.d.ts +46 -0
- package/dist/abc/interner.js +168 -0
- package/dist/abc/model.d.ts +40 -0
- package/dist/abc/model.js +146 -0
- package/dist/abc/opcodes.d.ts +39 -0
- package/dist/abc/opcodes.js +196 -0
- package/dist/abc/pcode/assembler.d.ts +42 -0
- package/dist/abc/pcode/assembler.js +635 -0
- package/dist/abc/pcode/disassembler.d.ts +17 -0
- package/dist/abc/pcode/disassembler.js +147 -0
- package/dist/abc/pcode/format.d.ts +23 -0
- package/dist/abc/pcode/format.js +131 -0
- package/dist/abc/pcode/lexer.d.ts +34 -0
- package/dist/abc/pcode/lexer.js +183 -0
- package/dist/assets/assets.d.ts +26 -0
- package/dist/assets/assets.js +151 -0
- package/dist/assets/images.d.ts +31 -0
- package/dist/assets/images.js +238 -0
- package/dist/assets/png.d.ts +12 -0
- package/dist/assets/png.js +196 -0
- package/dist/assets/sounds.d.ts +37 -0
- package/dist/assets/sounds.js +236 -0
- package/dist/avm1/avm1.d.ts +55 -0
- package/dist/avm1/avm1.js +642 -0
- package/dist/avm1/project.d.ts +16 -0
- package/dist/avm1/project.js +81 -0
- package/dist/cjs/abc/abc-file.d.ts +168 -0
- package/dist/cjs/abc/abc-file.js +663 -0
- package/dist/cjs/abc/analysis.d.ts +36 -0
- package/dist/cjs/abc/analysis.js +221 -0
- package/dist/cjs/abc/code.d.ts +59 -0
- package/dist/cjs/abc/code.js +387 -0
- package/dist/cjs/abc/constants.d.ts +80 -0
- package/dist/cjs/abc/constants.js +97 -0
- package/dist/cjs/abc/interner.d.ts +46 -0
- package/dist/cjs/abc/interner.js +172 -0
- package/dist/cjs/abc/model.d.ts +40 -0
- package/dist/cjs/abc/model.js +155 -0
- package/dist/cjs/abc/opcodes.d.ts +39 -0
- package/dist/cjs/abc/opcodes.js +201 -0
- package/dist/cjs/abc/pcode/assembler.d.ts +42 -0
- package/dist/cjs/abc/pcode/assembler.js +640 -0
- package/dist/cjs/abc/pcode/disassembler.d.ts +17 -0
- package/dist/cjs/abc/pcode/disassembler.js +152 -0
- package/dist/cjs/abc/pcode/format.d.ts +23 -0
- package/dist/cjs/abc/pcode/format.js +141 -0
- package/dist/cjs/abc/pcode/lexer.d.ts +34 -0
- package/dist/cjs/abc/pcode/lexer.js +188 -0
- package/dist/cjs/assets/assets.d.ts +26 -0
- package/dist/cjs/assets/assets.js +159 -0
- package/dist/cjs/assets/images.d.ts +31 -0
- package/dist/cjs/assets/images.js +246 -0
- package/dist/cjs/assets/png.d.ts +12 -0
- package/dist/cjs/assets/png.js +202 -0
- package/dist/cjs/assets/sounds.d.ts +37 -0
- package/dist/cjs/assets/sounds.js +246 -0
- package/dist/cjs/avm1/avm1.d.ts +55 -0
- package/dist/cjs/avm1/avm1.js +649 -0
- package/dist/cjs/avm1/project.d.ts +16 -0
- package/dist/cjs/avm1/project.js +88 -0
- package/dist/cjs/compiler/ast.d.ts +285 -0
- package/dist/cjs/compiler/ast.js +3 -0
- package/dist/cjs/compiler/class-compiler.d.ts +34 -0
- package/dist/cjs/compiler/class-compiler.js +440 -0
- package/dist/cjs/compiler/codegen.d.ts +144 -0
- package/dist/cjs/compiler/codegen.js +1589 -0
- package/dist/cjs/compiler/lexer.d.ts +27 -0
- package/dist/cjs/compiler/lexer.js +254 -0
- package/dist/cjs/compiler/parser.d.ts +58 -0
- package/dist/cjs/compiler/parser.js +989 -0
- package/dist/cjs/compiler/program.d.ts +29 -0
- package/dist/cjs/compiler/program.js +36 -0
- package/dist/cjs/compiler/scope.d.ts +22 -0
- package/dist/cjs/compiler/scope.js +242 -0
- package/dist/cjs/compression/lzma.d.ts +37 -0
- package/dist/cjs/compression/lzma.js +575 -0
- package/dist/cjs/compression/swf-container.d.ts +19 -0
- package/dist/cjs/compression/swf-container.js +92 -0
- package/dist/cjs/decompiler/ast.d.ts +228 -0
- package/dist/cjs/decompiler/ast.js +455 -0
- package/dist/cjs/decompiler/cfg.d.ts +40 -0
- package/dist/cjs/decompiler/cfg.js +236 -0
- package/dist/cjs/decompiler/class-printer.d.ts +9 -0
- package/dist/cjs/decompiler/class-printer.js +173 -0
- package/dist/cjs/decompiler/method.d.ts +41 -0
- package/dist/cjs/decompiler/method.js +436 -0
- package/dist/cjs/decompiler/simulate.d.ts +55 -0
- package/dist/cjs/decompiler/simulate.js +591 -0
- package/dist/cjs/decompiler/structure.d.ts +48 -0
- package/dist/cjs/decompiler/structure.js +764 -0
- package/dist/cjs/errors.d.ts +11 -0
- package/dist/cjs/errors.js +25 -0
- package/dist/cjs/index.d.ts +41 -0
- package/dist/cjs/index.js +82 -0
- package/dist/cjs/io/reader.d.ts +48 -0
- package/dist/cjs/io/reader.js +180 -0
- package/dist/cjs/io/writer.d.ts +37 -0
- package/dist/cjs/io/writer.js +175 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/project/pcode-project.d.ts +32 -0
- package/dist/cjs/project/pcode-project.js +210 -0
- package/dist/cjs/project/source-export.d.ts +12 -0
- package/dist/cjs/project/source-export.js +51 -0
- package/dist/cjs/project/source-import.d.ts +13 -0
- package/dist/cjs/project/source-import.js +73 -0
- package/dist/cjs/swf/swf.d.ts +63 -0
- package/dist/cjs/swf/swf.js +147 -0
- package/dist/cjs/swf/tag-codes.d.ts +77 -0
- package/dist/cjs/swf/tag-codes.js +109 -0
- package/dist/cjs/swf/tags.d.ts +118 -0
- package/dist/cjs/swf/tags.js +330 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +296 -0
- package/dist/compiler/ast.d.ts +285 -0
- package/dist/compiler/ast.js +2 -0
- package/dist/compiler/class-compiler.d.ts +34 -0
- package/dist/compiler/class-compiler.js +437 -0
- package/dist/compiler/codegen.d.ts +144 -0
- package/dist/compiler/codegen.js +1585 -0
- package/dist/compiler/lexer.d.ts +27 -0
- package/dist/compiler/lexer.js +249 -0
- package/dist/compiler/parser.d.ts +58 -0
- package/dist/compiler/parser.js +984 -0
- package/dist/compiler/program.d.ts +29 -0
- package/dist/compiler/program.js +32 -0
- package/dist/compiler/scope.d.ts +22 -0
- package/dist/compiler/scope.js +238 -0
- package/dist/compression/lzma.d.ts +37 -0
- package/dist/compression/lzma.js +569 -0
- package/dist/compression/swf-container.d.ts +19 -0
- package/dist/compression/swf-container.js +87 -0
- package/dist/decompiler/ast.d.ts +228 -0
- package/dist/decompiler/ast.js +441 -0
- package/dist/decompiler/cfg.d.ts +40 -0
- package/dist/decompiler/cfg.js +231 -0
- package/dist/decompiler/class-printer.d.ts +9 -0
- package/dist/decompiler/class-printer.js +170 -0
- package/dist/decompiler/method.d.ts +41 -0
- package/dist/decompiler/method.js +427 -0
- package/dist/decompiler/simulate.d.ts +55 -0
- package/dist/decompiler/simulate.js +586 -0
- package/dist/decompiler/structure.d.ts +48 -0
- package/dist/decompiler/structure.js +760 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +20 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +49 -0
- package/dist/io/reader.d.ts +48 -0
- package/dist/io/reader.js +176 -0
- package/dist/io/writer.d.ts +37 -0
- package/dist/io/writer.js +169 -0
- package/dist/project/pcode-project.d.ts +32 -0
- package/dist/project/pcode-project.js +203 -0
- package/dist/project/source-export.d.ts +12 -0
- package/dist/project/source-export.js +47 -0
- package/dist/project/source-import.d.ts +13 -0
- package/dist/project/source-import.js +70 -0
- package/dist/swf/swf.d.ts +63 -0
- package/dist/swf/swf.js +143 -0
- package/dist/swf/tag-codes.d.ts +77 -0
- package/dist/swf/tag-codes.js +105 -0
- package/dist/swf/tags.d.ts +118 -0
- package/dist/swf/tags.js +316 -0
- package/package.json +71 -4
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.multinameRuntimeArity = multinameRuntimeArity;
|
|
4
|
+
exports.stackEffect = stackEffect;
|
|
5
|
+
exports.registersUsed = registersUsed;
|
|
6
|
+
exports.computeLimits = computeLimits;
|
|
7
|
+
exports.minLocalCount = minLocalCount;
|
|
8
|
+
exports.verifyCode = verifyCode;
|
|
9
|
+
const constants_js_1 = require("./constants.js");
|
|
10
|
+
const opcodes_js_1 = require("./opcodes.js");
|
|
11
|
+
/** Number of values a multiname pops from the stack at runtime (namespace and/or name). */
|
|
12
|
+
function multinameRuntimeArity(abc, mnIndex) {
|
|
13
|
+
const m = abc.multinames[mnIndex];
|
|
14
|
+
if (!m)
|
|
15
|
+
return 0;
|
|
16
|
+
switch (m.kind) {
|
|
17
|
+
case constants_js_1.MultinameKind.RTQName:
|
|
18
|
+
case constants_js_1.MultinameKind.RTQNameA:
|
|
19
|
+
case constants_js_1.MultinameKind.MultinameL:
|
|
20
|
+
case constants_js_1.MultinameKind.MultinameLA:
|
|
21
|
+
return 1;
|
|
22
|
+
case constants_js_1.MultinameKind.RTQNameL:
|
|
23
|
+
case constants_js_1.MultinameKind.RTQNameLA:
|
|
24
|
+
return 2;
|
|
25
|
+
default:
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Values popped / pushed by an instruction. */
|
|
30
|
+
function stackEffect(abc, i) {
|
|
31
|
+
const info = (0, opcodes_js_1.opInfo)(i.op);
|
|
32
|
+
if (!info)
|
|
33
|
+
return { pop: 0, push: 0 };
|
|
34
|
+
let pop = info.pop;
|
|
35
|
+
if (info.popArgc) {
|
|
36
|
+
const argcPos = info.operands.filter((t) => t !== 'branch' && t !== 'switch').indexOf('argc');
|
|
37
|
+
pop += info.popArgc * (i.args[argcPos] ?? 0);
|
|
38
|
+
}
|
|
39
|
+
if (info.popMn) {
|
|
40
|
+
const mnPos = info.operands.filter((t) => t !== 'branch' && t !== 'switch').indexOf('multiname');
|
|
41
|
+
pop += multinameRuntimeArity(abc, i.args[mnPos] ?? 0);
|
|
42
|
+
}
|
|
43
|
+
return { pop, push: info.push };
|
|
44
|
+
}
|
|
45
|
+
/** Local registers referenced by an instruction. */
|
|
46
|
+
function registersUsed(i) {
|
|
47
|
+
switch (i.op) {
|
|
48
|
+
case opcodes_js_1.Op.getlocal0:
|
|
49
|
+
case opcodes_js_1.Op.setlocal0:
|
|
50
|
+
return [0];
|
|
51
|
+
case opcodes_js_1.Op.getlocal1:
|
|
52
|
+
case opcodes_js_1.Op.setlocal1:
|
|
53
|
+
return [1];
|
|
54
|
+
case opcodes_js_1.Op.getlocal2:
|
|
55
|
+
case opcodes_js_1.Op.setlocal2:
|
|
56
|
+
return [2];
|
|
57
|
+
case opcodes_js_1.Op.getlocal3:
|
|
58
|
+
case opcodes_js_1.Op.setlocal3:
|
|
59
|
+
return [3];
|
|
60
|
+
case opcodes_js_1.Op.getlocal:
|
|
61
|
+
case opcodes_js_1.Op.setlocal:
|
|
62
|
+
case opcodes_js_1.Op.kill:
|
|
63
|
+
case opcodes_js_1.Op.inclocal:
|
|
64
|
+
case opcodes_js_1.Op.declocal:
|
|
65
|
+
case opcodes_js_1.Op.inclocal_i:
|
|
66
|
+
case opcodes_js_1.Op.declocal_i:
|
|
67
|
+
return [i.args[0] ?? 0];
|
|
68
|
+
case opcodes_js_1.Op.hasnext2:
|
|
69
|
+
return [i.args[0] ?? 0, i.args[1] ?? 0];
|
|
70
|
+
case opcodes_js_1.Op.debug:
|
|
71
|
+
return i.args[0] === 1 ? [i.args[2] ?? 0] : [];
|
|
72
|
+
default:
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Computes stack / scope / register requirements by walking all control-flow
|
|
78
|
+
* paths (including exception handlers).
|
|
79
|
+
*/
|
|
80
|
+
function computeLimits(abc, items, exceptions = []) {
|
|
81
|
+
const labelIndex = new Map();
|
|
82
|
+
items.forEach((it, idx) => {
|
|
83
|
+
if (it.type === 'label')
|
|
84
|
+
labelIndex.set(it.id, idx);
|
|
85
|
+
});
|
|
86
|
+
let maxStack = 0;
|
|
87
|
+
let maxScope = 0;
|
|
88
|
+
let registers = 0;
|
|
89
|
+
for (const it of items)
|
|
90
|
+
if (it.type === 'op')
|
|
91
|
+
for (const r of registersUsed(it))
|
|
92
|
+
registers = Math.max(registers, r + 1);
|
|
93
|
+
const seenStack = new Int32Array(items.length).fill(-1);
|
|
94
|
+
const seenScope = new Int32Array(items.length).fill(-1);
|
|
95
|
+
const work = [[0, 0, 0]];
|
|
96
|
+
for (const e of exceptions) {
|
|
97
|
+
const t = labelIndex.get(e.target);
|
|
98
|
+
if (t !== undefined)
|
|
99
|
+
work.push([t, 1, 0]);
|
|
100
|
+
}
|
|
101
|
+
if (exceptions.length > 0)
|
|
102
|
+
maxStack = Math.max(maxStack, 1);
|
|
103
|
+
let budget = items.length * 64 + 1024;
|
|
104
|
+
while (work.length > 0 && budget-- > 0) {
|
|
105
|
+
let [idx, stack, scope] = work.pop();
|
|
106
|
+
while (idx < items.length) {
|
|
107
|
+
if (stack <= seenStack[idx] && scope <= seenScope[idx])
|
|
108
|
+
break;
|
|
109
|
+
seenStack[idx] = Math.max(seenStack[idx], stack);
|
|
110
|
+
seenScope[idx] = Math.max(seenScope[idx], scope);
|
|
111
|
+
const it = items[idx];
|
|
112
|
+
if (it.type === 'label') {
|
|
113
|
+
idx++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (it.type === 'bytes')
|
|
117
|
+
break;
|
|
118
|
+
const info = (0, opcodes_js_1.opInfo)(it.op);
|
|
119
|
+
if (!info)
|
|
120
|
+
break;
|
|
121
|
+
const { pop, push } = stackEffect(abc, it);
|
|
122
|
+
stack = Math.max(0, stack - pop) + push;
|
|
123
|
+
scope = Math.max(0, scope + info.scope);
|
|
124
|
+
maxStack = Math.max(maxStack, stack);
|
|
125
|
+
maxScope = Math.max(maxScope, scope);
|
|
126
|
+
for (let k = 0; k < it.targets.length; k++) {
|
|
127
|
+
const t = labelIndex.get(it.targets[k]);
|
|
128
|
+
if (t !== undefined)
|
|
129
|
+
work.push([t, stack, scope]);
|
|
130
|
+
}
|
|
131
|
+
if (info.flow === 'jump' || info.flow === 'switch' || info.flow === 'return' || info.flow === 'throw')
|
|
132
|
+
break;
|
|
133
|
+
idx++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { maxStack, maxScope, registers };
|
|
137
|
+
}
|
|
138
|
+
/** Minimum local_count required by a method signature (this + params + rest/arguments). */
|
|
139
|
+
function minLocalCount(method) {
|
|
140
|
+
let n = 1 + method.paramTypes.length;
|
|
141
|
+
if (method.flags & (constants_js_1.MethodFlags.NEED_REST | constants_js_1.MethodFlags.NEED_ARGUMENTS))
|
|
142
|
+
n++;
|
|
143
|
+
return n;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Structural verification similar to the AVM2 verifier's first pass:
|
|
147
|
+
* stack underflow, inconsistent stack / scope heights at merge points,
|
|
148
|
+
* falling off the end of the code and scope underflow.
|
|
149
|
+
*/
|
|
150
|
+
function verifyCode(abc, items, exceptions = []) {
|
|
151
|
+
const issues = [];
|
|
152
|
+
const labelIndex = new Map();
|
|
153
|
+
items.forEach((it, idx) => {
|
|
154
|
+
if (it.type === 'label')
|
|
155
|
+
labelIndex.set(it.id, idx);
|
|
156
|
+
});
|
|
157
|
+
const stackAt = new Int32Array(items.length + 1).fill(-1);
|
|
158
|
+
const scopeAt = new Int32Array(items.length + 1).fill(-1);
|
|
159
|
+
const work = [[0, 0, 0]];
|
|
160
|
+
for (const e of exceptions) {
|
|
161
|
+
const t = labelIndex.get(e.target);
|
|
162
|
+
if (t === undefined)
|
|
163
|
+
issues.push({ item: -1, message: `Exception target label ${e.target} undefined` });
|
|
164
|
+
else
|
|
165
|
+
work.push([t, 1, 0]);
|
|
166
|
+
}
|
|
167
|
+
const report = (item, message) => {
|
|
168
|
+
if (issues.length < 50 && !issues.some((i) => i.item === item && i.message === message))
|
|
169
|
+
issues.push({ item, message });
|
|
170
|
+
};
|
|
171
|
+
while (work.length) {
|
|
172
|
+
let [idx, stack, scope] = work.pop();
|
|
173
|
+
while (true) {
|
|
174
|
+
if (idx >= items.length) {
|
|
175
|
+
report(idx, 'Control falls off the end of the code');
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
if (stackAt[idx] >= 0) {
|
|
179
|
+
if (stackAt[idx] !== stack)
|
|
180
|
+
report(idx, `Stack height mismatch at merge (${stackAt[idx]} vs ${stack})`);
|
|
181
|
+
if (scopeAt[idx] !== scope)
|
|
182
|
+
report(idx, `Scope height mismatch at merge (${scopeAt[idx]} vs ${scope})`);
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
stackAt[idx] = stack;
|
|
186
|
+
scopeAt[idx] = scope;
|
|
187
|
+
const it = items[idx];
|
|
188
|
+
if (it.type === 'label') {
|
|
189
|
+
idx++;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (it.type === 'bytes')
|
|
193
|
+
break;
|
|
194
|
+
const info = (0, opcodes_js_1.opInfo)(it.op);
|
|
195
|
+
if (!info) {
|
|
196
|
+
report(idx, `Unknown opcode ${it.op}`);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
const { pop, push } = stackEffect(abc, it);
|
|
200
|
+
if (pop > stack)
|
|
201
|
+
report(idx, `Stack underflow in ${info.name} (needs ${pop}, has ${stack})`);
|
|
202
|
+
stack = Math.max(0, stack - pop) + push;
|
|
203
|
+
scope += info.scope;
|
|
204
|
+
if (scope < 0) {
|
|
205
|
+
report(idx, `Scope underflow in ${info.name}`);
|
|
206
|
+
scope = 0;
|
|
207
|
+
}
|
|
208
|
+
for (const t of it.targets) {
|
|
209
|
+
const ti = labelIndex.get(t);
|
|
210
|
+
if (ti === undefined)
|
|
211
|
+
report(idx, `Undefined label ${t}`);
|
|
212
|
+
else
|
|
213
|
+
work.push([ti, stack, scope]);
|
|
214
|
+
}
|
|
215
|
+
if (info.flow === 'jump' || info.flow === 'switch' || info.flow === 'return' || info.flow === 'throw')
|
|
216
|
+
break;
|
|
217
|
+
idx++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return issues;
|
|
221
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ExceptionInfo } from './abc-file.js';
|
|
2
|
+
export interface Instruction {
|
|
3
|
+
type: 'op';
|
|
4
|
+
op: number;
|
|
5
|
+
/** Non-branch operands in declaration order. */
|
|
6
|
+
args: number[];
|
|
7
|
+
/** Label ids of branch targets. lookupswitch: [default, case0, case1, ...]. */
|
|
8
|
+
targets: number[];
|
|
9
|
+
/** Byte offset in the original code (-1 for new instructions). */
|
|
10
|
+
offset: number;
|
|
11
|
+
}
|
|
12
|
+
export interface LabelItem {
|
|
13
|
+
type: 'label';
|
|
14
|
+
id: number;
|
|
15
|
+
}
|
|
16
|
+
/** Bytes that are not reachable / not decodable; kept verbatim. */
|
|
17
|
+
export interface BytesItem {
|
|
18
|
+
type: 'bytes';
|
|
19
|
+
data: Uint8Array;
|
|
20
|
+
}
|
|
21
|
+
export type CodeItem = Instruction | LabelItem | BytesItem;
|
|
22
|
+
/** Exception table entry with label ids instead of byte offsets. */
|
|
23
|
+
export interface CodeException {
|
|
24
|
+
from: number;
|
|
25
|
+
to: number;
|
|
26
|
+
target: number;
|
|
27
|
+
excType: number;
|
|
28
|
+
varName: number;
|
|
29
|
+
}
|
|
30
|
+
export interface DecodedCode {
|
|
31
|
+
items: CodeItem[];
|
|
32
|
+
exceptions: CodeException[];
|
|
33
|
+
/** Next free label id. */
|
|
34
|
+
nextLabel: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function insn(op: number, args?: number[], targets?: number[]): Instruction;
|
|
37
|
+
export declare class CodeDecodeError extends Error {
|
|
38
|
+
name: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Decodes method code. Instructions are discovered by following control flow
|
|
42
|
+
* from the entry point and exception handlers, so junk bytes inserted by
|
|
43
|
+
* obfuscators are preserved as {@link BytesItem}s instead of breaking decoding.
|
|
44
|
+
* Falls back to linear decoding when control flow overlaps instructions.
|
|
45
|
+
*
|
|
46
|
+
* Throws if the code cannot be represented with labels (e.g. branches
|
|
47
|
+
* outside the method).
|
|
48
|
+
*/
|
|
49
|
+
export declare function decodeCode(code: Uint8Array, exceptions: readonly ExceptionInfo[]): DecodedCode;
|
|
50
|
+
/** Encoded size of an instruction in bytes. */
|
|
51
|
+
export declare function instructionSize(i: Instruction): number;
|
|
52
|
+
export interface EncodedCode {
|
|
53
|
+
code: Uint8Array;
|
|
54
|
+
exceptions: ExceptionInfo[];
|
|
55
|
+
/** Byte offset of every label id. */
|
|
56
|
+
labelOffsets: Map<number, number>;
|
|
57
|
+
}
|
|
58
|
+
/** Encodes a label-based instruction list back into bytecode. */
|
|
59
|
+
export declare function encodeCode(items: readonly CodeItem[], exceptions?: readonly CodeException[]): EncodedCode;
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CodeDecodeError = void 0;
|
|
4
|
+
exports.insn = insn;
|
|
5
|
+
exports.decodeCode = decodeCode;
|
|
6
|
+
exports.instructionSize = instructionSize;
|
|
7
|
+
exports.encodeCode = encodeCode;
|
|
8
|
+
/**
|
|
9
|
+
* AVM2 method body code: decoding to a label-based instruction list and
|
|
10
|
+
* encoding back to bytes.
|
|
11
|
+
*/
|
|
12
|
+
const reader_js_1 = require("../io/reader.js");
|
|
13
|
+
const writer_js_1 = require("../io/writer.js");
|
|
14
|
+
const opcodes_js_1 = require("./opcodes.js");
|
|
15
|
+
function insn(op, args = [], targets = []) {
|
|
16
|
+
return { type: 'op', op, args, targets, offset: -1 };
|
|
17
|
+
}
|
|
18
|
+
class CodeDecodeError extends Error {
|
|
19
|
+
name = "CodeDecodeError";
|
|
20
|
+
}
|
|
21
|
+
exports.CodeDecodeError = CodeDecodeError;
|
|
22
|
+
function readInsn(code, pos) {
|
|
23
|
+
const info = (0, opcodes_js_1.opInfo)(code[pos]);
|
|
24
|
+
if (!info)
|
|
25
|
+
return null;
|
|
26
|
+
const r = new reader_js_1.ByteReader(code, pos + 1);
|
|
27
|
+
const args = [];
|
|
28
|
+
const targets = [];
|
|
29
|
+
try {
|
|
30
|
+
for (const t of info.operands) {
|
|
31
|
+
switch (t) {
|
|
32
|
+
case 'u8':
|
|
33
|
+
args.push(r.u8());
|
|
34
|
+
break;
|
|
35
|
+
case 's8':
|
|
36
|
+
args.push(r.s8());
|
|
37
|
+
break;
|
|
38
|
+
case 'short':
|
|
39
|
+
args.push(r.encodedU32() | 0);
|
|
40
|
+
break;
|
|
41
|
+
case 'branch': {
|
|
42
|
+
const off = r.s24();
|
|
43
|
+
targets.push(r.pos + off);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case 'switch': {
|
|
47
|
+
targets.push(pos + r.s24());
|
|
48
|
+
const count = r.encodedU32();
|
|
49
|
+
for (let i = 0; i <= count; i++)
|
|
50
|
+
targets.push(pos + r.s24());
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
default:
|
|
54
|
+
args.push(r.encodedU32());
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return { op: info.code, args, targets, length: r.pos - pos };
|
|
62
|
+
}
|
|
63
|
+
function isTerminal(op) {
|
|
64
|
+
const f = (0, opcodes_js_1.opInfo)(op)?.flow;
|
|
65
|
+
return f === 'jump' || f === 'switch' || f === 'return' || f === 'throw';
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Decodes method code. Instructions are discovered by following control flow
|
|
69
|
+
* from the entry point and exception handlers, so junk bytes inserted by
|
|
70
|
+
* obfuscators are preserved as {@link BytesItem}s instead of breaking decoding.
|
|
71
|
+
* Falls back to linear decoding when control flow overlaps instructions.
|
|
72
|
+
*
|
|
73
|
+
* Throws if the code cannot be represented with labels (e.g. branches
|
|
74
|
+
* outside the method).
|
|
75
|
+
*/
|
|
76
|
+
function decodeCode(code, exceptions) {
|
|
77
|
+
try {
|
|
78
|
+
return decodeWith(code, exceptions, flowDecode(code, exceptions));
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
if (!(e instanceof CodeDecodeError))
|
|
82
|
+
throw e;
|
|
83
|
+
return decodeWith(code, exceptions, linearDecode(code));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function flowDecode(code, exceptions) {
|
|
87
|
+
const len = code.length;
|
|
88
|
+
const owner = new Int32Array(len).fill(-1); // start offset of the instruction covering each byte
|
|
89
|
+
const insns = new Map();
|
|
90
|
+
const work = [0];
|
|
91
|
+
for (const e of exceptions)
|
|
92
|
+
work.push(e.target);
|
|
93
|
+
while (work.length > 0) {
|
|
94
|
+
let pos = work.pop();
|
|
95
|
+
while (pos >= 0 && pos < len) {
|
|
96
|
+
if (insns.has(pos))
|
|
97
|
+
break;
|
|
98
|
+
if (owner[pos] !== -1)
|
|
99
|
+
throw new CodeDecodeError(`Overlapping instruction at ${pos}`);
|
|
100
|
+
const ins = readInsn(code, pos);
|
|
101
|
+
// Unknown opcode, or a branch leaving the method: keep as raw bytes.
|
|
102
|
+
if (!ins || ins.targets.some((t) => t < 0 || t > len))
|
|
103
|
+
break;
|
|
104
|
+
for (let i = 0; i < ins.length; i++) {
|
|
105
|
+
if (owner[pos + i] !== -1)
|
|
106
|
+
throw new CodeDecodeError(`Overlapping instruction at ${pos + i}`);
|
|
107
|
+
owner[pos + i] = pos;
|
|
108
|
+
}
|
|
109
|
+
insns.set(pos, ins);
|
|
110
|
+
for (const t of ins.targets)
|
|
111
|
+
work.push(t);
|
|
112
|
+
if (isTerminal(ins.op))
|
|
113
|
+
break;
|
|
114
|
+
pos += ins.length;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
decodeGaps(code, insns);
|
|
118
|
+
return insns;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Unreachable regions (dead code emitted by compilers, or obfuscator junk)
|
|
122
|
+
* are decoded linearly when they decode cleanly into whole instructions whose
|
|
123
|
+
* branch targets are valid; otherwise they stay raw bytes.
|
|
124
|
+
*/
|
|
125
|
+
function decodeGaps(code, insns) {
|
|
126
|
+
const len = code.length;
|
|
127
|
+
const sorted = [...insns.keys()].sort((a, b) => a - b);
|
|
128
|
+
const gaps = [];
|
|
129
|
+
let pos = 0;
|
|
130
|
+
for (const s of sorted) {
|
|
131
|
+
if (s > pos)
|
|
132
|
+
gaps.push({ start: pos, end: s });
|
|
133
|
+
pos = Math.max(pos, s + insns.get(s).length);
|
|
134
|
+
}
|
|
135
|
+
if (pos < len)
|
|
136
|
+
gaps.push({ start: pos, end: len });
|
|
137
|
+
const candidates = [];
|
|
138
|
+
for (const g of gaps) {
|
|
139
|
+
const found = new Map();
|
|
140
|
+
let p = g.start;
|
|
141
|
+
let ok = true;
|
|
142
|
+
while (p < g.end) {
|
|
143
|
+
const ins = readInsn(code, p);
|
|
144
|
+
if (!ins || p + ins.length > g.end || ins.targets.some((t) => t < 0 || t > len)) {
|
|
145
|
+
ok = false;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
found.set(p, ins);
|
|
149
|
+
p += ins.length;
|
|
150
|
+
}
|
|
151
|
+
if (ok && found.size > 0)
|
|
152
|
+
candidates.push(found);
|
|
153
|
+
}
|
|
154
|
+
// Accept gap decodings whose targets all land on instruction boundaries.
|
|
155
|
+
let changed = true;
|
|
156
|
+
let accepted = candidates;
|
|
157
|
+
while (changed) {
|
|
158
|
+
changed = false;
|
|
159
|
+
const starts = new Set(insns.keys());
|
|
160
|
+
for (const c of accepted)
|
|
161
|
+
for (const k of c.keys())
|
|
162
|
+
starts.add(k);
|
|
163
|
+
starts.add(len);
|
|
164
|
+
const next = accepted.filter((c) => {
|
|
165
|
+
for (const ins of c.values())
|
|
166
|
+
for (const t of ins.targets)
|
|
167
|
+
if (!starts.has(t))
|
|
168
|
+
return false;
|
|
169
|
+
return true;
|
|
170
|
+
});
|
|
171
|
+
if (next.length !== accepted.length) {
|
|
172
|
+
accepted = next;
|
|
173
|
+
changed = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const c of accepted)
|
|
177
|
+
for (const [k, v] of c)
|
|
178
|
+
insns.set(k, v);
|
|
179
|
+
}
|
|
180
|
+
function linearDecode(code) {
|
|
181
|
+
const insns = new Map();
|
|
182
|
+
let pos = 0;
|
|
183
|
+
while (pos < code.length) {
|
|
184
|
+
const ins = readInsn(code, pos);
|
|
185
|
+
if (!ins) {
|
|
186
|
+
pos++;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
insns.set(pos, ins);
|
|
190
|
+
pos += ins.length;
|
|
191
|
+
}
|
|
192
|
+
return insns;
|
|
193
|
+
}
|
|
194
|
+
function decodeWith(code, exceptions, insns) {
|
|
195
|
+
const len = code.length;
|
|
196
|
+
const starts = new Uint8Array(len + 1); // 1 = instruction start, 2 = inside instruction
|
|
197
|
+
for (const [pos, ins] of insns) {
|
|
198
|
+
starts[pos] = 1;
|
|
199
|
+
for (let i = 1; i < ins.length; i++)
|
|
200
|
+
starts[pos + i] = 2;
|
|
201
|
+
}
|
|
202
|
+
const labelAt = new Map();
|
|
203
|
+
let nextLabel = 0;
|
|
204
|
+
const label = (offset) => {
|
|
205
|
+
if (offset < 0 || offset > len)
|
|
206
|
+
throw new CodeDecodeError(`Branch/exception offset ${offset} outside code (0..${len})`);
|
|
207
|
+
if (starts[offset] === 2)
|
|
208
|
+
throw new CodeDecodeError(`Offset ${offset} points inside an instruction`);
|
|
209
|
+
let id = labelAt.get(offset);
|
|
210
|
+
if (id === undefined) {
|
|
211
|
+
id = nextLabel++;
|
|
212
|
+
labelAt.set(offset, id);
|
|
213
|
+
}
|
|
214
|
+
return id;
|
|
215
|
+
};
|
|
216
|
+
// Allocate labels in offset order for readable output.
|
|
217
|
+
const wanted = new Set();
|
|
218
|
+
for (const ins of insns.values())
|
|
219
|
+
for (const t of ins.targets)
|
|
220
|
+
wanted.add(t);
|
|
221
|
+
for (const e of exceptions) {
|
|
222
|
+
wanted.add(e.from);
|
|
223
|
+
wanted.add(e.to);
|
|
224
|
+
wanted.add(e.target);
|
|
225
|
+
}
|
|
226
|
+
for (const off of [...wanted].sort((a, b) => a - b))
|
|
227
|
+
label(off);
|
|
228
|
+
const items = [];
|
|
229
|
+
let rawStart = -1;
|
|
230
|
+
const flushRaw = (end) => {
|
|
231
|
+
if (rawStart >= 0) {
|
|
232
|
+
items.push({ type: 'bytes', data: code.slice(rawStart, end) });
|
|
233
|
+
rawStart = -1;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
let pos = 0;
|
|
237
|
+
while (pos <= len) {
|
|
238
|
+
const l = labelAt.get(pos);
|
|
239
|
+
if (l !== undefined) {
|
|
240
|
+
flushRaw(pos);
|
|
241
|
+
items.push({ type: 'label', id: l });
|
|
242
|
+
}
|
|
243
|
+
if (pos === len)
|
|
244
|
+
break;
|
|
245
|
+
const ins = insns.get(pos);
|
|
246
|
+
if (ins) {
|
|
247
|
+
flushRaw(pos);
|
|
248
|
+
items.push({
|
|
249
|
+
type: 'op',
|
|
250
|
+
op: ins.op,
|
|
251
|
+
args: ins.args,
|
|
252
|
+
targets: ins.targets.map((t) => labelAt.get(t)),
|
|
253
|
+
offset: pos,
|
|
254
|
+
});
|
|
255
|
+
pos += ins.length;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
if (rawStart < 0)
|
|
259
|
+
rawStart = pos;
|
|
260
|
+
pos++;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
flushRaw(len);
|
|
264
|
+
return {
|
|
265
|
+
items,
|
|
266
|
+
exceptions: exceptions.map((e) => ({
|
|
267
|
+
from: labelAt.get(e.from),
|
|
268
|
+
to: labelAt.get(e.to),
|
|
269
|
+
target: labelAt.get(e.target),
|
|
270
|
+
excType: e.excType,
|
|
271
|
+
varName: e.varName,
|
|
272
|
+
})),
|
|
273
|
+
nextLabel,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function u30Size(v) {
|
|
277
|
+
v >>>= 0;
|
|
278
|
+
let n = 1;
|
|
279
|
+
while (v >= 0x80) {
|
|
280
|
+
v >>>= 7;
|
|
281
|
+
n++;
|
|
282
|
+
}
|
|
283
|
+
return n;
|
|
284
|
+
}
|
|
285
|
+
function operandSize(t, value) {
|
|
286
|
+
switch (t) {
|
|
287
|
+
case 'u8':
|
|
288
|
+
case 's8':
|
|
289
|
+
return 1;
|
|
290
|
+
case 'branch':
|
|
291
|
+
return 3;
|
|
292
|
+
default:
|
|
293
|
+
return u30Size(value);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/** Encoded size of an instruction in bytes. */
|
|
297
|
+
function instructionSize(i) {
|
|
298
|
+
const info = (0, opcodes_js_1.opInfo)(i.op);
|
|
299
|
+
if (!info)
|
|
300
|
+
throw new Error(`Unknown opcode 0x${i.op.toString(16)}`);
|
|
301
|
+
let size = 1;
|
|
302
|
+
let argIndex = 0;
|
|
303
|
+
for (const t of info.operands) {
|
|
304
|
+
if (t === 'switch') {
|
|
305
|
+
if (i.targets.length < 2)
|
|
306
|
+
throw new Error('lookupswitch needs a default target and at least one case');
|
|
307
|
+
size += 3 + u30Size(i.targets.length - 2) + 3 * (i.targets.length - 1);
|
|
308
|
+
}
|
|
309
|
+
else if (t === 'branch') {
|
|
310
|
+
size += 3;
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
size += operandSize(t, i.args[argIndex++] ?? 0);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return size;
|
|
317
|
+
}
|
|
318
|
+
/** Encodes a label-based instruction list back into bytecode. */
|
|
319
|
+
function encodeCode(items, exceptions = []) {
|
|
320
|
+
const labelOffsets = new Map();
|
|
321
|
+
let offset = 0;
|
|
322
|
+
for (const item of items) {
|
|
323
|
+
if (item.type === 'label') {
|
|
324
|
+
if (labelOffsets.has(item.id))
|
|
325
|
+
throw new Error(`Label ${item.id} defined twice`);
|
|
326
|
+
labelOffsets.set(item.id, offset);
|
|
327
|
+
}
|
|
328
|
+
else if (item.type === 'bytes')
|
|
329
|
+
offset += item.data.length;
|
|
330
|
+
else
|
|
331
|
+
offset += instructionSize(item);
|
|
332
|
+
}
|
|
333
|
+
const target = (id) => {
|
|
334
|
+
const o = labelOffsets.get(id);
|
|
335
|
+
if (o === undefined)
|
|
336
|
+
throw new Error(`Undefined label ${id}`);
|
|
337
|
+
return o;
|
|
338
|
+
};
|
|
339
|
+
const w = new writer_js_1.ByteWriter(offset + 16);
|
|
340
|
+
for (const item of items) {
|
|
341
|
+
if (item.type === 'label')
|
|
342
|
+
continue;
|
|
343
|
+
if (item.type === 'bytes') {
|
|
344
|
+
w.bytes(item.data);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const start = w.length;
|
|
348
|
+
const info = (0, opcodes_js_1.opInfo)(item.op);
|
|
349
|
+
w.u8(item.op);
|
|
350
|
+
let argIndex = 0;
|
|
351
|
+
for (const t of info.operands) {
|
|
352
|
+
switch (t) {
|
|
353
|
+
case 'u8':
|
|
354
|
+
w.u8(item.args[argIndex++] ?? 0);
|
|
355
|
+
break;
|
|
356
|
+
case 's8':
|
|
357
|
+
w.s8(item.args[argIndex++] ?? 0);
|
|
358
|
+
break;
|
|
359
|
+
case 'branch': {
|
|
360
|
+
const end = w.length + 3;
|
|
361
|
+
w.s24(target(item.targets[0]) - end);
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
case 'switch': {
|
|
365
|
+
w.s24(target(item.targets[0]) - start);
|
|
366
|
+
w.encodedU32(item.targets.length - 2);
|
|
367
|
+
for (let k = 1; k < item.targets.length; k++)
|
|
368
|
+
w.s24(target(item.targets[k]) - start);
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
default:
|
|
372
|
+
w.encodedU32((item.args[argIndex++] ?? 0) >>> 0);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
code: w.toBytes(),
|
|
378
|
+
exceptions: exceptions.map((e) => ({
|
|
379
|
+
from: target(e.from),
|
|
380
|
+
to: target(e.to),
|
|
381
|
+
target: target(e.target),
|
|
382
|
+
excType: e.excType,
|
|
383
|
+
varName: e.varName,
|
|
384
|
+
})),
|
|
385
|
+
labelOffsets,
|
|
386
|
+
};
|
|
387
|
+
}
|