js-confuser-vm 0.0.6 → 0.0.7

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.MD +101 -37
  3. package/dist/build-runtime.js +15 -2
  4. package/dist/compiler.js +98 -51
  5. package/dist/runtime.js +5 -1
  6. package/dist/transforms/bytecode/aliasedOpcodes.js +2 -8
  7. package/dist/transforms/bytecode/macroOpcodes.js +21 -19
  8. package/dist/transforms/bytecode/microOpcodes.js +236 -0
  9. package/dist/transforms/bytecode/resolveContants.js +5 -11
  10. package/dist/transforms/bytecode/resolveLabels.js +5 -3
  11. package/dist/transforms/bytecode/specializedOpcodes.js +21 -16
  12. package/dist/transforms/runtime/internalVariables.js +202 -0
  13. package/dist/transforms/runtime/macroOpcodes.js +30 -18
  14. package/dist/transforms/runtime/microOpcodes.js +76 -0
  15. package/dist/transforms/runtime/specializedOpcodes.js +20 -18
  16. package/dist/utils/op-utils.js +15 -8
  17. package/index.ts +3 -2
  18. package/jest.config.js +2 -0
  19. package/package.json +1 -1
  20. package/src/build-runtime.ts +18 -3
  21. package/src/compiler.ts +152 -65
  22. package/src/options.ts +1 -0
  23. package/src/runtime.ts +5 -1
  24. package/src/transforms/bytecode/aliasedOpcodes.ts +2 -12
  25. package/src/transforms/bytecode/macroOpcodes.ts +28 -29
  26. package/src/transforms/bytecode/microOpcodes.ts +291 -0
  27. package/src/transforms/bytecode/resolveContants.ts +6 -13
  28. package/src/transforms/bytecode/resolveLabels.ts +5 -4
  29. package/src/transforms/bytecode/specializedOpcodes.ts +38 -28
  30. package/src/transforms/runtime/internalVariables.ts +270 -0
  31. package/src/transforms/runtime/macroOpcodes.ts +47 -20
  32. package/src/transforms/runtime/microOpcodes.ts +93 -0
  33. package/src/transforms/runtime/specializedOpcodes.ts +27 -32
  34. package/src/types.ts +1 -1
  35. package/src/utils/op-utils.ts +21 -8
  36. package/src/utilts.ts +0 -3
@@ -0,0 +1,76 @@
1
+ import * as t from "@babel/types";
2
+ import traverseImport from "@babel/traverse";
3
+ import { ok } from "assert";
4
+ import { applyInternalVariablesToSwitchCase } from "./internalVariables.js";
5
+ const traverse = traverseImport.default || traverseImport;
6
+
7
+ // Extract the real statement list from a SwitchCase consequent.
8
+ function extractCaseBody(switchCase) {
9
+ let stmts;
10
+ if (switchCase.consequent.length === 1 && t.isBlockStatement(switchCase.consequent[0])) {
11
+ stmts = switchCase.consequent[0].body;
12
+ } else {
13
+ stmts = switchCase.consequent;
14
+ }
15
+ return stmts.filter(s => !t.isBreakStatement(s) && !t.isEmptyStatement(s));
16
+ }
17
+
18
+ // Append a generated switch case for every entry in compiler.MICRO_OPS.
19
+ // applyInteralVariablesToRuntime must run before this so that the source
20
+ // case bodies are already using this._internals[index] instead of local vars.
21
+ // Must be called BEFORE applyShuffleOpcodes so the new cases get shuffled.
22
+ export function applyMicroOpcodes(ast, compiler) {
23
+ if (!compiler.MICRO_OPS || Object.keys(compiler.MICRO_OPS).length === 0) {
24
+ return;
25
+ }
26
+ let switchStatement = null;
27
+ traverse(ast, {
28
+ SwitchStatement(path) {
29
+ if (path.node.leadingComments?.some(c => c.value.includes("@SWITCH"))) {
30
+ switchStatement = path.node;
31
+ path.stop();
32
+ }
33
+ }
34
+ });
35
+ ok(switchStatement, "Could not find @SWITCH statement for micro opcodes");
36
+
37
+ // Build opName → SwitchCase from existing cases.
38
+ const nameToCaseMap = new Map();
39
+ for (const sc of switchStatement.cases) {
40
+ const test = sc.test;
41
+ if (test && t.isMemberExpression(test) && t.isIdentifier(test.object, {
42
+ name: "OP"
43
+ }) && t.isIdentifier(test.property)) {
44
+ nameToCaseMap.set(test.property.name, sc);
45
+ }
46
+ }
47
+ for (const [microOpStr, info] of Object.entries(compiler.MICRO_OPS)) {
48
+ const microOpCode = Number(microOpStr);
49
+ const {
50
+ originalOp,
51
+ stmtIndex
52
+ } = info;
53
+ const originalName = compiler.OP_NAME[originalOp];
54
+ if (!originalName) continue;
55
+ const originalCase = nameToCaseMap.get(originalName);
56
+ if (!originalCase) continue;
57
+
58
+ // Extract and clone all non-break statements from the original case body.
59
+ const allStmts = extractCaseBody(originalCase);
60
+ if (stmtIndex >= allStmts.length) continue;
61
+ const rawStmt = t.cloneNode(allStmts[stmtIndex], true);
62
+ const newCase = t.switchCase(t.numericLiteral(microOpCode), [t.blockStatement([rawStmt, t.breakStatement()])]);
63
+
64
+ // Apply internal-variable substitution — this may replace rawStmt in the
65
+ // block body (var decl → assignment), so add the comment afterwards on
66
+ // whatever the first statement of the block actually is.
67
+ applyInternalVariablesToSwitchCase(newCase, compiler, microOpCode);
68
+ const blockBody = newCase.consequent[0].body;
69
+ const firstStmt = blockBody[0];
70
+ if (firstStmt) {
71
+ const microName = compiler.OP_NAME[microOpCode] ?? `MICRO_${microOpCode}`;
72
+ t.addComment(firstStmt, "leading", ` ${microName}`, true);
73
+ }
74
+ switchStatement.cases.push(newCase);
75
+ }
76
+ }
@@ -1,6 +1,7 @@
1
1
  import * as t from "@babel/types";
2
2
  import traverseImport from "@babel/traverse";
3
3
  import { ok } from "assert";
4
+ import { getOpcodeToCaseMap } from "./macroOpcodes.js";
4
5
  const traverse = traverseImport.default || traverseImport;
5
6
 
6
7
  // Extract the real statement list from a SwitchCase consequent (identical to the
@@ -23,6 +24,11 @@ function inlineFixedOperands(bodyStmts, resolvedValues) {
23
24
  // Wrap the statements in a temporary BlockStatement so traverse has a root.
24
25
  // The replacement mutates the original statement objects in place.
25
26
  var replaced = 0;
27
+ function consumeOperand() {
28
+ const resolvedValue = resolvedValues[replaced++];
29
+ ok(typeof resolvedValue === "number", `Expected a numeric operand value, got ${resolvedValue}`);
30
+ return t.numericLiteral(resolvedValue);
31
+ }
26
32
  traverse(t.blockStatement(bodyStmts), {
27
33
  noScope: true,
28
34
  CallExpression(path) {
@@ -33,10 +39,10 @@ function inlineFixedOperands(bodyStmts, resolvedValues) {
33
39
  }) && path.node.arguments.length === 0;
34
40
  };
35
41
  if (isMethodCall("_operand")) {
36
- path.replaceWith(t.numericLiteral(resolvedValues[replaced++]));
42
+ path.replaceWith(consumeOperand());
37
43
  }
38
44
  if (isMethodCall("_constant")) {
39
- path.node.arguments = [t.numericLiteral(resolvedValues[replaced++]), t.numericLiteral(resolvedValues[replaced++])];
45
+ path.node.arguments = [consumeOperand(), consumeOperand()];
40
46
  }
41
47
  }
42
48
  });
@@ -48,7 +54,7 @@ function inlineFixedOperands(bodyStmts, resolvedValues) {
48
54
  // replaced by the constant integer that was captured at compile time.
49
55
  // Must be called AFTER applyMacroOpcodes (so the original cases exist) but
50
56
  // BEFORE applyShuffleOpcodes so the new specialized cases also get shuffled.
51
- export function applySpecializedOpcodes(ast, bytecode, compiler) {
57
+ export function applySpecializedOpcodes(ast, compiler) {
52
58
  let switchStatement = null;
53
59
  traverse(ast, {
54
60
  SwitchStatement(path) {
@@ -61,15 +67,7 @@ export function applySpecializedOpcodes(ast, bytecode, compiler) {
61
67
  ok(switchStatement, "Could not find @SWITCH statement for specialized opcodes");
62
68
 
63
69
  // Build a map opName → SwitchCase from the existing OP.xxx case tests.
64
- const nameToCaseMap = new Map();
65
- for (const sc of switchStatement.cases) {
66
- const test = sc.test;
67
- if (test && t.isMemberExpression(test) && t.isIdentifier(test.object, {
68
- name: "OP"
69
- }) && t.isIdentifier(test.property)) {
70
- nameToCaseMap.set(test.property.name, sc);
71
- }
72
- }
70
+ const opcodeToCaseMap = getOpcodeToCaseMap(switchStatement, compiler);
73
71
  if (!compiler.SPECIALIZED_OPS) return;
74
72
  for (const [specialOpStr, info] of Object.entries(compiler.SPECIALIZED_OPS)) {
75
73
  const specialOpCode = Number(specialOpStr);
@@ -77,20 +75,24 @@ export function applySpecializedOpcodes(ast, bytecode, compiler) {
77
75
  originalOp,
78
76
  operands
79
77
  } = info;
80
- const newName = compiler.OP_NAME[specialOpCode];
78
+ let newName = compiler.OP_NAME[specialOpCode];
81
79
  const originalName = compiler.OP_NAME[originalOp];
82
- if (!originalName) continue;
83
- const originalCase = nameToCaseMap.get(originalName);
84
- if (!originalCase) continue;
80
+ const originalCase = opcodeToCaseMap.get(originalOp);
81
+ ok(originalCase, `Could not find original case for opcode ${originalName} (${originalOp})`);
85
82
 
86
83
  // Clone the original handler body
87
84
  const bodyStmts = extractCaseBody(originalCase).map(s => t.cloneNode(s, true));
88
- const placedOperands = info.resolvedOperands;
85
+ const placedOperands = info.operands;
89
86
  ok(placedOperands, `Could not find operand for original opcode ${newName}`);
90
87
  const resolvedValues = placedOperands.map(placedOperand => {
91
88
  return placedOperand?.resolvedValue ?? placedOperand;
92
89
  });
93
- ok(!resolvedValues.find(v => typeof v !== "number"), "Expected a numeric operand value");
90
+ if (resolvedValues.find(v => typeof v !== "number")) {
91
+ console.error(info);
92
+ throw new Error("Expected all resolved operand values to be numbers");
93
+ }
94
+ newName = `${originalName}_${resolvedValues.join("_")}`;
95
+ compiler.OP_NAME[specialOpCode] = newName;
94
96
 
95
97
  // Replace this._operand() with the baked-in constant
96
98
  inlineFixedOperands(bodyStmts, resolvedValues);
@@ -2,18 +2,25 @@ import { getRandomInt } from "./random-utils.js";
2
2
  export const U16_MAX = 0xffff; // bytecode operands are u16
3
3
 
4
4
  /** Returns the next free opcode slot, or -1 when the space is exhausted. */
5
- export function nextFreeSlot(usedOpcodes) {
5
+ export function nextFreeSlot(compiler) {
6
+ // ── Collect used opcodes exactly as specified ─────────────────────────────
7
+ const usedOpcodes = new Set(Object.keys(compiler.OP_NAME).map(k => parseInt(k, 10)).filter(v => !isNaN(v)));
6
8
  if (usedOpcodes.size > U16_MAX) return -1;
7
- let attempts = 0;
8
- while (attempts++ < 512) {
9
- const candidate = getRandomInt(0, U16_MAX);
10
- if (!usedOpcodes.has(candidate)) {
11
- usedOpcodes.add(candidate);
12
- return candidate;
9
+
10
+ // Random opcode
11
+ if (compiler.options.randomizeOpcodes) {
12
+ let attempts = 0;
13
+ while (attempts++ < 512) {
14
+ const candidate = getRandomInt(0, U16_MAX);
15
+ if (!usedOpcodes.has(candidate)) {
16
+ usedOpcodes.add(candidate);
17
+ return candidate;
18
+ }
13
19
  }
14
20
  }
21
+
15
22
  // Fallback: linear scan from a random start
16
- const start = getRandomInt(0, U16_MAX);
23
+ const start = Object.keys(compiler.OP_NAME).length;
17
24
  for (let i = 0; i <= U16_MAX; i++) {
18
25
  const v = start + i & U16_MAX;
19
26
  if (!usedOpcodes.has(v)) {
package/index.ts CHANGED
@@ -11,14 +11,15 @@ async function main() {
11
11
  target: "browser", // or "node"
12
12
  randomizeOpcodes: true, // randomize the opcode numbers?
13
13
  shuffleOpcodes: true, // shuffle order of opcode handlers in the runtime?
14
- encodeBytecode: true, // encode bytecode? when off, comments for instructions are added
14
+ encodeBytecode: true, // encode the bytecode array?
15
+ concealConstants: true, // conceal strings and integers in the constant pool?
15
16
  selfModifying: true, // do self-modifying bytecode for function bodies?
16
17
  macroOpcodes: true, // create combined opcodes for repeated instruction sequences?
18
+ microOpcodes: true, // break opcodes into sub-opcodes?
17
19
  specializedOpcodes: true, // create specialized opcodes for commonly used opcode+operand pairs?
18
20
  aliasedOpcodes: true, // create duplicate opcodes for commonly used opcodes?
19
21
  timingChecks: true, // add timing checks to detect debuggers?
20
22
  minify: true, // pass final output through Google Closure Compiler? (Renames VM class properties)
21
- concealConstants: true,
22
23
  });
23
24
 
24
25
  writeFileSync("output.original.js", orginalOutput, "utf-8");
package/jest.config.js CHANGED
@@ -6,6 +6,7 @@ const OPTIONS_MATRIX = [
6
6
  { displayName: "selfModifying", VM_OPTIONS: { selfModifying: true } },
7
7
  { displayName: "timingChecks", VM_OPTIONS: { timingChecks: true } },
8
8
  { displayName: "macroOpcodes", VM_OPTIONS: { macroOpcodes: true } },
9
+ { displayName: "microOpcodes", VM_OPTIONS: { microOpcodes: true } },
9
10
  {
10
11
  displayName: "specializedOpcodes",
11
12
  VM_OPTIONS: { specializedOpcodes: true },
@@ -27,6 +28,7 @@ const OPTIONS_MATRIX = [
27
28
  selfModifying: true,
28
29
  timingChecks: true,
29
30
  macroOpcodes: true,
31
+ microOpcodes: true,
30
32
  specializedOpcodes: true,
31
33
  aliasedOpcodes: true,
32
34
  concealConstants: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-confuser-vm",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
6
  "build": "babel src --out-dir dist --extensions \".ts,.js\"",
@@ -1,8 +1,10 @@
1
1
  import { generate } from "@babel/generator";
2
- import { parse, type ParseResult } from "@babel/parser";
2
+ import { parse } from "@babel/parser";
3
3
  import type * as t from "@babel/types";
4
4
  import type { Options } from "./options.ts";
5
5
  import { applyMacroOpcodes } from "./transforms/runtime/macroOpcodes.ts";
6
+ import { applyMicroOpcodes } from "./transforms/runtime/microOpcodes.ts";
7
+ import { applyInteralVariablesToRuntime } from "./transforms/runtime/internalVariables.ts";
6
8
  import { applyShuffleOpcodes } from "./transforms/runtime/shuffleOpcodes.ts";
7
9
  import { applyMinify } from "./transforms/runtime/minify.ts";
8
10
  import { Compiler } from "./compiler.ts";
@@ -14,7 +16,8 @@ export async function obfuscateRuntime(
14
16
  runtime: string,
15
17
  bytecode: b.Bytecode,
16
18
  options: Options,
17
- compiler?: Compiler,
19
+ compiler: Compiler,
20
+ generateBytecodeComment,
18
21
  ) {
19
22
  let ast: t.File;
20
23
  try {
@@ -25,7 +28,16 @@ export async function obfuscateRuntime(
25
28
 
26
29
  // Specialized opcode cases must be applied BEFORE shuffleOpcodes
27
30
  if (options.specializedOpcodes) {
28
- applySpecializedOpcodes(ast, bytecode, compiler);
31
+ applySpecializedOpcodes(ast, compiler);
32
+ }
33
+
34
+ if (options.microOpcodes) {
35
+ applyInteralVariablesToRuntime(ast, compiler);
36
+ }
37
+
38
+ // Micro opcode cases must be applied BEFORE shuffleOpcodes
39
+ if (options.microOpcodes && Object.keys(compiler.MICRO_OPS).length > 0) {
40
+ applyMicroOpcodes(ast, compiler);
29
41
  }
30
42
 
31
43
  // Macro opcode cases must be applied BEFORE shuffleOpcodes
@@ -50,6 +62,9 @@ export async function obfuscateRuntime(
50
62
  throw new Error("VM-Runtime final generation failed", { cause: error });
51
63
  }
52
64
 
65
+ // Add comment here for more accurate opcode names
66
+ generated = generateBytecodeComment() + "\n" + generated;
67
+
53
68
  // Minify code?
54
69
  if (options.minify) {
55
70
  try {
package/src/compiler.ts CHANGED
@@ -1,11 +1,11 @@
1
+ import * as t from "@babel/types";
2
+ import * as b from "./types.ts";
1
3
  import { parse } from "@babel/parser";
2
4
  import traverseImport from "@babel/traverse";
3
5
  import { generate } from "@babel/generator";
4
-
5
- import { readFileSync } from "fs";
6
6
  import { join } from "path";
7
+ import { readFileSync } from "fs";
7
8
  import { stripTypeScriptTypes } from "module";
8
- import * as t from "@babel/types";
9
9
  import { ok } from "assert";
10
10
  import { obfuscateRuntime } from "./build-runtime.ts";
11
11
  import { DEFAULT_OPTIONS, type Options } from "./options.ts";
@@ -13,7 +13,7 @@ import { resolveLabels } from "./transforms/bytecode/resolveLabels.ts";
13
13
  import { resolveConstants } from "./transforms/bytecode/resolveContants.ts";
14
14
  import { selfModifying } from "./transforms/bytecode/selfModifying.ts";
15
15
  import { macroOpcodes } from "./transforms/bytecode/macroOpcodes.ts";
16
- import * as b from "./types.ts";
16
+ import { microOpcodes } from "./transforms/bytecode/microOpcodes.ts";
17
17
  import { specializedOpcodes } from "./transforms/bytecode/specializedOpcodes.ts";
18
18
  import { aliasedOpcodes } from "./transforms/bytecode/aliasedOpcodes.ts";
19
19
  import { getRandomInt } from "./utils/random-utils.ts";
@@ -264,10 +264,21 @@ export class Compiler {
264
264
  {
265
265
  originalOp: number;
266
266
  operands: b.InstrOperand[];
267
- resolvedOperands?: b.InstrOperand[];
268
267
  }
269
268
  >;
270
269
  ALIASED_OPS: Record<number, { originalOp: number; order: number[] }>;
270
+ MICRO_OPS: Record<
271
+ number,
272
+ { originalOp: number; stmtIndex: number; irOperandCount: number }
273
+ >;
274
+
275
+ /** Internal variable slot registry.
276
+ * globally: shared name→index pool (written on first sight; reused by non-random mode or by 50% chance in random mode).
277
+ * opcodes: per-opcode source-of-truth — all assignment lookups are read/written here. */
278
+ _internals: {
279
+ globally: Map<string, number>;
280
+ opcodes: Map<number, Map<string, number>>;
281
+ };
271
282
 
272
283
  OP_NAME: Record<number, string>;
273
284
  JUMP_OPS: Set<number>;
@@ -293,8 +304,10 @@ export class Compiler {
293
304
 
294
305
  this.serializer = new Serializer(this);
295
306
  this.MACRO_OPS = {};
307
+ this.MICRO_OPS = {};
296
308
  this.SPECIALIZED_OPS = {};
297
309
  this.ALIASED_OPS = {};
310
+ this._internals = { globally: new Map(), opcodes: new Map() };
298
311
 
299
312
  this.OP = { ...OP_ORIGINAL };
300
313
 
@@ -455,14 +468,6 @@ export class Compiler {
455
468
  }
456
469
 
457
470
  compileAST(ast: t.File) {
458
- traverse(ast, {
459
- FunctionDeclaration: (path) => {
460
- if (path.parent.type !== "Program") return;
461
- this._compileFunctionDecl(path.node);
462
- path.skip();
463
- },
464
- });
465
-
466
471
  this._compileMain(ast.program.body);
467
472
  return this.bytecode;
468
473
  }
@@ -1291,14 +1296,10 @@ export class Compiler {
1291
1296
 
1292
1297
  case "TryStatement": {
1293
1298
  if (node.finalizer) {
1294
- throw new Error(
1295
- "try..finally is not supported. Use a helper function instead",
1296
- );
1299
+ throw new Error("try..finally is not supported");
1297
1300
  }
1298
1301
  if (!node.handler) {
1299
- throw new Error(
1300
- "try without catch is not supported (requires finally).",
1301
- );
1302
+ throw new Error("try without catch is not supported");
1302
1303
  }
1303
1304
 
1304
1305
  const catchLabel = this._makeLabel("catch");
@@ -1538,6 +1539,45 @@ export class Compiler {
1538
1539
  return reg_result;
1539
1540
  }
1540
1541
 
1542
+ case "TemplateLiteral": {
1543
+ const n = node as t.TemplateLiteral;
1544
+ // Fold: quasi[0] + expr[0] + quasi[1] + ... + quasi[last]
1545
+ let acc = ctx.allocReg();
1546
+ this.emit(
1547
+ bc,
1548
+ [
1549
+ this.OP.LOAD_CONST,
1550
+ acc,
1551
+ b.constantOperand(n.quasis[0].value.cooked ?? ""),
1552
+ ],
1553
+ node,
1554
+ );
1555
+ for (let i = 0; i < n.expressions.length; i++) {
1556
+ const exprReg = this._compileExpr(
1557
+ n.expressions[i] as t.Expression,
1558
+ scope,
1559
+ bc,
1560
+ );
1561
+ const t1 = ctx.allocReg();
1562
+ this.emit(bc, [this.OP.ADD, t1, acc, exprReg], node);
1563
+ acc = t1;
1564
+ const quasiReg = ctx.allocReg();
1565
+ this.emit(
1566
+ bc,
1567
+ [
1568
+ this.OP.LOAD_CONST,
1569
+ quasiReg,
1570
+ b.constantOperand(n.quasis[i + 1].value.cooked ?? ""),
1571
+ ],
1572
+ node,
1573
+ );
1574
+ const t2 = ctx.allocReg();
1575
+ this.emit(bc, [this.OP.ADD, t2, acc, quasiReg], node);
1576
+ acc = t2;
1577
+ }
1578
+ return acc;
1579
+ }
1580
+
1541
1581
  case "BinaryExpression": {
1542
1582
  const n = node as t.BinaryExpression;
1543
1583
  const lhsReg = this._compileExpr(n.left as t.Expression, scope, bc);
@@ -1579,15 +1619,68 @@ export class Compiler {
1579
1619
 
1580
1620
  case "UpdateExpression": {
1581
1621
  const n = node as t.UpdateExpression;
1622
+ const bumpOp = n.operator === "++" ? this.OP.ADD : this.OP.SUB;
1623
+
1624
+ // Shared: compute curReg +/- 1 into newReg, return [postfixResult, newReg]
1625
+ const applyBump = (curReg: number): [number, number] => {
1626
+ const postfixReg = n.prefix
1627
+ ? -1
1628
+ : (() => {
1629
+ const r = ctx.allocReg();
1630
+ this.emit(bc, [this.OP.MOVE, r, curReg], node as t.Node);
1631
+ return r;
1632
+ })();
1633
+ const oneReg = ctx.allocReg();
1634
+ this.emit(
1635
+ bc,
1636
+ [this.OP.LOAD_CONST, oneReg, b.constantOperand(1)],
1637
+ node as t.Node,
1638
+ );
1639
+ const newReg = ctx.allocReg();
1640
+ this.emit(bc, [bumpOp, newReg, curReg, oneReg], node as t.Node);
1641
+ return [postfixReg, newReg];
1642
+ };
1643
+
1644
+ if (n.argument.type === "MemberExpression") {
1645
+ const mem = n.argument as t.MemberExpression;
1646
+ const objReg = this._compileExpr(mem.object, scope, bc);
1647
+ let keyReg: number;
1648
+ if (mem.computed) {
1649
+ keyReg = this._compileExpr(mem.property as t.Expression, scope, bc);
1650
+ } else {
1651
+ keyReg = ctx.allocReg();
1652
+ this.emit(
1653
+ bc,
1654
+ [
1655
+ this.OP.LOAD_CONST,
1656
+ keyReg,
1657
+ b.constantOperand((mem.property as t.Identifier).name),
1658
+ ],
1659
+ node as t.Node,
1660
+ );
1661
+ }
1662
+ const curReg = ctx.allocReg();
1663
+ this.emit(
1664
+ bc,
1665
+ [this.OP.GET_PROP, curReg, objReg, keyReg],
1666
+ node as t.Node,
1667
+ );
1668
+ const [postfixReg, newReg] = applyBump(curReg);
1669
+ this.emit(
1670
+ bc,
1671
+ [this.OP.SET_PROP, objReg, keyReg, newReg],
1672
+ node as t.Node,
1673
+ );
1674
+ return n.prefix ? newReg : postfixReg;
1675
+ }
1676
+
1582
1677
  ok(
1583
1678
  n.argument.type === "Identifier",
1584
- "UpdateExpression requires identifier",
1679
+ "UpdateExpression requires identifier or member expression",
1585
1680
  );
1586
1681
  const name = (n.argument as t.Identifier).name;
1587
1682
  const res = this._resolve(name, this._currentCtx);
1588
- const bumpOp = n.operator === "++" ? this.OP.ADD : this.OP.SUB;
1589
1683
 
1590
- // Load current value into a register (locals are already in place)
1591
1684
  let curReg: number;
1592
1685
  if (res.kind === "local") {
1593
1686
  curReg = res.slot;
@@ -1607,27 +1700,8 @@ export class Compiler {
1607
1700
  );
1608
1701
  }
1609
1702
 
1610
- // For postfix we need to preserve the *old* value as the result
1611
- let resultReg: number;
1612
- if (!n.prefix) {
1613
- resultReg = ctx.allocReg();
1614
- this.emit(bc, [this.OP.MOVE, resultReg, curReg], node as t.Node);
1615
- } else {
1616
- resultReg = -1; // placeholder – will become newReg below
1617
- }
1618
-
1619
- const oneReg = ctx.allocReg();
1620
- this.emit(
1621
- bc,
1622
- [this.OP.LOAD_CONST, oneReg, b.constantOperand(1)],
1623
- node as t.Node,
1624
- );
1625
-
1626
- // Compute new value (always into a fresh register)
1627
- const newReg = ctx.allocReg();
1628
- this.emit(bc, [bumpOp, newReg, curReg, oneReg], node as t.Node);
1703
+ const [postfixReg, newReg] = applyBump(curReg);
1629
1704
 
1630
- // Write the new value back (local = MOVE, others = STORE_xxx)
1631
1705
  if (res.kind === "local") {
1632
1706
  this.emit(bc, [this.OP.MOVE, res.slot, newReg], node as t.Node);
1633
1707
  } else if (res.kind === "upvalue") {
@@ -1644,12 +1718,7 @@ export class Compiler {
1644
1718
  );
1645
1719
  }
1646
1720
 
1647
- // Prefix returns the *new* value (we already have it in newReg no reload needed)
1648
- if (n.prefix) {
1649
- resultReg = newReg;
1650
- }
1651
-
1652
- return resultReg;
1721
+ return n.prefix ? newReg : postfixReg;
1653
1722
  }
1654
1723
 
1655
1724
  case "AssignmentExpression": {
@@ -2138,13 +2207,12 @@ class Serializer {
2138
2207
  return out;
2139
2208
  }
2140
2209
 
2141
- _serializeInstr(
2142
- instr: b.Instruction,
2143
- constants: any[],
2144
- ): { text: string; values: number[] } {
2210
+ _serializeInstr(instr: b.Instruction): { text: string; values: number[] } {
2145
2211
  const op = instr[0] as number;
2146
2212
  const operands = instr.slice(1) as number[];
2147
2213
 
2214
+ const constants = this.compiler.constants;
2215
+
2148
2216
  const resolvedOperands = operands
2149
2217
  .filter((operand) => (operand as any)?.placeholder !== true)
2150
2218
  .map((o) => (o as any)?.resolvedValue ?? o);
@@ -2155,7 +2223,11 @@ class Serializer {
2155
2223
  }
2156
2224
  ok(op >= 0 && op <= 0xffff, `Opcode overflow (max 0xFFFF u16): ${op}`);
2157
2225
 
2158
- const name = this.OP_NAME[op] || `OP_${op}`;
2226
+ let name = this.OP_NAME[op];
2227
+ if (!name || name.includes("{")) {
2228
+ name = `OP_${op}`;
2229
+ }
2230
+
2159
2231
  let comment = name;
2160
2232
 
2161
2233
  function formatLoc(loc: t.Node["loc"]["start"]) {
@@ -2205,10 +2277,10 @@ class Serializer {
2205
2277
  comment += ` reg[${dst}] PC=${resolvedOperands[1]} (params=${resolvedOperands[2]} regs=${resolvedOperands[3]} upvalues=${resolvedOperands[4]})`;
2206
2278
  break;
2207
2279
  case this.OP.CALL:
2208
- comment += ` reg[${dst}] = call(reg[${resolvedOperands[1]}], ${resolvedOperands[2]} args)`;
2280
+ comment += ` reg[${dst}] = reg[${resolvedOperands[1]}](${resolvedOperands[2]} args)`;
2209
2281
  break;
2210
2282
  case this.OP.CALL_METHOD:
2211
- comment += ` reg[${dst}] = method(recv=reg[${resolvedOperands[1]}], fn=reg[${resolvedOperands[2]}], ${resolvedOperands[3]} args)`;
2283
+ comment += ` reg[${dst}] = reg[${resolvedOperands[2]}](recv=reg[${resolvedOperands[1]}], ${resolvedOperands[3]} args)`;
2212
2284
  break;
2213
2285
  case this.OP.NEW:
2214
2286
  comment += ` reg[${dst}] = new reg[${resolvedOperands[1]}](${resolvedOperands[2]} args)`;
@@ -2222,6 +2294,13 @@ class Serializer {
2222
2294
  case this.OP.BUILD_OBJECT:
2223
2295
  comment += ` reg[${dst}] = {${resolvedOperands[1]} pairs}`;
2224
2296
  break;
2297
+ case this.OP.GET_PROP:
2298
+ comment += ` reg[${dst}] = reg[${resolvedOperands[1]}][reg[${resolvedOperands[2]}]]`;
2299
+ break;
2300
+ case this.OP.SET_PROP:
2301
+ comment += ` reg[${resolvedOperands[0]}][reg[${resolvedOperands[1]}]] = reg[${resolvedOperands[2]}]`;
2302
+ break;
2303
+
2225
2304
  default:
2226
2305
  comment +=
2227
2306
  resolvedOperands.length === 1
@@ -2266,7 +2345,6 @@ class Serializer {
2266
2345
  const originalName = compiler.OP_NAME[specializedOpInfo.originalOp];
2267
2346
  compiler.OP_NAME[instr[0]] =
2268
2347
  `${originalName}_${resolvedValues.join("_")}`;
2269
- specializedOpInfo.resolvedOperands = operands;
2270
2348
  }
2271
2349
 
2272
2350
  serialized.push(instr);
@@ -2288,18 +2366,9 @@ class Serializer {
2288
2366
  const mainRegCount = compiler.mainRegCount;
2289
2367
  let sections = [];
2290
2368
 
2291
- var textForm = [];
2292
2369
  var initBody = [];
2293
-
2294
2370
  var bytecodeResult = this._serializeBytecode(bytecode, compiler);
2295
2371
 
2296
- for (const instr of bytecodeResult.bytecode) {
2297
- const serialized = this._serializeInstr(instr, constants);
2298
- textForm.push(serialized.text);
2299
- }
2300
-
2301
- initBody.push(textForm.map((line) => `// ${line}`).join("\n"));
2302
-
2303
2372
  const flat = bytecodeResult.bytecode.flatMap((instr) => {
2304
2373
  let filtered = instr.filter((x) => (x as any)?.placeholder !== true);
2305
2374
  let resolved = filtered.map((x) => (x as any)?.resolvedValue ?? x);
@@ -2348,6 +2417,10 @@ export async function compileAndSerialize(
2348
2417
  passes.push(specializedOpcodes);
2349
2418
  }
2350
2419
 
2420
+ if (options.microOpcodes) {
2421
+ passes.push(microOpcodes);
2422
+ }
2423
+
2351
2424
  if (options.macroOpcodes) {
2352
2425
  passes.push(macroOpcodes);
2353
2426
  }
@@ -2384,11 +2457,25 @@ export async function compileAndSerialize(
2384
2457
  compiler,
2385
2458
  );
2386
2459
 
2460
+ // This part was purposefully pulled out Serializer as OP_NAME's get resolved during obfuscateRuntime
2461
+ // So for the most useful comments, it's ran absolutely last
2462
+ // Tests also rely on correct comments so it's required
2463
+ const generateBytecodeComment = () => {
2464
+ var lines = [];
2465
+ for (const instr of bytecode) {
2466
+ const serialized = compiler.serializer._serializeInstr(instr);
2467
+ lines.push("// " + serialized.text);
2468
+ }
2469
+
2470
+ return lines.join("\n");
2471
+ };
2472
+
2387
2473
  const code = await obfuscateRuntime(
2388
2474
  runtimeSource,
2389
2475
  bytecode,
2390
2476
  options,
2391
2477
  compiler,
2478
+ generateBytecodeComment,
2392
2479
  );
2393
2480
 
2394
2481
  return { code };
package/src/options.ts CHANGED
@@ -6,6 +6,7 @@ export interface Options {
6
6
  encodeBytecode?: boolean; // encode bytecode? when off, comments for instructions are added
7
7
  selfModifying?: boolean; // do self-modifying bytecode for function bodies?
8
8
  macroOpcodes?: boolean; // create combined opcodes for repeated instruction sequences?
9
+ microOpcodes?: boolean; // break opcodes into sub-opcodes?
9
10
  specializedOpcodes?: boolean; // create specialized opcodes for commonly used opcode+operand pairs?
10
11
  aliasedOpcodes?: boolean; // create duplicate opcodes for commonly used opcodes?
11
12
  timingChecks?: boolean; // add timing checks to detect debuggers?