js-confuser 1.5.9 → 1.7.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.
Files changed (143) hide show
  1. package/.github/workflows/node.js.yml +2 -2
  2. package/CHANGELOG.md +55 -0
  3. package/README.md +346 -165
  4. package/dist/constants.js +6 -2
  5. package/dist/index.js +9 -21
  6. package/dist/obfuscator.js +19 -31
  7. package/dist/options.js +5 -5
  8. package/dist/order.js +1 -3
  9. package/dist/presets.js +6 -7
  10. package/dist/probability.js +2 -4
  11. package/dist/templates/bufferToString.js +13 -0
  12. package/dist/templates/crash.js +3 -3
  13. package/dist/templates/es5.js +18 -0
  14. package/dist/templates/functionLength.js +16 -0
  15. package/dist/transforms/calculator.js +77 -21
  16. package/dist/transforms/controlFlowFlattening/controlFlowFlattening.js +980 -367
  17. package/dist/transforms/controlFlowFlattening/expressionObfuscation.js +4 -1
  18. package/dist/transforms/controlFlowFlattening/switchCaseObfuscation.js +25 -26
  19. package/dist/transforms/deadCode.js +33 -25
  20. package/dist/transforms/dispatcher.js +8 -4
  21. package/dist/transforms/es5/antiDestructuring.js +2 -0
  22. package/dist/transforms/es5/es5.js +31 -34
  23. package/dist/transforms/extraction/duplicateLiteralsRemoval.js +92 -58
  24. package/dist/transforms/finalizer.js +82 -0
  25. package/dist/transforms/flatten.js +229 -148
  26. package/dist/transforms/identifier/globalAnalysis.js +88 -0
  27. package/dist/transforms/identifier/globalConcealing.js +10 -83
  28. package/dist/transforms/identifier/movedDeclarations.js +35 -88
  29. package/dist/transforms/identifier/renameVariables.js +124 -59
  30. package/dist/transforms/identifier/variableAnalysis.js +58 -62
  31. package/dist/transforms/lock/lock.js +0 -37
  32. package/dist/transforms/minify.js +60 -57
  33. package/dist/transforms/opaquePredicates.js +1 -1
  34. package/dist/transforms/preparation/preparation.js +2 -2
  35. package/dist/transforms/preparation.js +231 -0
  36. package/dist/transforms/renameLabels.js +1 -1
  37. package/dist/transforms/rgf.js +139 -247
  38. package/dist/transforms/stack.js +128 -26
  39. package/dist/transforms/string/encoding.js +150 -179
  40. package/dist/transforms/string/stringCompression.js +14 -15
  41. package/dist/transforms/string/stringConcealing.js +25 -8
  42. package/dist/transforms/string/stringEncoding.js +13 -24
  43. package/dist/transforms/transform.js +12 -19
  44. package/dist/traverse.js +24 -10
  45. package/dist/util/gen.js +17 -1
  46. package/dist/util/identifiers.js +37 -3
  47. package/dist/util/insert.js +35 -4
  48. package/dist/util/random.js +15 -0
  49. package/docs/ControlFlowFlattening.md +595 -0
  50. package/{Countermeasures.md → docs/Countermeasures.md} +1 -15
  51. package/{Integrity.md → docs/Integrity.md} +2 -2
  52. package/docs/RGF.md +419 -0
  53. package/package.json +5 -5
  54. package/src/constants.ts +3 -0
  55. package/src/index.ts +2 -2
  56. package/src/obfuscator.ts +19 -31
  57. package/src/options.ts +14 -103
  58. package/src/order.ts +1 -5
  59. package/src/presets.ts +6 -7
  60. package/src/probability.ts +2 -3
  61. package/src/templates/bufferToString.ts +68 -0
  62. package/src/templates/crash.ts +15 -19
  63. package/src/templates/es5.ts +131 -0
  64. package/src/templates/functionLength.ts +14 -0
  65. package/src/transforms/calculator.ts +122 -59
  66. package/src/transforms/controlFlowFlattening/controlFlowFlattening.ts +1583 -571
  67. package/src/transforms/controlFlowFlattening/expressionObfuscation.ts +4 -1
  68. package/src/transforms/deadCode.ts +383 -26
  69. package/src/transforms/dispatcher.ts +9 -4
  70. package/src/transforms/es5/antiDestructuring.ts +2 -0
  71. package/src/transforms/es5/es5.ts +32 -77
  72. package/src/transforms/extraction/duplicateLiteralsRemoval.ts +133 -129
  73. package/src/transforms/{hexadecimalNumbers.ts → finalizer.ts} +29 -13
  74. package/src/transforms/flatten.ts +357 -300
  75. package/src/transforms/identifier/globalAnalysis.ts +85 -0
  76. package/src/transforms/identifier/globalConcealing.ts +14 -103
  77. package/src/transforms/identifier/movedDeclarations.ts +49 -102
  78. package/src/transforms/identifier/renameVariables.ts +149 -78
  79. package/src/transforms/identifier/variableAnalysis.ts +66 -73
  80. package/src/transforms/lock/lock.ts +1 -42
  81. package/src/transforms/minify.ts +91 -75
  82. package/src/transforms/opaquePredicates.ts +2 -2
  83. package/src/transforms/preparation.ts +238 -0
  84. package/src/transforms/renameLabels.ts +2 -2
  85. package/src/transforms/rgf.ts +213 -405
  86. package/src/transforms/stack.ts +156 -36
  87. package/src/transforms/string/encoding.ts +115 -212
  88. package/src/transforms/string/stringCompression.ts +27 -18
  89. package/src/transforms/string/stringConcealing.ts +39 -9
  90. package/src/transforms/string/stringEncoding.ts +18 -18
  91. package/src/transforms/transform.ts +21 -23
  92. package/src/traverse.ts +23 -4
  93. package/src/types.ts +2 -1
  94. package/src/util/gen.ts +28 -3
  95. package/src/util/identifiers.ts +43 -2
  96. package/src/util/insert.ts +38 -3
  97. package/src/util/random.ts +13 -0
  98. package/test/code/Cash.test.ts +1 -1
  99. package/test/code/Dynamic.test.ts +12 -10
  100. package/test/code/ES6.src.js +146 -0
  101. package/test/code/ES6.test.ts +28 -2
  102. package/test/index.test.ts +2 -1
  103. package/test/probability.test.ts +44 -0
  104. package/test/templates/template.test.ts +1 -1
  105. package/test/transforms/antiTooling.test.ts +22 -0
  106. package/test/transforms/calculator.test.ts +40 -0
  107. package/test/transforms/controlFlowFlattening/controlFlowFlattening.test.ts +702 -160
  108. package/test/transforms/controlFlowFlattening/expressionObfuscation.test.ts +173 -0
  109. package/test/transforms/deadCode.test.ts +66 -15
  110. package/test/transforms/dispatcher.test.ts +20 -1
  111. package/test/transforms/es5/antiDestructuring.test.ts +16 -0
  112. package/test/transforms/flatten.test.ts +399 -86
  113. package/test/transforms/identifier/movedDeclarations.test.ts +63 -8
  114. package/test/transforms/identifier/renameVariables.test.ts +119 -0
  115. package/test/transforms/lock/antiDebug.test.ts +2 -2
  116. package/test/transforms/lock/lock.test.ts +1 -48
  117. package/test/transforms/minify.test.ts +104 -0
  118. package/test/transforms/preparation.test.ts +157 -0
  119. package/test/transforms/rgf.test.ts +261 -381
  120. package/test/transforms/stack.test.ts +143 -21
  121. package/test/transforms/string/stringCompression.test.ts +39 -0
  122. package/test/transforms/string/stringConcealing.test.ts +82 -0
  123. package/test/transforms/string/stringEncoding.test.ts +53 -2
  124. package/test/transforms/transform.test.ts +66 -0
  125. package/test/traverse.test.ts +139 -0
  126. package/test/util/identifiers.test.ts +113 -1
  127. package/test/util/insert.test.ts +57 -3
  128. package/src/transforms/controlFlowFlattening/choiceFlowObfuscation.ts +0 -87
  129. package/src/transforms/controlFlowFlattening/controlFlowObfuscation.ts +0 -203
  130. package/src/transforms/controlFlowFlattening/switchCaseObfuscation.ts +0 -130
  131. package/src/transforms/eval.ts +0 -89
  132. package/src/transforms/hideInitializingCode.ts +0 -432
  133. package/src/transforms/identifier/nameRecycling.ts +0 -280
  134. package/src/transforms/label.ts +0 -64
  135. package/src/transforms/preparation/nameConflicts.ts +0 -102
  136. package/src/transforms/preparation/preparation.ts +0 -176
  137. package/test/transforms/controlFlowFlattening/controlFlowObfuscation.test.ts +0 -101
  138. package/test/transforms/controlFlowFlattening/switchCaseObfuscation.test.ts +0 -120
  139. package/test/transforms/eval.test.ts +0 -131
  140. package/test/transforms/hideInitializingCode.test.ts +0 -336
  141. package/test/transforms/identifier/nameRecycling.test.ts +0 -205
  142. package/test/transforms/preparation/nameConflicts.test.ts +0 -52
  143. package/test/transforms/preparation/preparation.test.ts +0 -62
@@ -1,23 +1,24 @@
1
1
  import { ok } from "assert";
2
- import { compileJsSync } from "../../compiler";
3
2
  import { ObfuscateOrder } from "../../order";
4
3
  import { ComputeProbabilityMap } from "../../probability";
5
4
  import Template from "../../templates/template";
6
- import { getBlock, isBlock, walk } from "../../traverse";
5
+ import { isBlock, walk } from "../../traverse";
7
6
  import {
8
7
  ArrayExpression,
9
- ArrayPattern,
10
8
  AssignmentExpression,
9
+ AssignmentPattern,
11
10
  BinaryExpression,
12
11
  BreakStatement,
13
12
  CallExpression,
14
13
  ConditionalExpression,
15
14
  ExpressionStatement,
16
- FunctionDeclaration,
15
+ FunctionExpression,
17
16
  Identifier,
18
17
  IfStatement,
19
18
  LabeledStatement,
20
19
  Literal,
20
+ Location,
21
+ LogicalExpression,
21
22
  MemberExpression,
22
23
  Node,
23
24
  ObjectExpression,
@@ -26,6 +27,7 @@ import {
26
27
  SequenceExpression,
27
28
  SwitchCase,
28
29
  SwitchStatement,
30
+ UnaryExpression,
29
31
  VariableDeclaration,
30
32
  VariableDeclarator,
31
33
  WhileStatement,
@@ -37,25 +39,35 @@ import {
37
39
  import {
38
40
  clone,
39
41
  getBlockBody,
40
- getFunction,
41
- getVarContext,
42
+ isContext,
43
+ isForInitialize,
44
+ isFunction,
42
45
  isVarContext,
43
46
  } from "../../util/insert";
44
- import { choice, getRandomInteger, shuffle } from "../../util/random";
47
+ import { chance, choice, getRandomInteger, shuffle } from "../../util/random";
45
48
  import Transform from "../transform";
46
- import ChoiceFlowObfuscation from "./choiceFlowObfuscation";
47
- import ControlFlowObfuscation from "./controlFlowObfuscation";
48
49
  import ExpressionObfuscation from "./expressionObfuscation";
49
- import SwitchCaseObfuscation from "./switchCaseObfuscation";
50
50
  import { isModuleSource } from "../string/stringConcealing";
51
+ import { reservedIdentifiers } from "../../constants";
52
+ import { isDirective } from "../../util/compare";
51
53
 
52
- var flattenStructures = new Set([
54
+ const flattenStructures = new Set([
53
55
  "IfStatement",
54
56
  "ForStatement",
55
57
  "WhileStatement",
56
58
  "DoWhileStatement",
57
59
  ]);
58
60
 
61
+ /**
62
+ * A chunk represents a small segment of code
63
+ */
64
+ interface Chunk {
65
+ label: string;
66
+ body: Node[];
67
+
68
+ impossible?: boolean;
69
+ }
70
+
59
71
  /**
60
72
  * Breaks functions into DAGs (Directed Acyclic Graphs)
61
73
  *
@@ -70,204 +82,140 @@ var flattenStructures = new Set([
70
82
  * - 3. The while loop continues until the the state variable is the end state.
71
83
  */
72
84
  export default class ControlFlowFlattening extends Transform {
85
+ // in Debug mode, the output is much easier to read
73
86
  isDebug = false;
87
+ flattenControlStructures = true; // Flatten if-statements, for-loops, etc
88
+ addToControlObject = true; // var control = { str1, num1 }
89
+ mangleNumberLiterals = true; // 50 => state + X
90
+ mangleBooleanLiterals = true; // true => state == X
91
+ mangleIdentifiers = true; // console => (state == X ? console : _)
92
+ outlineStatements = true; // Tries to outline entire chunks
93
+ outlineExpressions = true; // Tries to outline expressions found in chunks
94
+ addComplexTest = true; // case s != 49 && s - 10:
95
+ addFakeTest = true; // case 100: case 490: case 510: ...
96
+ addDeadCode = true; // add fakes chunks of code
97
+ addOpaquePredicates = true; // predicate ? REAL : FAKE
98
+ addFlaggedLabels = true; // s=NEXT_STATE,flag=true,break
99
+
100
+ // Limit amount of mangling
101
+ mangledExpressionsMade = 0;
102
+
103
+ // Amount of blocks changed by Control Flow Flattening
104
+ cffCount = 0;
74
105
 
75
106
  constructor(o) {
76
107
  super(o, ObfuscateOrder.ControlFlowFlattening);
77
108
 
78
109
  if (!this.isDebug) {
79
110
  this.before.push(new ExpressionObfuscation(o));
80
-
81
- this.after.push(new ControlFlowObfuscation(o));
82
- this.after.push(new SwitchCaseObfuscation(o));
83
111
  } else {
84
112
  console.warn("Debug mode enabled");
85
113
  }
86
-
87
- // this.after.push(new ChoiceFlowObfuscation(o));
88
114
  }
89
115
 
90
116
  match(object, parents) {
91
117
  return (
92
118
  isBlock(object) &&
93
- (!parents[1] || !flattenStructures.has(parents[1].type)) &&
94
- (!parents[2] || !flattenStructures.has(parents[2].type))
119
+ (!parents[0] || !flattenStructures.has(parents[0].type)) &&
120
+ (!parents[1] || !flattenStructures.has(parents[1].type))
95
121
  );
96
122
  }
97
123
 
98
124
  transform(object, parents) {
99
- return () => {
100
- if (object.body.length < 3) {
101
- return;
102
- }
103
- if (containsLexicallyBoundVariables(object, parents)) {
104
- return;
105
- }
125
+ // Must be at least 3 statements or more
126
+ if (object.body.length < 3) {
127
+ return;
128
+ }
129
+ // No 'let'/'const' allowed (These won't work in Switch cases!)
130
+ if (containsLexicallyBoundVariables(object, parents)) {
131
+ return;
132
+ }
133
+ // Check user's threshold setting
134
+ if (!ComputeProbabilityMap(this.options.controlFlowFlattening, (x) => x)) {
135
+ return;
136
+ }
106
137
 
107
- if (
108
- !ComputeProbabilityMap(this.options.controlFlowFlattening, (x) => x)
109
- ) {
110
- return;
111
- }
138
+ var objectBody = getBlockBody(object.body);
139
+ if (!objectBody.length) {
140
+ return;
141
+ }
112
142
 
113
- var body = getBlockBody(object.body);
114
- if (!body.length) {
115
- return;
116
- }
117
- // First step is to reorder the body
118
- // Fix 1. Bring hoisted functions up to be declared first
143
+ // Purely for naming purposes
144
+ var cffIndex = this.cffCount++;
145
+
146
+ // The controlVar is an object containing:
147
+ // - Strings found in chunks
148
+ // - Numbers found in chunks
149
+ // - Helper functions to adjust the state
150
+ // - Outlined expressions changed into functions
151
+ var controlVar = this.getPlaceholder() + `_c${cffIndex}_CONTROL`;
152
+ var controlProperties: Node[] = [];
153
+ var controlConstantMap = new Map<string | number, { key: string }>();
154
+ var controlGen = this.getGenerator("mangled");
155
+ var controlTestKey = controlGen.generate();
156
+
157
+ // This 'controlVar' can be accessed by child-nodes
158
+ object.$controlVar = controlVar;
159
+ object.$controlConstantMap = controlConstantMap;
160
+ object.$controlProperties = controlProperties;
161
+ object.$controlGen = controlGen;
119
162
 
120
- var functionDeclarations: Set<Node> = new Set();
121
- var fnNames: Set<string> = new Set();
122
- var illegalFnNames = new Set<string>();
163
+ return () => {
164
+ ok(Array.isArray(objectBody));
123
165
 
124
- /**
125
- * The variable names
126
- *
127
- * index -> var name
128
- */
166
+ // The state variable names (and quantity)
129
167
  var stateVars = Array(this.isDebug ? 1 : getRandomInteger(2, 5))
130
168
  .fill(0)
131
- .map(() => this.getPlaceholder());
132
-
133
- body.forEach((stmt, i) => {
134
- if (stmt.type == "FunctionDeclaration") {
135
- functionDeclarations.add(stmt);
136
- var name = stmt.id && stmt.id.name;
137
- fnNames.add(name);
138
- if (stmt.body.type !== "BlockStatement") {
139
- illegalFnNames.add(name);
140
- } else {
141
- walk(stmt, [body, object, ...parents], (o, p) => {
142
- if (
143
- o.type == "ThisExpression" ||
144
- o.type == "SuperExpression" ||
145
- (o.type == "Identifier" &&
146
- (o.name == "arguments" || o.name == "eval"))
147
- ) {
148
- illegalFnNames.add(name);
149
- return "EXIT";
150
- }
151
- });
152
- }
153
- }
154
- });
155
-
156
- walk(object, parents, (o, p) => {
157
- if (o.type == "Identifier" && fnNames.has(o.name)) {
158
- var info = getIdentifierInfo(o, p);
159
- if (!info.spec.isReferenced) {
160
- return;
161
- }
162
-
163
- if (info.spec.isModified) {
164
- fnNames.delete(o.name);
165
- } else if (info.spec.isDefined) {
166
- if (info.isFunctionDeclaration) {
167
- if (!functionDeclarations.has(p[0])) {
168
- fnNames.delete(o.name);
169
- }
170
- } else {
171
- fnNames.delete(o.name);
172
- }
173
- }
174
-
175
- if (!info.spec.isDefined) {
176
- var b = getBlock(o, p);
177
- if (b !== object || !p[0] || p[0].type !== "CallExpression") {
178
- illegalFnNames.add(o.name);
179
- } else {
180
- var isExtractable = false;
181
- if (p[1]) {
182
- if (
183
- p[1].type == "ExpressionStatement" &&
184
- p[1].expression == p[0] &&
185
- p[2] == object.body
186
- ) {
187
- isExtractable = true;
188
- p[1].$callExpression = "ExpressionStatement";
189
- p[1].$fnName = o.name;
190
- } else if (
191
- p[1].type == "VariableDeclarator" &&
192
- p[1].init == p[0] &&
193
- p[2].length === 1 &&
194
- p[4] == object.body
195
- ) {
196
- isExtractable = true;
197
- p[3].$callExpression = "VariableDeclarator";
198
- p[3].$fnName = o.name;
199
- } else if (
200
- p[1].type == "AssignmentExpression" &&
201
- p[1].operator == "=" &&
202
- p[1].right === p[0] &&
203
- p[2] &&
204
- p[2].type == "ExpressionStatement" &&
205
- p[3] == object.body
206
- ) {
207
- isExtractable = true;
208
- p[2].$callExpression = "AssignmentExpression";
209
- p[2].$fnName = o.name;
210
- }
211
- }
212
-
213
- if (!isExtractable) {
214
- illegalFnNames.add(o.name);
215
- }
216
- }
217
- }
218
- }
219
- });
220
-
221
- // redefined function,
222
- if (functionDeclarations.size !== fnNames.size) {
223
- return;
224
- }
225
-
226
- illegalFnNames.forEach((illegal) => {
227
- fnNames.delete(illegal);
228
- });
229
-
230
- var importDeclarations = [];
231
- for (var stmt of body) {
232
- if (stmt.type === "ImportDeclaration") {
233
- importDeclarations.push(stmt);
169
+ .map((_, i) => this.getPlaceholder() + `_c${cffIndex}_S${i}`);
170
+
171
+ // How often should chunks be split up?
172
+ // Percentage between 10% and 90% based on block size
173
+ var splitPercent = Math.max(10, 90 - objectBody.length * 5);
174
+
175
+ // Find functions and import declarations
176
+ var importDeclarations: Node[] = [];
177
+ var functionDeclarationNames = new Set<string>();
178
+ var functionDeclarationValues = new Map<string, Node>();
179
+
180
+ // Find all parent control-nodes
181
+ const allControlNodes = [object];
182
+ parents
183
+ .filter((x) => x.$controlVar)
184
+ .forEach((node) => allControlNodes.push(node));
185
+
186
+ const addControlMapConstant = (literalValue: number | string) => {
187
+ // Choose a random control node to add to
188
+ var controlNode = choice(allControlNodes);
189
+ var selectedControlVar = controlNode.$controlVar;
190
+ var selectedControlConstantMap = controlNode.$controlConstantMap;
191
+ var selectedControlProperties = controlNode.$controlProperties;
192
+
193
+ var key = selectedControlConstantMap.get(literalValue)?.key;
194
+
195
+ // Not found, create
196
+ if (!key) {
197
+ key = controlNode.$controlGen.generate();
198
+ selectedControlConstantMap.set(literalValue, { key: key });
199
+
200
+ selectedControlProperties.push(
201
+ Property(Literal(key), Literal(literalValue), false)
202
+ );
234
203
  }
235
- }
236
204
 
237
- var fraction = 0.9;
238
- if (body.length > 20) {
239
- fraction /= Math.max(1.2, body.length - 18);
240
- }
241
- fraction = Math.min(0.1, fraction);
242
- if (isNaN(fraction) || !isFinite(fraction)) {
243
- fraction = 0.5;
244
- }
205
+ return getControlMember(key, selectedControlVar);
206
+ };
245
207
 
246
- var resultVar = this.getPlaceholder();
247
- var argVar = this.getPlaceholder();
248
- var testVar = this.getPlaceholder();
249
- var stringBankVar = this.getPlaceholder();
250
- var stringBank: { [strValue: string]: string } = Object.create(null);
251
- var stringBankByLabels: { [label: string]: Set<string> } =
252
- Object.create(null);
253
- let stringBankGen = this.getGenerator();
254
-
255
- var needsTestVar = false;
256
- var needsResultAndArgVar = false;
257
- var needsStringBankVar = false;
258
- var fnToLabel: { [fnName: string]: string } = Object.create(null);
259
-
260
- fnNames.forEach((fnName) => {
261
- fnToLabel[fnName] = this.getPlaceholder();
262
- });
208
+ // Helper function to easily make control object accessors
209
+ const getControlMember = (key: string, objectName = controlVar) =>
210
+ MemberExpression(Identifier(objectName), Literal(key), true);
263
211
 
264
- const flattenBody = (
265
- body: Node[],
266
- startingLabel = this.getPlaceholder()
267
- ): { label: string; body: Node[] }[] => {
268
- var chunks = [];
269
- var currentBody = [];
212
+ // This function recursively calls itself to flatten and split up code into 'chunks'
213
+ const flattenBody = (body: Node[], startingLabel: string): Chunk[] => {
214
+ var chunks: Chunk[] = [];
215
+ var currentBody: Node[] = [];
270
216
  var currentLabel = startingLabel;
217
+
218
+ // This function ends the current chunk being created ('currentBody')
271
219
  const finishCurrentChunk = (
272
220
  pointingLabel?: string,
273
221
  newLabel?: string,
@@ -289,36 +237,65 @@ export default class ControlFlowFlattening extends Transform {
289
237
  body: [...currentBody],
290
238
  });
291
239
 
240
+ // Random chance of this chunk being flagged (First label cannot be flagged)
241
+ if (
242
+ !this.isDebug &&
243
+ this.addFlaggedLabels &&
244
+ currentLabel !== startLabel &&
245
+ chance(25)
246
+ ) {
247
+ flaggedLabels[currentLabel] = {
248
+ flagKey: controlGen.generate(),
249
+ flagValue: choice([true, false]),
250
+ };
251
+ }
252
+
292
253
  walk(currentBody, [], (o, p) => {
293
- if (
294
- o.type == "Literal" &&
295
- typeof o.value == "string" &&
296
- !isModuleSource(o, p) &&
297
- !o.regex &&
298
- Math.random() / (Object.keys(stringBank).length / 2 + 1) > 0.5
299
- ) {
300
- needsStringBankVar = true;
301
- if (!stringBankByLabels[currentLabel]) {
302
- stringBankByLabels[currentLabel] = new Set();
254
+ if (o.type === "Literal" && !this.isDebug) {
255
+ // Add strings to the control object
256
+ if (
257
+ this.addToControlObject &&
258
+ typeof o.value === "string" &&
259
+ o.value.length >= 3 &&
260
+ o.value.length <= 100 &&
261
+ !isModuleSource(o, p) &&
262
+ !isDirective(o, p) &&
263
+ !o.regex &&
264
+ chance(
265
+ 50 -
266
+ controlConstantMap.size -
267
+ this.mangledExpressionsMade / 100
268
+ )
269
+ ) {
270
+ return () => {
271
+ this.replaceIdentifierOrLiteral(
272
+ o,
273
+ addControlMapConstant(o.value),
274
+ p
275
+ );
276
+ };
303
277
  }
304
278
 
305
- stringBankByLabels[currentLabel].add(o.value);
306
-
307
- if (typeof stringBank[o.value] === "undefined") {
308
- stringBank[o.value] = stringBankGen.generate();
279
+ // Add numbers to the control object
280
+ if (
281
+ this.addToControlObject &&
282
+ typeof o.value === "number" &&
283
+ Math.floor(o.value) === o.value &&
284
+ Math.abs(o.value) < 100_000 &&
285
+ chance(
286
+ 50 -
287
+ controlConstantMap.size -
288
+ this.mangledExpressionsMade / 100
289
+ )
290
+ ) {
291
+ return () => {
292
+ this.replaceIdentifierOrLiteral(
293
+ o,
294
+ addControlMapConstant(o.value),
295
+ p
296
+ );
297
+ };
309
298
  }
310
-
311
- return () => {
312
- this.replaceIdentifierOrLiteral(
313
- o,
314
- MemberExpression(
315
- Identifier(stringBankVar),
316
- Literal(stringBank[o.value]),
317
- true
318
- ),
319
- p
320
- );
321
- };
322
299
  }
323
300
  });
324
301
 
@@ -326,63 +303,52 @@ export default class ControlFlowFlattening extends Transform {
326
303
  currentBody = [];
327
304
  };
328
305
 
329
- body.forEach((stmt, i) => {
330
- if (
331
- functionDeclarations.has(stmt) ||
332
- stmt.type === "ImportDeclaration"
333
- ) {
334
- return;
335
- }
306
+ if (body !== objectBody) {
307
+ // This code is nested. Move function declarations up
336
308
 
337
- if (stmt.$exit) {
338
- currentBody.push(stmt);
339
- currentBody.push(BreakStatement(switchLabel));
340
- finishCurrentChunk(null, null, false);
341
- return;
309
+ var newBody = [];
310
+ for (var stmt of body) {
311
+ if (stmt.type === "FunctionDeclaration") {
312
+ newBody.unshift(stmt);
313
+ } else {
314
+ newBody.push(stmt);
315
+ }
342
316
  }
343
317
 
344
- if (stmt.$callExpression && fnToLabel[stmt.$fnName]) {
345
- var afterPath = this.getPlaceholder();
346
- var args = [];
347
-
348
- switch (stmt.$callExpression) {
349
- // var a = fn();
350
- case "VariableDeclarator":
351
- args = stmt.declarations[0].init.arguments;
352
- stmt.declarations[0].init = Identifier(resultVar);
353
- break;
354
-
355
- // fn();
356
- case "ExpressionStatement":
357
- args = stmt.expression.arguments;
358
- stmt.expression = Identifier("undefined");
359
- break;
360
-
361
- // a = fn();
362
- case "AssignmentExpression":
363
- args = stmt.expression.right.arguments;
364
- stmt.expression.right = Identifier(resultVar);
365
- break;
366
- }
318
+ body = newBody;
319
+ }
367
320
 
368
- needsResultAndArgVar = true;
321
+ body.forEach((stmt, i) => {
322
+ if (stmt.type === "ImportDeclaration") {
323
+ // The 'importDeclarations' hold statements that are required to be left untouched at the top of the block
324
+ importDeclarations.push(stmt);
325
+ return;
326
+ } else if (stmt.type === "FunctionDeclaration") {
327
+ var functionName = stmt.id.name;
369
328
 
370
- currentBody.push(
371
- ExpressionStatement(
372
- AssignmentExpression(
373
- "=",
374
- Identifier(argVar),
375
- ArrayExpression([
376
- {
377
- type: "StateIdentifier",
378
- label: afterPath,
379
- },
380
- ArrayExpression(args),
381
- ])
329
+ stmt.type = "FunctionExpression";
330
+ stmt.id = null;
331
+
332
+ functionDeclarationNames.add(functionName);
333
+ if (objectBody === body) {
334
+ functionDeclarationValues.set(functionName, stmt);
335
+ return;
336
+ } else {
337
+ currentBody.push(
338
+ ExpressionStatement(
339
+ AssignmentExpression("=", Identifier(functionName), stmt)
382
340
  )
383
- )
384
- );
385
- finishCurrentChunk(fnToLabel[stmt.$fnName], afterPath);
341
+ );
342
+ }
343
+
344
+ return;
345
+ } else if (stmt.directive) {
346
+ if (objectBody === body) {
347
+ importDeclarations.push(stmt);
348
+ } else {
349
+ this.error(new Error("Unimplemented directive support."));
350
+ }
351
+ return;
386
352
  }
387
353
 
388
354
  if (stmt.type == "GotoStatement" && i !== body.length - 1) {
@@ -390,9 +356,13 @@ export default class ControlFlowFlattening extends Transform {
390
356
  return;
391
357
  }
392
358
 
393
- if (stmt.type == "LabeledStatement") {
359
+ // The Preparation transform adds labels to every Control-Flow node
360
+ if (
361
+ this.flattenControlStructures &&
362
+ stmt.type == "LabeledStatement"
363
+ ) {
394
364
  var lbl = stmt.label.name;
395
- var control = stmt.body;
365
+ var control: Node = stmt.body;
396
366
 
397
367
  var isSwitchStatement = control.type === "SwitchStatement";
398
368
 
@@ -404,22 +374,7 @@ export default class ControlFlowFlattening extends Transform {
404
374
  control.body.type == "BlockStatement")
405
375
  ) {
406
376
  if (isSwitchStatement) {
407
- if (
408
- control.cases.length == 0 || // at least 1 case
409
- control.cases.find(
410
- (x) =>
411
- !x.test || // cant be default case
412
- !x.consequent.length || // must have body
413
- x.consequent.findIndex(
414
- (node) => node.type == "BreakStatement"
415
- ) !==
416
- x.consequent.length - 1 || // break statement must be at the end
417
- x.consequent[x.consequent.length - 1].type !== // must end with break statement
418
- "BreakStatement" ||
419
- !x.consequent[x.consequent.length - 1].label || // must be labeled and correct
420
- x.consequent[x.consequent.length - 1].label.name != lbl
421
- )
422
- ) {
377
+ if (control.cases.length == 0) {
423
378
  currentBody.push(stmt);
424
379
  return;
425
380
  }
@@ -435,12 +390,31 @@ export default class ControlFlowFlattening extends Transform {
435
390
  var possible = true;
436
391
  var toReplace = [];
437
392
 
438
- walk(control.body, [], (o, p) => {
393
+ // Find all break; and continue; statements and change them into 'GotoStatement's
394
+ walk(control.body || control.cases, [], (o, p) => {
439
395
  if (
440
- o.type == "BreakStatement" ||
441
- (supportContinueStatement && o.type == "ContinueStatement")
396
+ o.type === "BreakStatement" ||
397
+ o.type === "ContinueStatement"
442
398
  ) {
443
- if (!o.label || o.label.name !== lbl) {
399
+ var allowedLabels = new Set(
400
+ p
401
+ .filter(
402
+ (x) =>
403
+ x.type === "LabeledStatement" &&
404
+ x.body.type === "SwitchStatement"
405
+ )
406
+ .map((x) => x.label.name)
407
+ );
408
+
409
+ var isUnsupportedContinue =
410
+ !supportContinueStatement && o.type === "ContinueStatement";
411
+
412
+ var isInvalidLabel =
413
+ !o.label ||
414
+ (o.label.name !== lbl && !allowedLabels.has(o.label.name));
415
+
416
+ // This seems like the best solution:
417
+ if (isUnsupportedContinue || isInvalidLabel) {
444
418
  possible = false;
445
419
  return "EXIT";
446
420
  }
@@ -465,58 +439,114 @@ export default class ControlFlowFlattening extends Transform {
465
439
  toReplace.forEach((v) => this.replace(v[0], v[1]));
466
440
 
467
441
  if (isSwitchStatement) {
468
- var switchVarName = this.getPlaceholder();
442
+ var switchDiscriminantName = this.getPlaceholder() + "_switchD"; // Stores the value of the discriminant
443
+ var switchTestName = this.getPlaceholder() + "_switchT"; // Set to true when a Switch case is matched
469
444
 
470
445
  currentBody.push(
471
446
  VariableDeclaration(
472
- VariableDeclarator(switchVarName, control.discriminant)
447
+ VariableDeclarator(
448
+ switchDiscriminantName,
449
+ control.discriminant
450
+ )
473
451
  )
474
452
  );
475
453
 
476
- var afterPath = this.getPlaceholder();
477
- finishCurrentChunk();
454
+ currentBody.push(
455
+ VariableDeclaration(
456
+ VariableDeclarator(switchTestName, Literal(false))
457
+ )
458
+ );
459
+
460
+ // case labels are:
461
+ // `${caseLabelPrefix}_test_${index}`
462
+ // `${caseLabelPrefix}_entry_${index}`
463
+ var caseLabelPrefix = this.getPlaceholder();
464
+ var defaultCaseIndex = control.cases.findIndex(
465
+ (x) => x.test === null
466
+ );
467
+
478
468
  control.cases.forEach((switchCase, i) => {
479
- var entryPath = this.getPlaceholder();
469
+ var testPath = caseLabelPrefix + "_test_" + i;
470
+ var entryPath = caseLabelPrefix + "_entry_" + i;
471
+ var nextEntryPath =
472
+ i === control.cases.length - 1 // Last path goes to afterPath
473
+ ? afterPath // Else go to next entry path (fall-through behavior)
474
+ : caseLabelPrefix + "_entry_" + (i + 1);
475
+ var nextTestPath =
476
+ i === control.cases.length - 1
477
+ ? afterPath
478
+ : caseLabelPrefix + "_test_" + (i + 1);
479
+
480
+ finishCurrentChunk(testPath, testPath, i == 0);
481
+
482
+ if (switchCase.test) {
483
+ // Check the case condition and goto statement
484
+ currentBody.push(
485
+ IfStatement(
486
+ BinaryExpression(
487
+ "===",
488
+ Identifier(switchDiscriminantName),
489
+ switchCase.test
490
+ ),
491
+ [
492
+ ExpressionStatement(
493
+ AssignmentExpression(
494
+ "=",
495
+ Identifier(switchTestName),
496
+ Literal(true)
497
+ )
498
+ ),
499
+ {
500
+ type: "GotoStatement",
501
+ label: entryPath,
502
+ },
503
+ ]
504
+ )
505
+ );
506
+ } else {
507
+ // Default case: No test needed.
508
+ }
480
509
 
481
- currentBody.push(
482
- IfStatement(
483
- BinaryExpression(
484
- "===",
485
- Identifier(switchVarName),
486
- switchCase.test
487
- ),
488
- [
489
- {
490
- type: "GotoStatement",
491
- label: entryPath,
492
- },
493
- ]
494
- )
495
- );
510
+ // If default case, on last test, if no case was matched, goto default case
511
+ if (
512
+ i === control.cases.length - 1 &&
513
+ defaultCaseIndex !== -1
514
+ ) {
515
+ currentBody.push(
516
+ IfStatement(
517
+ UnaryExpression("!", Identifier(switchTestName)),
518
+ [
519
+ {
520
+ type: "GotoStatement",
521
+ label:
522
+ caseLabelPrefix + "_entry_" + defaultCaseIndex,
523
+ },
524
+ ]
525
+ )
526
+ );
527
+ }
528
+
529
+ // Jump to next test
530
+ currentBody.push({
531
+ type: "GotoStatement",
532
+ label: nextTestPath,
533
+ });
496
534
 
497
535
  chunks.push(
498
536
  ...flattenBody(
499
537
  [
500
- ...switchCase.consequent.slice(
501
- 0,
502
- switchCase.consequent.length - 1
503
- ),
538
+ ...switchCase.consequent,
504
539
  {
505
540
  type: "GotoStatement",
506
- label: afterPath,
541
+ label: nextEntryPath,
507
542
  },
508
543
  ],
509
544
  entryPath
510
545
  )
511
546
  );
512
-
513
- if (i === control.cases.length - 1) {
514
- } else {
515
- finishCurrentChunk();
516
- }
517
547
  });
518
548
 
519
- finishCurrentChunk(afterPath, afterPath);
549
+ finishCurrentChunk(afterPath, afterPath, false);
520
550
  return;
521
551
  } else if (isLoop) {
522
552
  var isPostTest = control.type == "DoWhileStatement";
@@ -537,18 +567,16 @@ export default class ControlFlowFlattening extends Transform {
537
567
  ExpressionStatement(
538
568
  AssignmentExpression(
539
569
  "=",
540
- Identifier(testVar),
570
+ getControlMember(controlTestKey),
541
571
  control.test || Literal(true)
542
572
  )
543
573
  )
544
574
  );
545
575
 
546
- needsTestVar = true;
547
-
548
576
  finishCurrentChunk();
549
577
 
550
578
  currentBody.push(
551
- IfStatement(Identifier(testVar), [
579
+ IfStatement(getControlMember(controlTestKey), [
552
580
  {
553
581
  type: "GotoStatement",
554
582
  label: bodyPath,
@@ -586,6 +614,7 @@ export default class ControlFlowFlattening extends Transform {
586
614
  }
587
615
 
588
616
  if (
617
+ this.flattenControlStructures &&
589
618
  stmt.type == "IfStatement" &&
590
619
  stmt.consequent.type == "BlockStatement" &&
591
620
  (!stmt.alternate || stmt.alternate.type == "BlockStatement")
@@ -594,12 +623,14 @@ export default class ControlFlowFlattening extends Transform {
594
623
 
595
624
  currentBody.push(
596
625
  ExpressionStatement(
597
- AssignmentExpression("=", Identifier(testVar), stmt.test)
626
+ AssignmentExpression(
627
+ "=",
628
+ getControlMember(controlTestKey),
629
+ stmt.test
630
+ )
598
631
  )
599
632
  );
600
633
 
601
- needsTestVar = true;
602
-
603
634
  finishCurrentChunk();
604
635
 
605
636
  var hasAlternate = !!stmt.alternate;
@@ -610,7 +641,7 @@ export default class ControlFlowFlattening extends Transform {
610
641
  var afterPath = this.getPlaceholder();
611
642
 
612
643
  currentBody.push(
613
- IfStatement(Identifier(testVar), [
644
+ IfStatement(getControlMember(controlTestKey), [
614
645
  {
615
646
  type: "GotoStatement",
616
647
  label: yesPath,
@@ -653,7 +684,7 @@ export default class ControlFlowFlattening extends Transform {
653
684
  return;
654
685
  }
655
686
 
656
- if (!currentBody.length || Math.random() < fraction) {
687
+ if (!currentBody.length || !chance(splitPercent)) {
657
688
  currentBody.push(stmt);
658
689
  } else {
659
690
  // Start new chunk
@@ -668,105 +699,123 @@ export default class ControlFlowFlattening extends Transform {
668
699
  return chunks;
669
700
  };
670
701
 
671
- var chunks = [];
702
+ /**
703
+ * Executable code segments are broken down into `chunks` typically 1-3 statements each
704
+ *
705
+ * Chunked Code has a special `GotoStatement` node that get processed later on
706
+ * This allows more complex control structures like `IfStatement`s and `ForStatement`s to be converted into basic
707
+ * conditional jumps and flattened in the switch body
708
+ *
709
+ * IfStatement would be converted like this:
710
+ *
711
+ * MAIN:
712
+ * if ( TEST ) {
713
+ * GOTO consequent_label;
714
+ * } else? {
715
+ * GOTO alternate_label;
716
+ * }
717
+ * GOTO NEXT_CHUNK;
718
+ */
719
+ const chunks: Chunk[] = [];
720
+
721
+ // Flagged labels have addition code protecting the control state
722
+ const flaggedLabels: {
723
+ [label: string]: { flagKey: string; flagValue: boolean };
724
+ } = Object.create(null);
672
725
 
673
726
  /**
674
727
  * label: switch(a+b+c){...break label...}
675
728
  */
676
- var switchLabel = this.getPlaceholder();
729
+ const switchLabel = this.getPlaceholder();
677
730
 
678
- functionDeclarations.forEach((node) => {
679
- if (node.id && fnNames.has(node.id.name)) {
680
- var exitStateName = this.getPlaceholder();
681
- var argumentsName = this.getPlaceholder();
731
+ const startLabel = this.getPlaceholder();
682
732
 
683
- needsResultAndArgVar = true;
733
+ chunks.push(...flattenBody(objectBody, startLabel));
734
+ chunks[chunks.length - 1].body.push({
735
+ type: "GotoStatement",
736
+ label: "END_LABEL",
737
+ });
738
+ chunks.push({
739
+ label: "END_LABEL",
740
+ body: [],
741
+ });
684
742
 
685
- node.body.body.push(ReturnStatement());
743
+ const endLabel = chunks[Object.keys(chunks).length - 1].label;
744
+
745
+ if (!this.isDebug && this.addDeadCode) {
746
+ // DEAD CODE 1/3: Add fake chunks that are never reached
747
+ var fakeChunkCount = getRandomInteger(1, 5);
748
+ for (var i = 0; i < fakeChunkCount; i++) {
749
+ // These chunks just jump somewhere random, they are never executed
750
+ // so it could contain any code
751
+ var fakeChunkBody = [
752
+ // This a fake assignment expression
753
+ ExpressionStatement(
754
+ AssignmentExpression(
755
+ "=",
756
+ Identifier(choice(stateVars)),
757
+ Literal(getRandomInteger(-150, 150))
758
+ )
759
+ ),
686
760
 
687
- walk(node.body, [], (o, p) => {
688
- if (o.type == "ReturnStatement") {
689
- if (!getFunction(o, p)) {
690
- return () => {
691
- var exitExpr = SequenceExpression([
692
- AssignmentExpression(
693
- "=",
694
- ArrayPattern(stateVars.map(Identifier)),
695
- Identifier(exitStateName)
696
- ),
697
- AssignmentExpression(
698
- "=",
699
- Identifier(resultVar),
700
- o.argument || Identifier("undefined")
701
- ),
702
- ]);
761
+ {
762
+ type: "GotoStatement",
763
+ label: choice(chunks).label,
764
+ },
765
+ ];
703
766
 
704
- this.replace(o, ReturnStatement(exitExpr));
705
- };
706
- }
707
- }
767
+ chunks.push({
768
+ label: this.getPlaceholder(),
769
+ body: fakeChunkBody,
770
+ impossible: true,
708
771
  });
772
+ }
709
773
 
710
- var declarations = [
711
- VariableDeclarator(
712
- ArrayPattern([
713
- Identifier(exitStateName),
714
- Identifier(argumentsName),
715
- ]),
716
- Identifier(argVar)
717
- ),
718
- ];
774
+ // DEAD CODE 2/3: Add fake jumps to really mess with deobfuscators
775
+ chunks.forEach((chunk) => {
776
+ if (chance(25)) {
777
+ var randomLabel = choice(chunks).label;
719
778
 
720
- if (node.params.length) {
721
- declarations.push(
722
- VariableDeclarator(
723
- ArrayPattern(node.params),
724
- Identifier(argumentsName)
725
- )
779
+ // The `false` literal will be mangled
780
+ chunk.body.unshift(
781
+ IfStatement(Literal(false), [
782
+ {
783
+ type: "GotoStatement",
784
+ label: randomLabel,
785
+ impossible: true,
786
+ },
787
+ ])
726
788
  );
727
789
  }
790
+ });
728
791
 
729
- var innerName = this.getPlaceholder();
792
+ // DEAD CODE 3/3: Clone chunks but these chunks are never ran
793
+ var cloneChunkCount = getRandomInteger(1, 5);
794
+ for (var i = 0; i < cloneChunkCount; i++) {
795
+ var randomChunk = choice(chunks);
796
+ var clonedChunk = {
797
+ body: clone(randomChunk.body),
798
+ label: this.getPlaceholder(),
799
+ impossible: true,
800
+ };
801
+
802
+ // Don't double define functions
803
+ var hasDeclaration = clonedChunk.body.find((stmt) => {
804
+ return (
805
+ stmt.type === "FunctionDeclaration" ||
806
+ stmt.type === "ClassDeclaration"
807
+ );
808
+ });
730
809
 
731
- chunks.push(
732
- ...flattenBody(
733
- [
734
- FunctionDeclaration(
735
- innerName,
736
- [],
737
- [VariableDeclaration(declarations), ...node.body.body]
738
- ),
739
- this.objectAssign(
740
- ExpressionStatement(
741
- CallExpression(Identifier(innerName), [])
742
- ),
743
- {
744
- $exit: true,
745
- } as any
746
- ),
747
- ],
748
- fnToLabel[node.id.name]
749
- )
750
- );
810
+ if (!hasDeclaration) {
811
+ chunks.unshift(clonedChunk);
812
+ }
751
813
  }
752
- });
753
-
754
- var startLabel = this.getPlaceholder();
755
-
756
- chunks.push(...flattenBody(body, startLabel));
757
- chunks[chunks.length - 1].body.push({
758
- type: "GotoStatement",
759
- label: "END_LABEL",
760
- });
761
- chunks.push({
762
- label: "END_LABEL",
763
- body: [],
764
- });
814
+ }
765
815
 
816
+ // Generate a unique 'state' number for each chunk
766
817
  var caseSelection: Set<number> = new Set();
767
-
768
818
  var uniqueStatesNeeded = chunks.length;
769
- var endLabel = chunks[Object.keys(chunks).length - 1].label;
770
819
 
771
820
  do {
772
821
  var newState = getRandomInteger(1, chunks.length * 15);
@@ -794,12 +843,18 @@ export default class ControlFlowFlattening extends Transform {
794
843
  */
795
844
  var labelToStates: { [label: string]: number[] } = Object.create(null);
796
845
 
846
+ var lastLabel;
847
+
797
848
  Object.values(chunks).forEach((chunk, i) => {
798
849
  var state = caseStates[i];
799
850
 
800
851
  var stateValues = Array(stateVars.length)
801
852
  .fill(0)
802
- .map(() => getRandomInteger(-250, 250));
853
+ .map((_, i) =>
854
+ lastLabel && chance(95) // Try to make state changes not as drastic (If last label, re-use some of it's values)
855
+ ? labelToStates[lastLabel][i]
856
+ : getRandomInteger(-500, 500)
857
+ );
803
858
 
804
859
  const getCurrentState = () => {
805
860
  return stateValues.reduce((a, b) => b + a, 0);
@@ -810,82 +865,600 @@ export default class ControlFlowFlattening extends Transform {
810
865
  state - (getCurrentState() - stateValues[correctIndex]);
811
866
 
812
867
  labelToStates[chunk.label] = stateValues;
868
+ lastLabel = chunk.label;
813
869
  });
814
870
 
815
- // console.log(labelToStates);
816
-
817
871
  var initStateValues = [...labelToStates[startLabel]];
818
872
  var endState = labelToStates[endLabel].reduce((a, b) => b + a, 0);
819
873
 
820
- const numberLiteral = (num, depth, stateValues) => {
821
- ok(Array.isArray(stateValues));
822
- if (depth > 10 || Math.random() > 0.8 / (depth * 4)) {
823
- return Literal(num);
824
- }
874
+ // Creates a predicate based on the state-variables and control-object properties
875
+ const createPredicate = (
876
+ stateValues: number[]
877
+ ): { test: Node; testValue: boolean } => {
878
+ this.mangledExpressionsMade++;
825
879
 
826
- var opposing = getRandomInteger(0, stateVars.length);
880
+ var index = getRandomInteger(0, stateVars.length);
827
881
 
828
- if (Math.random() > 0.5) {
829
- var x = getRandomInteger(-250, 250);
830
- var operator = choice(["<", ">"]);
831
- var answer =
832
- operator == "<"
833
- ? x < stateValues[opposing]
834
- : x > stateValues[opposing];
835
- var correct = numberLiteral(num, depth + 1, stateValues);
836
- var incorrect = numberLiteral(
837
- getRandomInteger(-250, 250),
838
- depth + 1,
839
- stateValues
840
- );
882
+ var compareValue = choice([
883
+ stateValues[index],
884
+ getRandomInteger(-100, 100),
885
+ ]);
841
886
 
842
- return ConditionalExpression(
843
- BinaryExpression(
844
- operator,
845
- numberLiteral(x, depth + 1, stateValues),
846
- Identifier(stateVars[opposing])
847
- ),
848
- answer ? correct : incorrect,
849
- answer ? incorrect : correct
887
+ // 'state equality' test
888
+ var test: Node = BinaryExpression(
889
+ "==",
890
+ Identifier(stateVars[index]),
891
+ createStateBoundNumberLiteral(compareValue, stateValues)
892
+ );
893
+ var testValue = stateValues[index] === compareValue;
894
+
895
+ // 'control' equality test
896
+ if (controlConstantMap.size && chance(50)) {
897
+ // The controlMap maps LITERAL-values to STRING property names
898
+ var actualValue = choice(Array.from(controlConstantMap.keys()));
899
+ var controlKey = controlConstantMap.get(actualValue)?.key;
900
+
901
+ var controlCompareValue = choice([
902
+ actualValue,
903
+ stateValues[index],
904
+ getRandomInteger(-100, 100),
905
+ controlGen.generate(),
906
+ ]);
907
+
908
+ // 'control equality' test
909
+ test = BinaryExpression(
910
+ "==",
911
+ getControlMember(controlKey),
912
+ Literal(controlCompareValue)
850
913
  );
914
+ testValue = actualValue == controlCompareValue;
915
+
916
+ // 'control typeof' test
917
+ if (chance(10)) {
918
+ var compareTypeofValue = choice([
919
+ "number",
920
+ "string",
921
+ "object",
922
+ "function",
923
+ "undefined",
924
+ ]);
925
+
926
+ test = BinaryExpression(
927
+ "==",
928
+ UnaryExpression("typeof", getControlMember(controlKey)),
929
+ Literal(compareTypeofValue)
930
+ );
931
+ testValue = typeof actualValue === compareTypeofValue;
932
+ }
933
+
934
+ // 'control hasOwnProperty' test
935
+ if (chance(10)) {
936
+ var hasOwnProperty = choice([controlKey, controlGen.generate()]);
937
+ test = CallExpression(
938
+ MemberExpression(
939
+ Identifier(controlVar),
940
+ Literal("hasOwnProperty"),
941
+ true
942
+ ),
943
+ [Literal(hasOwnProperty)]
944
+ );
945
+ testValue = hasOwnProperty === controlKey;
946
+ }
851
947
  }
852
948
 
853
- return BinaryExpression(
854
- "+",
855
- Identifier(stateVars[opposing]),
856
- numberLiteral(num - stateValues[opposing], depth + 1, stateValues)
857
- );
949
+ return { test, testValue };
858
950
  };
859
951
 
860
- const createTransitionExpression = (
861
- index: number,
862
- add: number,
863
- mutatingStateValues: number[]
864
- ) => {
865
- var newValue = mutatingStateValues[index] + add;
866
-
867
- var expr = null;
952
+ // A "state-less" number literal is a Number Literal that is mangled in with the Control properties.
953
+ // Example: X = CONTROL.Y + Z. These can be used anywhere because control properties are constant (unlike state variables)
954
+ const createStatelessNumberLiteral = (num: number, depth = 0) => {
955
+ if (
956
+ !controlConstantMap.size ||
957
+ depth > 4 ||
958
+ chance(75 + depth * 5 + this.mangledExpressionsMade / 25)
959
+ ) {
960
+ // Add to control constant map?
961
+ if (
962
+ chance(
963
+ 25 - controlConstantMap.size - this.mangledExpressionsMade / 100
964
+ )
965
+ ) {
966
+ return addControlMapConstant(num);
967
+ }
968
+ return Literal(num);
969
+ }
970
+ this.mangledExpressionsMade++;
971
+
972
+ if (controlConstantMap.has(num)) {
973
+ return getControlMember(controlConstantMap.get(num)?.key);
974
+ }
975
+
976
+ var allControlNodes = [object];
977
+ parents
978
+ .filter((x) => x.$controlVar && x.$controlConstantMap.size > 0)
979
+ .forEach((node) => allControlNodes.push(node));
980
+
981
+ var controlNode = choice(allControlNodes);
982
+ var selectedControlConstantMap = controlNode.$controlConstantMap;
983
+ var selectedControlVar = controlNode.$controlVar;
984
+
985
+ var actualValue = choice(Array.from(selectedControlConstantMap.keys()));
986
+ var controlKey = selectedControlConstantMap.get(actualValue)?.key;
987
+
988
+ if (typeof actualValue === "number") {
989
+ var difference = actualValue - num;
990
+
991
+ return BinaryExpression(
992
+ "-",
993
+ getControlMember(controlKey, selectedControlVar),
994
+ createStatelessNumberLiteral(difference, depth + 1)
995
+ );
996
+ } else if (typeof actualValue === "string") {
997
+ // 'control string length' test
998
+ var compareValue = choice([
999
+ actualValue.length,
1000
+ getRandomInteger(0, 50),
1001
+ ]);
1002
+
1003
+ var test = BinaryExpression(
1004
+ "==",
1005
+ MemberExpression(
1006
+ getControlMember(controlKey, selectedControlVar),
1007
+ Literal("length"),
1008
+ true
1009
+ ),
1010
+ createStatelessNumberLiteral(compareValue, depth + 1)
1011
+ );
1012
+ var testValue = actualValue.length == compareValue;
1013
+
1014
+ var consequent: Node = createStatelessNumberLiteral(num, depth + 1);
1015
+ var alternate: Node = Literal(getRandomInteger(-100, 100));
1016
+
1017
+ return ConditionalExpression(
1018
+ test,
1019
+ testValue ? consequent : alternate,
1020
+ !testValue ? consequent : alternate
1021
+ );
1022
+ } else {
1023
+ throw new Error("Unknown: " + typeof actualValue);
1024
+ }
1025
+ };
1026
+
1027
+ // A "state-bound" number literal is a Number Literal that is mangled in with the current state variables
1028
+ // Example: X = STATE + Y. This can only be used when the state-values are guaranteed to be known.
1029
+ const createStateBoundNumberLiteral = (
1030
+ num: number,
1031
+ stateValues: number[],
1032
+ depth = 0
1033
+ ): Node => {
1034
+ ok(Array.isArray(stateValues));
1035
+
1036
+ // Base case: After 4 depth, OR random chance
1037
+ if (
1038
+ depth > 4 ||
1039
+ chance(75 + depth * 5 + this.mangledExpressionsMade / 25)
1040
+ ) {
1041
+ // Add this number to the control object?
1042
+ // Add to control constant map?
1043
+ if (chance(25 - controlConstantMap.size)) {
1044
+ return addControlMapConstant(num);
1045
+ }
1046
+
1047
+ return Literal(num);
1048
+ }
1049
+ this.mangledExpressionsMade++;
1050
+
1051
+ if (chance(10)) {
1052
+ return createStatelessNumberLiteral(num, depth + 1);
1053
+ }
1054
+
1055
+ // Terminated predicate
1056
+ if (chance(50)) {
1057
+ var { test, testValue } = createPredicate(stateValues);
1058
+
1059
+ var alternateNode = choice([
1060
+ Literal(getRandomInteger(-100, 100)),
1061
+ Literal(controlGen.generate()),
1062
+ getControlMember(controlGen.generate()),
1063
+ ]);
1064
+
1065
+ return ConditionalExpression(
1066
+ test,
1067
+ testValue ? Literal(num) : alternateNode,
1068
+ !testValue ? Literal(num) : alternateNode
1069
+ );
1070
+ }
1071
+
1072
+ // Recursive predicate
1073
+ var opposing = getRandomInteger(0, stateVars.length);
1074
+
1075
+ if (chance(10)) {
1076
+ // state > compare ? real : fake
1077
+
1078
+ var compareValue: number = choice([
1079
+ stateValues[opposing],
1080
+ getRandomInteger(-150, 150),
1081
+ ]);
1082
+
1083
+ var operator = choice(["<", ">", "==", "!="]);
1084
+ var answer: boolean = {
1085
+ ">": compareValue > stateValues[opposing],
1086
+ "<": compareValue < stateValues[opposing],
1087
+ "==": compareValue === stateValues[opposing],
1088
+ "!=": compareValue !== stateValues[opposing],
1089
+ }[operator];
1090
+
1091
+ var correct = createStateBoundNumberLiteral(
1092
+ num,
1093
+ stateValues,
1094
+ depth + 1
1095
+ );
1096
+ var incorrect = createStateBoundNumberLiteral(
1097
+ getRandomInteger(-150, 150),
1098
+ stateValues,
1099
+ depth + 1
1100
+ );
1101
+
1102
+ return ConditionalExpression(
1103
+ BinaryExpression(
1104
+ operator,
1105
+ createStateBoundNumberLiteral(
1106
+ compareValue,
1107
+ stateValues,
1108
+ depth + 1
1109
+ ),
1110
+ Identifier(stateVars[opposing])
1111
+ ),
1112
+ answer ? correct : incorrect,
1113
+ answer ? incorrect : correct
1114
+ );
1115
+ }
1116
+
1117
+ // state + 10 = <REAL>
1118
+ var difference = num - stateValues[opposing];
1119
+
1120
+ if (difference === 0) {
1121
+ return Identifier(stateVars[opposing]);
1122
+ }
1123
+
1124
+ return BinaryExpression(
1125
+ "+",
1126
+ Identifier(stateVars[opposing]),
1127
+ createStateBoundNumberLiteral(difference, stateValues, depth + 1)
1128
+ );
1129
+ };
1130
+
1131
+ var outlinesCreated = 0;
1132
+
1133
+ const isExpression = (object: Node, parents: Node[]) => {
1134
+ var fnIndex = parents.findIndex((x) => isFunction(x));
1135
+ if (fnIndex != -1) {
1136
+ // This does NOT mutate
1137
+ parents = parents.slice(0, fnIndex);
1138
+ }
1139
+ var assignmentIndex = parents.findIndex(
1140
+ (x) => x.type === "AssignmentExpression"
1141
+ );
1142
+
1143
+ // Left-hand assignment validation
1144
+ if (assignmentIndex != -1) {
1145
+ if (
1146
+ parents[assignmentIndex].left ===
1147
+ (parents[assignmentIndex - 1] || object)
1148
+ ) {
1149
+ return false;
1150
+ }
1151
+ }
1152
+
1153
+ // For in/of left validation
1154
+ var forInOfIndex = parents.findIndex(
1155
+ (x) => x.type === "ForInStatement" || x.type === "ForOfStatement"
1156
+ );
1157
+ if (forInOfIndex != -1) {
1158
+ if (
1159
+ parents[forInOfIndex].left === (parents[forInOfIndex - 1] || object)
1160
+ ) {
1161
+ return false;
1162
+ }
1163
+ }
1164
+
1165
+ // Bound call-expression validation
1166
+ var callExpressionIndex = parents.findIndex(
1167
+ (x) => x.type === "CallExpression"
1168
+ );
1169
+ if (callExpressionIndex != -1) {
1170
+ if (
1171
+ parents[callExpressionIndex].callee ==
1172
+ (parents[callExpressionIndex - 1] || object)
1173
+ ) {
1174
+ var callee = parents[callExpressionIndex].callee;
1175
+
1176
+ // Detected bound call expression. Not supported.
1177
+ if (callee.type === "MemberExpression") {
1178
+ return false;
1179
+ }
1180
+ }
1181
+ }
1182
+
1183
+ // Update-expression validation:
1184
+ var updateExpressionIndex = parents.findIndex(
1185
+ (x) => x.type === "UpdateExpression"
1186
+ );
1187
+ if (updateExpressionIndex !== -1) return false;
1188
+
1189
+ return true;
1190
+ };
1191
+
1192
+ // This function checks if the expression or statements is possible to be outlined
1193
+ const canOutline = (object: Node | Node[], parents: Node[]) => {
1194
+ var isIllegal = false;
1195
+
1196
+ var breakStatements: Location[] = [];
1197
+ var returnStatements: Location[] = [];
1198
+
1199
+ if (!Array.isArray(object) && !isExpression(object, parents)) {
1200
+ return { isIllegal: true, breakStatements: [], returnStatements: [] };
1201
+ }
1202
+
1203
+ walk(object, parents, (o, p) => {
1204
+ if (
1205
+ o.type === "ThisExpression" ||
1206
+ o.type === "MetaProperty" ||
1207
+ o.type === "Super"
1208
+ ) {
1209
+ isIllegal = true;
1210
+ return "EXIT";
1211
+ }
1212
+
1213
+ if (o.type === "BreakStatement") {
1214
+ // This can be safely outlined
1215
+ if (o.label && o.label.name === switchLabel) {
1216
+ breakStatements.push([o, p]);
1217
+ } else {
1218
+ isIllegal = true;
1219
+ return "EXIT";
1220
+ }
1221
+ }
1222
+
1223
+ if (
1224
+ (o.type === "ContinueStatement" ||
1225
+ o.type === "AwaitExpression" ||
1226
+ o.type === "YieldExpression" ||
1227
+ o.type === "ReturnStatement" ||
1228
+ o.type === "VariableDeclaration" ||
1229
+ o.type === "FunctionDeclaration" ||
1230
+ o.type === "ClassDeclaration") &&
1231
+ !p.find((x) => isVarContext(x))
1232
+ ) {
1233
+ // This can be safely outlined
1234
+ if (o.type === "ReturnStatement") {
1235
+ returnStatements.push([o, p]);
1236
+ } else {
1237
+ isIllegal = true;
1238
+ return "EXIT";
1239
+ }
1240
+ }
1241
+
1242
+ if (o.type === "Identifier") {
1243
+ if (o.name === "arguments") {
1244
+ isIllegal = true;
1245
+ return "EXIT";
1246
+ }
1247
+ }
1248
+ });
1249
+
1250
+ return { isIllegal, breakStatements, returnStatements };
1251
+ };
1252
+
1253
+ const createOutlineFunction = (
1254
+ body: Node[],
1255
+ stateValues: number[],
1256
+ label: string
1257
+ ) => {
1258
+ var key = controlGen.generate();
1259
+
1260
+ var functionExpression = FunctionExpression([], body);
1261
+ if (!this.options.es5 && chance(50)) {
1262
+ functionExpression.type = "ArrowFunctionExpression";
1263
+ }
1264
+
1265
+ controlProperties.push(
1266
+ Property(Literal(key), functionExpression, false)
1267
+ );
1268
+
1269
+ // Add dead code to function
1270
+ if (!this.isDebug && chance(25)) {
1271
+ var { test, testValue } = createPredicate(stateValues);
1272
+ var deadCodeVar = this.getPlaceholder();
1273
+ functionExpression.params.push(
1274
+ AssignmentPattern(Identifier(deadCodeVar), test)
1275
+ );
1276
+ var alternate = [
1277
+ ReturnStatement(
1278
+ choice([
1279
+ BinaryExpression(
1280
+ "==",
1281
+ Identifier(choice(stateVars)),
1282
+ Literal(getRandomInteger(-100, 100))
1283
+ ),
1284
+ Literal(controlGen.generate()),
1285
+ Identifier("arguments"),
1286
+ Identifier(choice(stateVars)),
1287
+ Identifier(controlVar),
1288
+ CallExpression(getControlMember(controlGen.generate()), []),
1289
+ ])
1290
+ ),
1291
+ ];
1292
+
1293
+ functionExpression.body.body.unshift(
1294
+ IfStatement(
1295
+ testValue
1296
+ ? UnaryExpression("!", Identifier(deadCodeVar))
1297
+ : Identifier(deadCodeVar),
1298
+ alternate
1299
+ )
1300
+ );
1301
+ }
1302
+
1303
+ outlinesCreated++;
1304
+
1305
+ return key;
1306
+ };
1307
+
1308
+ const attemptOutlineStatements = (
1309
+ statements: Node[],
1310
+ parentBlock: Node[],
1311
+ stateValues: number[],
1312
+ label: string
1313
+ ) => {
1314
+ if (
1315
+ this.isDebug ||
1316
+ !this.outlineStatements ||
1317
+ chance(75 + outlinesCreated - this.mangledExpressionsMade / 25)
1318
+ ) {
1319
+ return;
1320
+ }
1321
+
1322
+ var index = parentBlock.indexOf(statements[0]);
1323
+ if (index === -1) return;
1324
+
1325
+ var outlineInfo = canOutline(statements, parentBlock);
1326
+ if (outlineInfo.isIllegal) return;
1327
+
1328
+ var breakFlag = controlGen.generate();
1329
+
1330
+ outlineInfo.breakStatements.forEach(([breakStatement, p]) => {
1331
+ this.replace(breakStatement, ReturnStatement(Literal(breakFlag)));
1332
+ });
1333
+
1334
+ var returnFlag = controlGen.generate();
1335
+
1336
+ outlineInfo.returnStatements.forEach(([returnStatement, p]) => {
1337
+ var argument = returnStatement.argument || Identifier("undefined");
1338
+
1339
+ this.replace(
1340
+ returnStatement,
1341
+ ReturnStatement(
1342
+ ObjectExpression([Property(Literal(returnFlag), argument, false)])
1343
+ )
1344
+ );
1345
+ });
1346
+
1347
+ // Outline these statements!
1348
+ var key = createOutlineFunction(clone(statements), stateValues, label);
1349
+ var callExpression = CallExpression(getControlMember(key), []);
1350
+
1351
+ var newStatements: Node[] = [];
1352
+ if (
1353
+ outlineInfo.breakStatements.length === 0 &&
1354
+ outlineInfo.returnStatements.length === 0
1355
+ ) {
1356
+ newStatements.push(ExpressionStatement(callExpression));
1357
+ } else if (outlineInfo.returnStatements.length === 0) {
1358
+ newStatements.push(
1359
+ IfStatement(
1360
+ BinaryExpression("==", callExpression, Literal(breakFlag)),
1361
+ [BreakStatement(switchLabel)]
1362
+ )
1363
+ );
1364
+ } else {
1365
+ var tempVar = this.getPlaceholder();
1366
+ newStatements.push(
1367
+ VariableDeclaration(VariableDeclarator(tempVar, callExpression))
1368
+ );
1369
+
1370
+ const t = (str): Node => Template(str).single().expression;
1371
+
1372
+ newStatements.push(
1373
+ IfStatement(
1374
+ t(`${tempVar} === "${breakFlag}"`),
1375
+ [BreakStatement(switchLabel)],
1376
+ [
1377
+ IfStatement(t(`typeof ${tempVar} == "object"`), [
1378
+ ReturnStatement(t(`${tempVar}["${returnFlag}"]`)),
1379
+ ]),
1380
+ ]
1381
+ )
1382
+ );
1383
+ }
1384
+
1385
+ // Remove the original statements from the block and replace it with the call expression
1386
+ parentBlock.splice(index, statements.length, ...newStatements);
1387
+ };
1388
+
1389
+ const attemptOutlineExpression = (
1390
+ expression: Node,
1391
+ expressionParents: Node[],
1392
+ stateValues: number[],
1393
+ label: string
1394
+ ) => {
1395
+ if (
1396
+ this.isDebug ||
1397
+ !this.outlineExpressions ||
1398
+ chance(75 + outlinesCreated - this.mangledExpressionsMade / 25)
1399
+ ) {
1400
+ return;
1401
+ }
1402
+
1403
+ var outlineInfo = canOutline(expression, expressionParents);
1404
+ if (
1405
+ outlineInfo.isIllegal ||
1406
+ outlineInfo.breakStatements.length ||
1407
+ outlineInfo.returnStatements.length
1408
+ )
1409
+ return;
1410
+
1411
+ // Outline this expression!
1412
+ var key = createOutlineFunction(
1413
+ [ReturnStatement(clone(expression))],
1414
+ stateValues,
1415
+ label
1416
+ );
1417
+
1418
+ var callExpression = CallExpression(getControlMember(key), []);
1419
+
1420
+ this.replaceIdentifierOrLiteral(
1421
+ expression,
1422
+ callExpression,
1423
+ expressionParents
1424
+ );
1425
+ };
1426
+
1427
+ const createTransitionExpression = (
1428
+ index: number,
1429
+ add: number,
1430
+ mutatingStateValues: number[],
1431
+ label: string
1432
+ ) => {
1433
+ var beforeStateValues = [...mutatingStateValues];
1434
+ var newValue = mutatingStateValues[index] + add;
1435
+
1436
+ var expr = null;
868
1437
 
869
1438
  if (this.isDebug) {
1439
+ // state = NEW_STATE
870
1440
  expr = AssignmentExpression(
871
1441
  "=",
872
1442
  Identifier(stateVars[index]),
873
1443
  Literal(newValue)
874
1444
  );
875
- } else if (Math.random() > 0.5) {
1445
+ } else if (chance(90)) {
1446
+ // state += (NEW_STATE - CURRENT_STATE)
876
1447
  expr = AssignmentExpression(
877
1448
  "+=",
878
1449
  Identifier(stateVars[index]),
879
- numberLiteral(add, 0, mutatingStateValues)
1450
+ createStateBoundNumberLiteral(add, mutatingStateValues)
880
1451
  );
881
1452
  } else {
1453
+ // state *= 2
1454
+ // state -= DIFFERENCE
882
1455
  var double = mutatingStateValues[index] * 2;
883
1456
  var diff = double - newValue;
884
1457
 
885
1458
  var first = AssignmentExpression(
886
1459
  "*=",
887
1460
  Identifier(stateVars[index]),
888
- numberLiteral(2, 0, mutatingStateValues)
1461
+ createStateBoundNumberLiteral(2, mutatingStateValues)
889
1462
  );
890
1463
  mutatingStateValues[index] = double;
891
1464
 
@@ -894,23 +1467,27 @@ export default class ControlFlowFlattening extends Transform {
894
1467
  AssignmentExpression(
895
1468
  "-=",
896
1469
  Identifier(stateVars[index]),
897
- numberLiteral(diff, 0, mutatingStateValues)
1470
+ createStateBoundNumberLiteral(diff, mutatingStateValues)
898
1471
  ),
899
1472
  ]);
900
1473
  }
901
1474
 
902
1475
  mutatingStateValues[index] = newValue;
903
1476
 
1477
+ // These are lower quality outlines vs. the entire transition outline
1478
+ if (chance(50)) {
1479
+ attemptOutlineExpression(expr, [], [...beforeStateValues], label);
1480
+ }
1481
+
904
1482
  return expr;
905
1483
  };
906
1484
 
907
1485
  interface Case {
908
1486
  state: number;
909
1487
  body: Node[];
910
- order: number;
1488
+ label: string;
911
1489
  }
912
1490
 
913
- var order = Object.create(null);
914
1491
  var cases: Case[] = [];
915
1492
 
916
1493
  chunks.forEach((chunk, i) => {
@@ -921,34 +1498,201 @@ export default class ControlFlowFlattening extends Transform {
921
1498
 
922
1499
  ok(labelToStates[chunk.label]);
923
1500
  var state = caseStates[i];
924
- var made = 1;
925
1501
 
926
- var breaksInsertion = [];
927
1502
  var staticStateValues = [...labelToStates[chunk.label]];
928
1503
  var potentialBranches = new Set<string>();
929
1504
 
930
- chunk.body.forEach((stmt, stmtIndex) => {
931
- var addBreak = false;
1505
+ [...chunk.body].forEach((stmt) => {
932
1506
  walk(stmt, [], (o, p) => {
1507
+ // This mangles certain literals with the state variables
1508
+ // Ex: A number literal (50) changed to a expression (stateVar + 40), when stateVar = 10
933
1509
  if (
934
1510
  !this.isDebug &&
935
- o.type == "Literal" &&
936
- typeof o.value === "number" &&
937
- Math.floor(o.value) === o.value &&
938
- Math.abs(o.value) < 100_000 &&
939
- Math.random() < 4 / made &&
1511
+ o.type === "Literal" &&
940
1512
  !p.find((x) => isVarContext(x))
941
1513
  ) {
942
- made++;
943
- return () => {
944
- this.replaceIdentifierOrLiteral(
945
- o,
946
- numberLiteral(o.value, 0, staticStateValues),
947
- p
1514
+ if (
1515
+ typeof o.value === "number" &&
1516
+ Math.floor(o.value) === o.value && // Only whole numbers
1517
+ Math.abs(o.value) < 100_000 && // Hard-coded limit
1518
+ this.mangleNumberLiterals &&
1519
+ chance(50 - this.mangledExpressionsMade / 100)
1520
+ ) {
1521
+ // 50 -> state1 - 10, when state1 = 60. The result is still 50
1522
+
1523
+ return () => {
1524
+ this.replaceIdentifierOrLiteral(
1525
+ o,
1526
+ createStateBoundNumberLiteral(o.value, staticStateValues),
1527
+ p
1528
+ );
1529
+ };
1530
+ }
1531
+
1532
+ if (
1533
+ typeof o.value === "boolean" &&
1534
+ this.mangleBooleanLiterals &&
1535
+ chance(50 - this.mangledExpressionsMade / 100)
1536
+ ) {
1537
+ // true -> state1 == 10, when state1 = 10. The result is still true
1538
+
1539
+ // Choose a random state var to compare again
1540
+ var index = getRandomInteger(0, stateVars.length);
1541
+
1542
+ var compareValue = staticStateValues[index];
1543
+
1544
+ // When false, always choose a different number, so the expression always equals false
1545
+ while (!o.value && compareValue === staticStateValues[index]) {
1546
+ compareValue = getRandomInteger(-150, 150);
1547
+ }
1548
+
1549
+ var mangledExpression: Node = BinaryExpression(
1550
+ "==",
1551
+ Identifier(stateVars[index]),
1552
+ createStateBoundNumberLiteral(compareValue, staticStateValues)
948
1553
  );
1554
+
1555
+ return () => {
1556
+ this.replaceIdentifierOrLiteral(o, mangledExpression, p);
1557
+
1558
+ attemptOutlineExpression(
1559
+ o,
1560
+ p,
1561
+ staticStateValues,
1562
+ chunk.label
1563
+ );
1564
+ };
1565
+ }
1566
+ }
1567
+
1568
+ // Mangle certain referenced identifiers
1569
+ // console.log("hi") -> (x ? console : window).log("hi"), when is x true. The result is the same
1570
+ if (
1571
+ !this.isDebug &&
1572
+ o.type === "Identifier" &&
1573
+ this.mangleIdentifiers &&
1574
+ chance(50 - this.mangledExpressionsMade / 100) &&
1575
+ !p.find((x) => isVarContext(x))
1576
+ ) {
1577
+ // ONLY referenced identifiers (like actual variable names) can be changed
1578
+ var info = getIdentifierInfo(o, p);
1579
+ if (
1580
+ !info.spec.isReferenced ||
1581
+ info.spec.isDefined ||
1582
+ info.spec.isModified ||
1583
+ info.spec.isExported
1584
+ ) {
1585
+ return;
1586
+ }
1587
+
1588
+ // TYPEOF expression check
1589
+ if (
1590
+ p[0] &&
1591
+ p[0].type === "UnaryExpression" &&
1592
+ p[0].operator === "typeof" &&
1593
+ p[0].argument === o
1594
+ ) {
1595
+ return;
1596
+ }
1597
+
1598
+ // Update expression check
1599
+ if (p[0] && p[0].type === "UpdateExpression") {
1600
+ return;
1601
+ }
1602
+
1603
+ // FOR-in/of initializer check
1604
+ if (isForInitialize(o, p) === "left-hand") {
1605
+ return;
1606
+ }
1607
+
1608
+ var { test, testValue } = createPredicate(staticStateValues);
1609
+
1610
+ // test && real
1611
+ var mangledExpression: Node = LogicalExpression(
1612
+ testValue ? "&&" : "||",
1613
+ test,
1614
+ Identifier(o.name)
1615
+ );
1616
+
1617
+ // control.fake = real
1618
+ if (chance(50)) {
1619
+ mangledExpression = AssignmentExpression(
1620
+ "=",
1621
+ getControlMember(controlGen.generate()),
1622
+ Identifier(o.name)
1623
+ );
1624
+ }
1625
+
1626
+ // test ? real : fake
1627
+ if (chance(50)) {
1628
+ var alternateName = choice([
1629
+ controlVar,
1630
+ ...stateVars,
1631
+ ...this.options.globalVariables,
1632
+ ...reservedIdentifiers,
1633
+ ]);
1634
+
1635
+ // Don't use 'arguments'
1636
+ if (alternateName === "arguments") alternateName = "undefined";
1637
+
1638
+ mangledExpression = ConditionalExpression(
1639
+ test,
1640
+ Identifier(testValue ? o.name : alternateName),
1641
+ Identifier(!testValue ? o.name : alternateName)
1642
+ );
1643
+ }
1644
+
1645
+ return () => {
1646
+ this.replaceIdentifierOrLiteral(o, mangledExpression, p);
949
1647
  };
950
1648
  }
951
1649
 
1650
+ // Function outlining: bring out certain expressions
1651
+ if (
1652
+ !this.isDebug &&
1653
+ o.type &&
1654
+ [
1655
+ "BinaryExpression",
1656
+ "LogicalExpression",
1657
+ "CallExpression",
1658
+ "AssignmentExpression",
1659
+ "MemberExpression",
1660
+ "ObjectExpression",
1661
+ "ConditionalExpression",
1662
+ ].includes(o.type) &&
1663
+ !chance(p.length * 5) && // The further down the tree the lower quality of expression
1664
+ !p.find((x) => isContext(x) || x.$outlining)
1665
+ ) {
1666
+ o.$outlining = true;
1667
+ return () => {
1668
+ attemptOutlineExpression(o, p, staticStateValues, chunk.label);
1669
+ };
1670
+ }
1671
+
1672
+ // Opaque predicates: If Statements, Conditional Statements, Switch Case test
1673
+ if (
1674
+ !this.isDebug &&
1675
+ this.addOpaquePredicates &&
1676
+ p[0] &&
1677
+ chance(50 - outlinesCreated - this.mangledExpressionsMade / 100)
1678
+ ) {
1679
+ var isTestExpression =
1680
+ (p[0].type == "IfStatement" && p[0].test === o) ||
1681
+ (p[0].type === "ConditionalExpression" && p[0].test === o) ||
1682
+ (p[0].type === "SwitchCase" && p[0].test === o);
1683
+
1684
+ if (isTestExpression && !p.find((x) => isContext(x))) {
1685
+ return () => {
1686
+ var { test, testValue } = createPredicate(staticStateValues);
1687
+
1688
+ this.replace(
1689
+ o,
1690
+ LogicalExpression(testValue ? "&&" : "||", test, clone(o))
1691
+ );
1692
+ };
1693
+ }
1694
+ }
1695
+
952
1696
  if (o.type == "StateIdentifier") {
953
1697
  return () => {
954
1698
  ok(labelToStates[o.label]);
@@ -961,172 +1705,440 @@ export default class ControlFlowFlattening extends Transform {
961
1705
 
962
1706
  if (o.type == "GotoStatement") {
963
1707
  return () => {
964
- var blockIndex = p.findIndex((node) => isBlock(node));
1708
+ var blockIndex = p.findIndex(
1709
+ (node) => isBlock(node) || node.type === "SwitchCase"
1710
+ );
965
1711
  if (blockIndex === -1) {
966
- addBreak = true;
967
- } else {
968
- var child = p[blockIndex - 2] || o;
969
- var childIndex = p[blockIndex].body.indexOf(child);
1712
+ var index = chunk.body.indexOf(stmt);
1713
+ ok(index != -1);
970
1714
 
971
- p[blockIndex].body.splice(
972
- childIndex + 1,
973
- 0,
974
- BreakStatement(switchLabel)
975
- );
1715
+ // Top level: Insert break statement in the chunk body
1716
+ // This is OKAY because this forEach uses a cloned version of the body `[...chunk.body]`
1717
+ chunk.body.splice(index + 1, 0, BreakStatement(switchLabel));
1718
+ } else {
1719
+ var block = p[blockIndex];
1720
+
1721
+ if (block.type === "SwitchCase") {
1722
+ // Handle switch case break placement (Important!)
1723
+ block.consequent.splice(
1724
+ block.consequent.indexOf(p[blockIndex - 2] || o) + 1,
1725
+ 0,
1726
+ BreakStatement(switchLabel)
1727
+ );
1728
+ } else {
1729
+ // Standard block placement
1730
+ var child = p[blockIndex - 2] || o;
1731
+ var childIndex = block.body.indexOf(child);
1732
+
1733
+ block.body.splice(
1734
+ childIndex + 1,
1735
+ 0,
1736
+ BreakStatement(switchLabel)
1737
+ );
1738
+ }
976
1739
  }
977
1740
 
978
- potentialBranches.add(o.label);
1741
+ if (!o.impossible) {
1742
+ potentialBranches.add(o.label);
1743
+ }
979
1744
 
980
1745
  var mutatingStateValues = [...labelToStates[chunk.label]];
981
1746
  var nextStateValues = labelToStates[o.label];
982
1747
  ok(nextStateValues, o.label);
983
- this.replace(
984
- o,
985
- ExpressionStatement(
986
- SequenceExpression(
987
- mutatingStateValues.map((_v, stateValueIndex) => {
988
- var diff =
989
- nextStateValues[stateValueIndex] -
990
- mutatingStateValues[stateValueIndex];
991
- return createTransitionExpression(
992
- stateValueIndex,
993
- diff,
994
- mutatingStateValues
995
- );
996
- })
1748
+
1749
+ var transitionExpressions: Node[] = [];
1750
+ for (
1751
+ var stateValueIndex = 0;
1752
+ stateValueIndex < stateVars.length;
1753
+ stateValueIndex++
1754
+ ) {
1755
+ var diff =
1756
+ nextStateValues[stateValueIndex] -
1757
+ mutatingStateValues[stateValueIndex];
1758
+
1759
+ // Only add if state value changed
1760
+ // If pointing to itself then always add to ensure SequenceExpression isn't empty
1761
+ if (diff !== 0 || o.label === chunk.label) {
1762
+ transitionExpressions.push(
1763
+ createTransitionExpression(
1764
+ stateValueIndex,
1765
+ diff,
1766
+ mutatingStateValues,
1767
+ chunk.label
1768
+ )
1769
+ );
1770
+ }
1771
+ }
1772
+
1773
+ ok(transitionExpressions.length !== 0);
1774
+
1775
+ var sequenceExpression = SequenceExpression(
1776
+ transitionExpressions
1777
+ );
1778
+
1779
+ // Check if flagged and additional code here
1780
+ if (typeof flaggedLabels[o.label] === "object") {
1781
+ var { flagKey, flagValue } = flaggedLabels[o.label];
1782
+
1783
+ sequenceExpression.expressions.push(
1784
+ AssignmentExpression(
1785
+ "=",
1786
+ getControlMember(flagKey),
1787
+ Literal(flagValue)
997
1788
  )
998
- )
1789
+ );
1790
+ }
1791
+
1792
+ attemptOutlineExpression(
1793
+ sequenceExpression,
1794
+ [],
1795
+ staticStateValues,
1796
+ chunk.label
999
1797
  );
1798
+
1799
+ this.replace(o, ExpressionStatement(sequenceExpression));
1000
1800
  };
1001
1801
  }
1002
1802
  });
1003
-
1004
- if (addBreak) {
1005
- breaksInsertion.push(stmtIndex);
1006
- }
1007
1803
  });
1008
1804
 
1009
- breaksInsertion.sort();
1010
- breaksInsertion.reverse();
1011
-
1012
- breaksInsertion.forEach((index) => {
1013
- chunk.body.splice(index + 1, 0, BreakStatement(switchLabel));
1014
- });
1805
+ attemptOutlineStatements(
1806
+ chunk.body,
1807
+ chunk.body,
1808
+ staticStateValues,
1809
+ chunk.label
1810
+ );
1015
1811
 
1016
- for (var branch of Array.from(potentialBranches)) {
1017
- var strings = stringBankByLabels[branch];
1018
- if (strings) {
1019
- chunk.body.unshift(
1020
- ExpressionStatement(
1021
- SequenceExpression(
1022
- Array.from(strings).map((strValue) => {
1023
- return AssignmentExpression(
1024
- "=",
1025
- MemberExpression(
1026
- Identifier(stringBankVar),
1027
- Literal(stringBank[strValue]),
1028
- true
1029
- ),
1030
- Literal(strValue)
1031
- );
1032
- })
1033
- )
1034
- )
1035
- );
1036
- }
1812
+ if (!chunk.impossible) {
1813
+ // FUTURE OBFUSCATION IDEA: Update controlObject based on 'potentialBranches' code
1814
+ // This idea would require a lot of work but would make some seriously effective obfuscation
1815
+ // for protecting the data. In 'inactive' states the data could be overwritten to fake values
1816
+ // And in the 'active' state the data would brought back just in time. This would require the controlObject
1817
+ // state to be known in all chunks
1037
1818
  }
1038
1819
 
1039
- // var c = Identifier("undefined");
1040
- // this.addComment(c, stateValues.join(", "));
1041
- // transitionStatements.push(c);
1042
-
1043
- var caseObject = {
1820
+ var caseObject: Case = {
1044
1821
  body: chunk.body,
1045
1822
  state: state,
1046
- order: i,
1823
+ label: chunk.label,
1047
1824
  };
1048
- order[i] = caseObject;
1049
1825
 
1050
1826
  cases.push(caseObject);
1051
1827
  });
1052
1828
 
1829
+ if (!this.isDebug && this.addDeadCode) {
1830
+ // Add fake control object updates
1831
+ chunks.forEach((chunk) => {
1832
+ if (chance(10)) {
1833
+ // These deadCode variants can NOT break the state/control variables
1834
+ // They are executed!
1835
+ var deadCodeChoices = [
1836
+ ExpressionStatement(
1837
+ AssignmentExpression(
1838
+ "=",
1839
+ getControlMember(controlGen.generate()),
1840
+ Literal(controlGen.generate())
1841
+ )
1842
+ ),
1843
+ ExpressionStatement(
1844
+ UnaryExpression(
1845
+ "delete",
1846
+ getControlMember(controlGen.generate())
1847
+ )
1848
+ ),
1849
+ ];
1850
+
1851
+ // These deadCode variants can make breaking changes
1852
+ // because they are never ran
1853
+ if (chunk.impossible) {
1854
+ var randomControlKey =
1855
+ choice(
1856
+ controlProperties
1857
+ .map((prop) => prop.key?.value)
1858
+ .filter((x) => x && typeof x === "string")
1859
+ ) || controlGen.generate();
1860
+
1861
+ deadCodeChoices = deadCodeChoices.concat([
1862
+ ExpressionStatement(
1863
+ AssignmentExpression(
1864
+ "=",
1865
+ Identifier(controlVar),
1866
+ Literal(false)
1867
+ )
1868
+ ),
1869
+ ExpressionStatement(
1870
+ AssignmentExpression(
1871
+ "=",
1872
+ Identifier(controlVar),
1873
+ Identifier("undefined")
1874
+ )
1875
+ ),
1876
+ ExpressionStatement(
1877
+ AssignmentExpression(
1878
+ "=",
1879
+ getControlMember(randomControlKey),
1880
+ Identifier("undefined")
1881
+ )
1882
+ ),
1883
+ ExpressionStatement(
1884
+ UnaryExpression("delete", getControlMember(randomControlKey))
1885
+ ),
1886
+ ]);
1887
+ }
1888
+
1889
+ chunk.body.unshift(choice(deadCodeChoices));
1890
+ }
1891
+ });
1892
+ }
1893
+
1053
1894
  if (!this.isDebug) {
1054
1895
  shuffle(cases);
1896
+ shuffle(controlProperties);
1055
1897
  }
1056
1898
 
1057
1899
  var discriminant = Template(`${stateVars.join("+")}`).single().expression;
1058
1900
 
1059
- body.length = 0;
1901
+ objectBody.length = 0;
1902
+ // Perverse position of import declarations
1060
1903
  for (var importDeclaration of importDeclarations) {
1061
- body.push(importDeclaration);
1904
+ objectBody.push(importDeclaration);
1062
1905
  }
1063
1906
 
1064
- if (functionDeclarations.size) {
1065
- functionDeclarations.forEach((x) => {
1066
- if (!x.id || illegalFnNames.has(x.id.name)) {
1067
- body.unshift(clone(x));
1068
- }
1069
- });
1907
+ // As well as functions are brought up
1908
+ for (var functionName of functionDeclarationNames) {
1909
+ objectBody.push(
1910
+ VariableDeclaration(
1911
+ VariableDeclarator(
1912
+ functionName,
1913
+ functionDeclarationValues.get(functionName)
1914
+ )
1915
+ )
1916
+ );
1070
1917
  }
1071
1918
 
1072
- var switchStatement: Node = SwitchStatement(
1073
- discriminant,
1074
- cases.map((x, i) => {
1075
- var statements = [];
1919
+ var defaultCaseIndex = getRandomInteger(0, cases.length);
1920
+ var switchCases: Node[] = [];
1076
1921
 
1077
- statements.push(...x.body);
1922
+ cases.forEach((caseObject, i) => {
1923
+ // Empty case OR single break statement is skipped
1924
+ if (
1925
+ caseObject.body.length === 0 ||
1926
+ (caseObject.body.length === 1 &&
1927
+ caseObject.body[0].type === "BreakStatement" &&
1928
+ caseObject.body[0].label?.name === switchLabel)
1929
+ )
1930
+ return;
1078
1931
 
1079
- var test = Literal(x.state);
1932
+ var test = Literal(caseObject.state);
1933
+ var isEligibleForOutlining = false;
1080
1934
 
1081
- return SwitchCase(test, statements);
1082
- })
1083
- );
1935
+ // Check if Control Map has this value
1936
+ if (!this.isDebug && controlConstantMap.has(caseObject.state)) {
1937
+ test = getControlMember(
1938
+ controlConstantMap.get(caseObject.state)?.key
1939
+ );
1940
+ }
1084
1941
 
1085
- var declarations = [];
1942
+ // Create complex test expressions for each switch case
1943
+ if (!this.isDebug && this.addComplexTest && chance(25)) {
1944
+ isEligibleForOutlining = true;
1086
1945
 
1087
- if (needsTestVar) {
1088
- declarations.push(VariableDeclarator(testVar));
1089
- }
1946
+ // case STATE+X:
1947
+ var stateVarIndex = getRandomInteger(0, stateVars.length);
1090
1948
 
1091
- if (needsResultAndArgVar) {
1092
- declarations.push(VariableDeclarator(resultVar));
1093
- declarations.push(VariableDeclarator(argVar));
1094
- }
1949
+ var stateValues = labelToStates[caseObject.label];
1950
+ var difference = stateValues[stateVarIndex] - caseObject.state;
1951
+
1952
+ var conditionNodes: Node[] = [];
1953
+ var alreadyConditionedItems = new Set<string>();
1954
+
1955
+ // This code finds clash conditions and adds them to 'conditionNodes' array
1956
+ Object.keys(labelToStates).forEach((label) => {
1957
+ if (label !== caseObject.label) {
1958
+ var labelStates = labelToStates[label];
1959
+ var totalState = labelStates.reduce((a, b) => a + b, 0);
1095
1960
 
1096
- if (needsStringBankVar) {
1097
- declarations.push(
1098
- VariableDeclarator(
1099
- stringBankVar,
1100
- ObjectExpression(
1101
- stringBankByLabels[startLabel]
1102
- ? Array.from(stringBankByLabels[startLabel]).map((strValue) =>
1103
- Property(
1104
- Literal(stringBank[strValue]),
1105
- Literal(strValue),
1106
- false
1961
+ if (totalState === labelStates[stateVarIndex] - difference) {
1962
+ var differentIndex = labelStates.findIndex(
1963
+ (v, i) => v !== stateValues[i]
1964
+ );
1965
+ if (differentIndex !== -1) {
1966
+ var expressionAsString =
1967
+ stateVars[differentIndex] +
1968
+ "!=" +
1969
+ labelStates[differentIndex];
1970
+ if (!alreadyConditionedItems.has(expressionAsString)) {
1971
+ alreadyConditionedItems.add(expressionAsString);
1972
+
1973
+ conditionNodes.push(
1974
+ BinaryExpression(
1975
+ "!=",
1976
+ Identifier(stateVars[differentIndex]),
1977
+ Literal(labelStates[differentIndex])
1978
+ )
1979
+ );
1980
+ }
1981
+ } else {
1982
+ conditionNodes.push(
1983
+ BinaryExpression(
1984
+ "!=",
1985
+ clone(discriminant),
1986
+ Literal(totalState)
1107
1987
  )
1108
- )
1109
- : []
1988
+ );
1989
+ }
1990
+ }
1991
+ }
1992
+ });
1993
+
1994
+ // case STATE!=Y && STATE+X
1995
+ test = BinaryExpression(
1996
+ "-",
1997
+ Identifier(stateVars[stateVarIndex]),
1998
+ Literal(difference)
1999
+ );
2000
+
2001
+ // Use the 'conditionNodes' to not cause state clashing issues
2002
+ conditionNodes.forEach((conditionNode) => {
2003
+ test = LogicalExpression("&&", conditionNode, test);
2004
+ });
2005
+ }
2006
+
2007
+ // A 'flagged' label has addition 'flagKey' that gets switched before jumped to
2008
+ if (flaggedLabels[caseObject.label]) {
2009
+ isEligibleForOutlining = true;
2010
+
2011
+ var { flagKey, flagValue } = flaggedLabels[caseObject.label];
2012
+
2013
+ var alternateNum: number;
2014
+ do {
2015
+ alternateNum = getRandomInteger(-1000, 1000 + chunks.length);
2016
+ } while (caseSelection.has(alternateNum));
2017
+
2018
+ var alternate = Literal(alternateNum);
2019
+
2020
+ // case FLAG ? <REAL> : <FAKE>:
2021
+ test = ConditionalExpression(
2022
+ getControlMember(flagKey),
2023
+
2024
+ flagValue ? test : alternate,
2025
+ !flagValue ? test : alternate
2026
+ );
2027
+ }
2028
+
2029
+ // Outline this switch case test
2030
+ if (
2031
+ !this.isDebug &&
2032
+ this.outlineExpressions &&
2033
+ isEligibleForOutlining &&
2034
+ chance(75 - outlinesCreated - this.mangledExpressionsMade / 25)
2035
+ ) {
2036
+ this.mangledExpressionsMade++;
2037
+
2038
+ // Selected a random parent node (or this node) to insert this function in
2039
+ var selectedControlNode = choice(allControlNodes);
2040
+ var selectedControlProperties =
2041
+ selectedControlNode.$controlProperties;
2042
+ var selectedControlVar = selectedControlNode.$controlVar;
2043
+ var selectedControlGen = selectedControlNode.$controlGen;
2044
+
2045
+ var fnKey = selectedControlGen.generate();
2046
+
2047
+ // Pass in the:
2048
+ // - controlVar for 'flagged labels' code check
2049
+ // - stateVars for 'complex test expressions'
2050
+ // (Check which identifiers are actually needed)
2051
+ var argumentList = [],
2052
+ watchingFor = new Set([controlVar, ...stateVars]);
2053
+ walk(test, [], (o, p) => {
2054
+ if (o.type === "Identifier" && watchingFor.has(o.name)) {
2055
+ watchingFor.delete(o.name);
2056
+ argumentList.push(Identifier(o.name));
2057
+ }
2058
+ });
2059
+
2060
+ selectedControlProperties.push(
2061
+ Property(
2062
+ Literal(fnKey),
2063
+ FunctionExpression(argumentList, [ReturnStatement(test)]),
2064
+ true
1110
2065
  )
1111
- )
1112
- );
1113
- }
2066
+ );
2067
+
2068
+ // case control.a(control, s1, s2):
2069
+ test = CallExpression(
2070
+ getControlMember(fnKey, selectedControlVar),
2071
+ clone(argumentList)
2072
+ );
2073
+ }
2074
+
2075
+ // One random case gets to be default
2076
+ if (!this.isDebug && i === defaultCaseIndex) test = null;
2077
+
2078
+ var testArray: Node[] = [test];
2079
+ if (!this.isDebug && this.addFakeTest && chance(50)) {
2080
+ // Add fake test
2081
+ // case <FAKE>:
2082
+ // case <REAL>:
2083
+ // case <FAKE>:
2084
+ var fakeTestCount = getRandomInteger(1, 4);
2085
+ for (var i = 0; i < fakeTestCount; i++) {
2086
+ // Create a fake test number that doesn't interfere with the actual states
2087
+ var fakeTestNum;
2088
+ do {
2089
+ fakeTestNum = getRandomInteger(1, 1000 + caseSelection.size);
2090
+ } while (caseSelection.has(fakeTestNum));
2091
+
2092
+ // Add this fake test
2093
+ testArray.push(Literal(fakeTestNum));
2094
+ }
1114
2095
 
2096
+ shuffle(testArray);
2097
+ }
2098
+
2099
+ testArray.forEach((test, i) => {
2100
+ var body = i === testArray.length - 1 ? caseObject.body : [];
2101
+
2102
+ switchCases.push(SwitchCase(test, body));
2103
+ });
2104
+ });
2105
+
2106
+ // switch(state) { case ... }
2107
+ var switchStatement: Node = SwitchStatement(discriminant, switchCases);
2108
+
2109
+ var declarations: Node[] = [];
2110
+
2111
+ // var state = START_STATE
1115
2112
  declarations.push(
1116
2113
  ...stateVars.map((stateVar, i) => {
1117
2114
  return VariableDeclarator(stateVar, Literal(initStateValues[i]));
1118
2115
  })
1119
2116
  );
1120
2117
 
1121
- body.push(
1122
- VariableDeclaration(declarations),
2118
+ // var control = { strings, numbers, outlined functions, etc... }
2119
+ var objectExpression = ObjectExpression(controlProperties);
2120
+ declarations.push(VariableDeclarator(controlVar, objectExpression));
1123
2121
 
1124
- WhileStatement(
1125
- BinaryExpression("!=", clone(discriminant), Literal(endState)),
1126
- [LabeledStatement(switchLabel, switchStatement)]
2122
+ objectBody.push(
2123
+ // Use individual variable declarations instead so Stack can apply
2124
+ ...declarations.map((declaration) =>
2125
+ VariableDeclaration(declaration, "var")
1127
2126
  )
1128
2127
  );
1129
2128
 
2129
+ // while (state != END_STATE) {...}
2130
+ var whileTest = BinaryExpression(
2131
+ "!=",
2132
+ clone(discriminant),
2133
+ Literal(endState)
2134
+ );
2135
+
2136
+ objectBody.push(
2137
+ WhileStatement(whileTest, [
2138
+ LabeledStatement(switchLabel, switchStatement),
2139
+ ])
2140
+ );
2141
+
1130
2142
  // mark this object for switch case obfuscation
1131
2143
  switchStatement.$controlFlowFlattening = true;
1132
2144
  };