js-confuser-vm 0.0.4 → 0.0.5

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.
@@ -0,0 +1,102 @@
1
+ import * as t from "@babel/types";
2
+ import traverseImport from "@babel/traverse";
3
+ import { ok } from "assert";
4
+ const traverse = traverseImport.default || traverseImport;
5
+
6
+ // Extract the real statement list from a SwitchCase consequent (identical to the
7
+ // helper used by applyMacroOpcodes so the two files stay in sync).
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
+ // Inline a fixed numeric operand in place of every `this._operand()` call.
19
+ // Because specialized opcodes are only created for instructions that have
20
+ // *exactly one* numeric operand, every `_operand()` call inside the original
21
+ // handler is replaced by the constant value that was baked into the opcode.
22
+ function inlineFixedOperand(bodyStmts, resolvedValue) {
23
+ // Wrap the statements in a temporary BlockStatement so traverse has a root.
24
+ // The replacement mutates the original statement objects in place.
25
+ traverse(t.blockStatement(bodyStmts), {
26
+ noScope: true,
27
+ CallExpression(path) {
28
+ const callee = path.node.callee;
29
+ if (t.isMemberExpression(callee) && t.isThisExpression(callee.object) && t.isIdentifier(callee.property, {
30
+ name: "_operand"
31
+ }) && path.node.arguments.length === 0) {
32
+ path.replaceWith(t.numericLiteral(resolvedValue));
33
+ }
34
+ }
35
+ });
36
+ }
37
+
38
+ // Append a generated switch case for every entry in compiler.SPECIALIZED_OPS.
39
+ // Each case is a clone of the original opcode’s handler with `this._operand()`
40
+ // replaced by the constant integer that was captured at compile time.
41
+ // Must be called AFTER applyMacroOpcodes (so the original cases exist) but
42
+ // BEFORE applyShuffleOpcodes so the new specialized cases also get shuffled.
43
+ export function applySpecializedOpcodes(ast, bytecode, compiler) {
44
+ let switchStatement = null;
45
+ traverse(ast, {
46
+ SwitchStatement(path) {
47
+ if (path.node.leadingComments?.some(c => c.value.includes("@SWITCH"))) {
48
+ switchStatement = path.node;
49
+ path.stop();
50
+ }
51
+ }
52
+ });
53
+ ok(switchStatement, "Could not find @SWITCH statement for specialized opcodes");
54
+
55
+ // Build a map opName → SwitchCase from the existing OP.xxx case tests.
56
+ const nameToCaseMap = new Map();
57
+ for (const sc of switchStatement.cases) {
58
+ const test = sc.test;
59
+ if (test && t.isMemberExpression(test) && t.isIdentifier(test.object, {
60
+ name: "OP"
61
+ }) && t.isIdentifier(test.property)) {
62
+ nameToCaseMap.set(test.property.name, sc);
63
+ }
64
+ }
65
+ if (!compiler.SPECIALIZED_OPS) return;
66
+ for (const [specialOpStr, info] of Object.entries(compiler.SPECIALIZED_OPS)) {
67
+ const specialOpCode = Number(specialOpStr);
68
+ const {
69
+ originalOp,
70
+ operand
71
+ } = info;
72
+ const newName = compiler.OP_NAME[specialOpCode];
73
+ const originalName = compiler.OP_NAME[originalOp];
74
+ if (!originalName) continue;
75
+ const originalCase = nameToCaseMap.get(originalName);
76
+ if (!originalCase) continue;
77
+
78
+ // Clone the original handler body
79
+ const bodyStmts = extractCaseBody(originalCase).map(s => t.cloneNode(s, true));
80
+ const placedOperand = info.resolvedOperand || {
81
+ resolvedValue: 1337
82
+ };
83
+ ok(placedOperand !== undefined, `Could not find operand for original opcode ${newName}`);
84
+ const resolvedValue = placedOperand?.resolvedValue ?? placedOperand;
85
+ if (typeof resolvedValue !== "number") {
86
+ console.error(resolvedValue);
87
+ }
88
+ ok(typeof resolvedValue === "number", "Expected a numeric operand value");
89
+
90
+ // Replace this._operand() with the baked-in constant
91
+ inlineFixedOperand(bodyStmts, resolvedValue);
92
+
93
+ // Add a leading comment so the generated source stays readable
94
+ if (bodyStmts.length > 0) {
95
+ t.addComment(bodyStmts[0], "leading", ` ${compiler.OP_NAME[specialOpCode]} (specialized)`, true);
96
+ }
97
+ bodyStmts.push(t.breakStatement());
98
+
99
+ // Insert the new specialized case into the big switch
100
+ switchStatement.cases.push(t.switchCase(t.numericLiteral(specialOpCode), [t.blockStatement(bodyStmts)]));
101
+ }
102
+ }
@@ -0,0 +1,25 @@
1
+ import { getRandomInt } from "./random-utils.js";
2
+ export const U16_MAX = 0xffff; // bytecode operands are u16
3
+
4
+ /** Returns the next free opcode slot, or -1 when the space is exhausted. */
5
+ export function nextFreeSlot(usedOpcodes) {
6
+ 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;
13
+ }
14
+ }
15
+ // Fallback: linear scan from a random start
16
+ const start = getRandomInt(0, U16_MAX);
17
+ for (let i = 0; i <= U16_MAX; i++) {
18
+ const v = start + i & U16_MAX;
19
+ if (!usedOpcodes.has(v)) {
20
+ usedOpcodes.add(v);
21
+ return v;
22
+ }
23
+ }
24
+ return -1;
25
+ }
@@ -0,0 +1,27 @@
1
+ import { ok } from "assert";
2
+ export function getPlaceholder() {
3
+ return Math.random().toString(36).substring(2, 15);
4
+ }
5
+ export function choice(elements) {
6
+ ok(elements.length > 0, "choice() called on empty sequence");
7
+ return elements[Math.floor(Math.random() * elements.length)];
8
+ }
9
+ export function getRandom() {
10
+ return Math.random();
11
+ }
12
+ export function getRandomInt(min, max) {
13
+ ok(min <= max, "min must be <= max");
14
+ return Math.floor(Math.random() * (max - min + 1)) + min;
15
+ }
16
+
17
+ /**
18
+ * Shuffles an array in-place using the Fisher-Yates algorithm.
19
+ * @param array - The array to shuffle (mutated)
20
+ */
21
+ export function shuffle(array) {
22
+ for (let i = array.length - 1; i > 0; i--) {
23
+ const j = Math.floor(Math.random() * (i + 1));
24
+ [array[i], array[j]] = [array[j], array[i]];
25
+ }
26
+ return array;
27
+ }
package/dist/types.js ADDED
@@ -0,0 +1,15 @@
1
+ // Bytecode supports both real instructions and IR pseudo-instructions
2
+ // Real instruction: [OP.ADD, 5] or multi-operand: [OP.MAKE_CLOSURE, labelRef, 2, 3, 0]
3
+ // IR instruction: [null, { type: "defineLabel", label: "FN_ENTRY_1" }]
4
+
5
+ // IR instructions are used to hold symbolic information during compilation
6
+ // All "null" instructions are dropped before assembly time.
7
+ // Instructions may carry any number of operands; the flat output serializes
8
+ // each operand as a separate u16 slot in the bytecode array.
9
+
10
+ export function constantOperand(value) {
11
+ return {
12
+ type: "constant",
13
+ value: value
14
+ };
15
+ }
package/dist/utilts.js ADDED
@@ -0,0 +1,3 @@
1
+ export function escapeRegex(s) {
2
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3
+ }
package/package.json CHANGED
@@ -1,15 +1,14 @@
1
1
  {
2
2
  "name": "js-confuser-vm",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
- "build": "babel src --out-dir dist --extensions '.ts,.js'",
6
+ "build": "babel src --out-dir dist --extensions \".ts,.js\"",
7
7
  "index": "cross-env NODE_OPTIONS=\"--disable-warning=ExperimentalWarning\" node index.ts",
8
8
  "test": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --experimental-strip-types --disable-warning=ExperimentalWarning\" jest --coverage --coverageReporters=html --selectProjects default",
9
9
  "test-all": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --experimental-strip-types --disable-warning=ExperimentalWarning\" jest --coverage --coverageReporters=html",
10
10
  "test262": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --experimental-strip-types --disable-warning=ExperimentalWarning\" node test262-scripts/run-test262.ts",
11
- "prepublishOnly": "npm run build && npm run test",
12
- "set-env": ""
11
+ "prepublishOnly": "npm run build && npm run test"
13
12
  },
14
13
  "type": "module",
15
14
  "keywords": [