webfuscator 1.2.1 → 1.3.1

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 (209) hide show
  1. package/README.md +3 -3
  2. package/dist/analysis/constant.d.ts.map +1 -1
  3. package/dist/analysis/constant.js +10 -27
  4. package/dist/analysis/constant.js.map +1 -1
  5. package/dist/analysis/inlineability.d.ts.map +1 -1
  6. package/dist/analysis/inlineability.js +5 -13
  7. package/dist/analysis/inlineability.js.map +1 -1
  8. package/dist/analysis/purity.d.ts +3 -3
  9. package/dist/analysis/purity.d.ts.map +1 -1
  10. package/dist/analysis/purity.js +28 -75
  11. package/dist/analysis/purity.js.map +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/obfuscator.d.ts +2 -1
  15. package/dist/obfuscator.d.ts.map +1 -1
  16. package/dist/obfuscator.js +47 -12
  17. package/dist/obfuscator.js.map +1 -1
  18. package/dist/options.d.ts +17 -4
  19. package/dist/options.d.ts.map +1 -1
  20. package/dist/preparation/expand-destructuring.d.ts +2 -3
  21. package/dist/preparation/expand-destructuring.d.ts.map +1 -1
  22. package/dist/preparation/expand-destructuring.js +2 -8
  23. package/dist/preparation/expand-destructuring.js.map +1 -1
  24. package/dist/transforms/arguments-to-parameters.d.ts +5 -4
  25. package/dist/transforms/arguments-to-parameters.d.ts.map +1 -1
  26. package/dist/transforms/arguments-to-parameters.js +5 -4
  27. package/dist/transforms/arguments-to-parameters.js.map +1 -1
  28. package/dist/transforms/arrow-to-function.d.ts +4 -3
  29. package/dist/transforms/arrow-to-function.d.ts.map +1 -1
  30. package/dist/transforms/arrow-to-function.js +6 -10
  31. package/dist/transforms/arrow-to-function.js.map +1 -1
  32. package/dist/transforms/collapse-single-use-temps.d.ts +2 -1
  33. package/dist/transforms/collapse-single-use-temps.d.ts.map +1 -1
  34. package/dist/transforms/collapse-single-use-temps.js +12 -20
  35. package/dist/transforms/collapse-single-use-temps.js.map +1 -1
  36. package/dist/transforms/conceal-strings.d.ts +56 -0
  37. package/dist/transforms/conceal-strings.d.ts.map +1 -0
  38. package/dist/transforms/conceal-strings.js +356 -0
  39. package/dist/transforms/conceal-strings.js.map +1 -0
  40. package/dist/transforms/const-let-to-var.d.ts +4 -3
  41. package/dist/transforms/const-let-to-var.d.ts.map +1 -1
  42. package/dist/transforms/const-let-to-var.js +8 -67
  43. package/dist/transforms/const-let-to-var.js.map +1 -1
  44. package/dist/transforms/const-to-let.d.ts +1 -3
  45. package/dist/transforms/const-to-let.d.ts.map +1 -1
  46. package/dist/transforms/const-to-let.js +1 -8
  47. package/dist/transforms/const-to-let.js.map +1 -1
  48. package/dist/transforms/drop-console.d.ts.map +1 -1
  49. package/dist/transforms/drop-console.js +3 -5
  50. package/dist/transforms/drop-console.js.map +1 -1
  51. package/dist/transforms/expand-binary-assignment.d.ts.map +1 -1
  52. package/dist/transforms/expand-binary-assignment.js +0 -5
  53. package/dist/transforms/expand-binary-assignment.js.map +1 -1
  54. package/dist/transforms/expand-logical-assignment.d.ts.map +1 -1
  55. package/dist/transforms/expand-logical-assignment.js +0 -5
  56. package/dist/transforms/expand-logical-assignment.js.map +1 -1
  57. package/dist/transforms/extract-object-properties.js +5 -12
  58. package/dist/transforms/extract-object-properties.js.map +1 -1
  59. package/dist/transforms/fold-builtin-methods.d.ts.map +1 -1
  60. package/dist/transforms/fold-builtin-methods.js +44 -14
  61. package/dist/transforms/fold-builtin-methods.js.map +1 -1
  62. package/dist/transforms/fold-constants.js +5 -2
  63. package/dist/transforms/fold-constants.js.map +1 -1
  64. package/dist/transforms/function-declaration-to-expression.d.ts +5 -6
  65. package/dist/transforms/function-declaration-to-expression.d.ts.map +1 -1
  66. package/dist/transforms/function-declaration-to-expression.js +5 -6
  67. package/dist/transforms/function-declaration-to-expression.js.map +1 -1
  68. package/dist/transforms/hoist-variables.d.ts +29 -0
  69. package/dist/transforms/hoist-variables.d.ts.map +1 -0
  70. package/dist/transforms/hoist-variables.js +215 -0
  71. package/dist/transforms/hoist-variables.js.map +1 -0
  72. package/dist/transforms/inline-functions.js +11 -40
  73. package/dist/transforms/inline-functions.js.map +1 -1
  74. package/dist/transforms/logical-to-ternary.d.ts +1 -1
  75. package/dist/transforms/logical-to-ternary.js +5 -9
  76. package/dist/transforms/logical-to-ternary.js.map +1 -1
  77. package/dist/transforms/mangle-properties.d.ts +2 -5
  78. package/dist/transforms/mangle-properties.d.ts.map +1 -1
  79. package/dist/transforms/mangle-properties.js +9 -9
  80. package/dist/transforms/mangle-properties.js.map +1 -1
  81. package/dist/transforms/merge-variable-declarations.d.ts +17 -0
  82. package/dist/transforms/merge-variable-declarations.d.ts.map +1 -0
  83. package/dist/transforms/merge-variable-declarations.js +68 -0
  84. package/dist/transforms/merge-variable-declarations.js.map +1 -0
  85. package/dist/transforms/nullish-coalescing-to-ternary.d.ts +4 -5
  86. package/dist/transforms/nullish-coalescing-to-ternary.d.ts.map +1 -1
  87. package/dist/transforms/nullish-coalescing-to-ternary.js +7 -12
  88. package/dist/transforms/nullish-coalescing-to-ternary.js.map +1 -1
  89. package/dist/transforms/numbers-to-strings.d.ts.map +1 -1
  90. package/dist/transforms/numbers-to-strings.js +1 -8
  91. package/dist/transforms/numbers-to-strings.js.map +1 -1
  92. package/dist/transforms/object-method-to-property.d.ts +1 -1
  93. package/dist/transforms/object-method-to-property.js +1 -1
  94. package/dist/transforms/optional-chaining-to-ternary.d.ts +4 -3
  95. package/dist/transforms/optional-chaining-to-ternary.d.ts.map +1 -1
  96. package/dist/transforms/optional-chaining-to-ternary.js +9 -9
  97. package/dist/transforms/optional-chaining-to-ternary.js.map +1 -1
  98. package/dist/transforms/pack-declarations-into-parameters.d.ts.map +1 -1
  99. package/dist/transforms/pack-declarations-into-parameters.js +5 -21
  100. package/dist/transforms/pack-declarations-into-parameters.js.map +1 -1
  101. package/dist/transforms/pack.d.ts +4 -13
  102. package/dist/transforms/pack.d.ts.map +1 -1
  103. package/dist/transforms/pack.js +9 -29
  104. package/dist/transforms/pack.js.map +1 -1
  105. package/dist/transforms/remove-anonymous-function-names.d.ts +2 -4
  106. package/dist/transforms/remove-anonymous-function-names.d.ts.map +1 -1
  107. package/dist/transforms/remove-anonymous-function-names.js +2 -40
  108. package/dist/transforms/remove-anonymous-function-names.js.map +1 -1
  109. package/dist/transforms/remove-unreachable-code.js +2 -21
  110. package/dist/transforms/remove-unreachable-code.js.map +1 -1
  111. package/dist/transforms/remove-unused-code.d.ts.map +1 -1
  112. package/dist/transforms/remove-unused-code.js +12 -49
  113. package/dist/transforms/remove-unused-code.js.map +1 -1
  114. package/dist/transforms/rename-identifiers.d.ts +3 -3
  115. package/dist/transforms/rename-identifiers.d.ts.map +1 -1
  116. package/dist/transforms/rename-identifiers.js +31 -35
  117. package/dist/transforms/rename-identifiers.js.map +1 -1
  118. package/dist/transforms/reorder-function-declarations.d.ts +24 -0
  119. package/dist/transforms/reorder-function-declarations.d.ts.map +1 -0
  120. package/dist/transforms/reorder-function-declarations.js +62 -0
  121. package/dist/transforms/reorder-function-declarations.js.map +1 -0
  122. package/dist/transforms/specials-to-strings.d.ts.map +1 -1
  123. package/dist/transforms/specials-to-strings.js +2 -3
  124. package/dist/transforms/specials-to-strings.js.map +1 -1
  125. package/dist/transforms/switch-to-if.js +5 -15
  126. package/dist/transforms/switch-to-if.js.map +1 -1
  127. package/dist/transforms/ternary-to-if.d.ts.map +1 -1
  128. package/dist/transforms/ternary-to-if.js +5 -9
  129. package/dist/transforms/ternary-to-if.js.map +1 -1
  130. package/dist/transforms/update-to-assignment.d.ts.map +1 -1
  131. package/dist/transforms/update-to-assignment.js +6 -23
  132. package/dist/transforms/update-to-assignment.js.map +1 -1
  133. package/dist/transforms/yodify-conditions.d.ts +3 -4
  134. package/dist/transforms/yodify-conditions.d.ts.map +1 -1
  135. package/dist/transforms/yodify-conditions.js +3 -4
  136. package/dist/transforms/yodify-conditions.js.map +1 -1
  137. package/dist/utils/ast.d.ts +1 -3
  138. package/dist/utils/ast.d.ts.map +1 -1
  139. package/dist/utils/ast.js +4 -60
  140. package/dist/utils/ast.js.map +1 -1
  141. package/dist/utils/evaluation-order.js +1 -1
  142. package/dist/utils/evaluation-order.js.map +1 -1
  143. package/dist/utils/literal.d.ts.map +1 -1
  144. package/dist/utils/literal.js +74 -2
  145. package/dist/utils/literal.js.map +1 -1
  146. package/dist/utils/logical-lowering.d.ts.map +1 -1
  147. package/dist/utils/logical-lowering.js +7 -9
  148. package/dist/utils/logical-lowering.js.map +1 -1
  149. package/dist/utils/nullish.d.ts +4 -0
  150. package/dist/utils/nullish.d.ts.map +1 -0
  151. package/dist/utils/nullish.js +8 -0
  152. package/dist/utils/nullish.js.map +1 -0
  153. package/dist/utils/paths.d.ts +0 -2
  154. package/dist/utils/paths.d.ts.map +1 -1
  155. package/dist/utils/paths.js +0 -28
  156. package/dist/utils/paths.js.map +1 -1
  157. package/dist/utils/statement-lists.d.ts +9 -0
  158. package/dist/utils/statement-lists.d.ts.map +1 -0
  159. package/dist/utils/statement-lists.js +27 -0
  160. package/dist/utils/statement-lists.js.map +1 -0
  161. package/package.json +6 -2
  162. package/src/analysis/constant.ts +12 -33
  163. package/src/analysis/inlineability.ts +5 -13
  164. package/src/analysis/purity.ts +29 -92
  165. package/src/index.ts +1 -0
  166. package/src/obfuscator.ts +51 -14
  167. package/src/options.ts +21 -3
  168. package/src/preparation/expand-destructuring.ts +2 -10
  169. package/src/transforms/arguments-to-parameters.ts +5 -4
  170. package/src/transforms/arrow-to-function.ts +6 -10
  171. package/src/transforms/collapse-single-use-temps.ts +12 -23
  172. package/src/transforms/conceal-strings.ts +525 -0
  173. package/src/transforms/const-let-to-var.ts +9 -72
  174. package/src/transforms/const-to-let.ts +1 -8
  175. package/src/transforms/drop-console.ts +3 -5
  176. package/src/transforms/expand-binary-assignment.ts +0 -5
  177. package/src/transforms/expand-logical-assignment.ts +0 -5
  178. package/src/transforms/extract-object-properties.ts +5 -14
  179. package/src/transforms/fold-builtin-methods.ts +63 -17
  180. package/src/transforms/fold-constants.ts +5 -2
  181. package/src/transforms/function-declaration-to-expression.ts +5 -6
  182. package/src/transforms/hoist-variables.ts +262 -0
  183. package/src/transforms/inline-functions.ts +16 -47
  184. package/src/transforms/logical-to-ternary.ts +5 -10
  185. package/src/transforms/mangle-properties.ts +9 -12
  186. package/src/transforms/merge-variable-declarations.ts +91 -0
  187. package/src/transforms/nullish-coalescing-to-ternary.ts +7 -13
  188. package/src/transforms/numbers-to-strings.ts +2 -12
  189. package/src/transforms/object-method-to-property.ts +1 -1
  190. package/src/transforms/optional-chaining-to-ternary.ts +9 -9
  191. package/src/transforms/pack-declarations-into-parameters.ts +11 -21
  192. package/src/transforms/pack.ts +9 -30
  193. package/src/transforms/remove-anonymous-function-names.ts +3 -46
  194. package/src/transforms/remove-unreachable-code.ts +2 -22
  195. package/src/transforms/remove-unused-code.ts +14 -74
  196. package/src/transforms/rename-identifiers.ts +44 -35
  197. package/src/transforms/reorder-function-declarations.ts +78 -0
  198. package/src/transforms/specials-to-strings.ts +2 -3
  199. package/src/transforms/switch-to-if.ts +5 -22
  200. package/src/transforms/ternary-to-if.ts +5 -9
  201. package/src/transforms/update-to-assignment.ts +6 -24
  202. package/src/transforms/yodify-conditions.ts +3 -4
  203. package/src/utils/ast.ts +4 -69
  204. package/src/utils/evaluation-order.ts +1 -1
  205. package/src/utils/literal.ts +91 -2
  206. package/src/utils/logical-lowering.ts +7 -11
  207. package/src/utils/nullish.ts +23 -0
  208. package/src/utils/paths.ts +0 -32
  209. package/src/utils/statement-lists.ts +45 -0
@@ -0,0 +1,525 @@
1
+ import traverse from '@babel/traverse'
2
+ import type { NodePath, Visitor } from '@babel/traverse'
3
+ import * as t from '@babel/types'
4
+ import type { File } from '@babel/types'
5
+
6
+ import type { ConcealStringsOptions, TransformContext } from '../options'
7
+ import { isNonComputedKey } from '../utils/ast'
8
+ import type { ChangeState } from '../utils/change-tracking'
9
+ import { mulberry32, shuffleInPlace } from '../utils/random'
10
+
11
+ const RECORD_WIDTH = 4
12
+
13
+ /**
14
+ * Replaces runtime string literals with calls to a generated decoder. Each vault
15
+ * stores masked positions in a shuffled UTF-16 pool. The decoder handles lone
16
+ * surrogates without relying on mutable built-ins. The default `regional` pool
17
+ * covers U+0000-00FF and every 256-unit page used by the program. `full` covers
18
+ * all 65,536 code units.
19
+ *
20
+ * `scope: 'program'` creates one vault. `scope: 'function'` creates a vault per
21
+ * function but repeats the program-wide pool in each one. Parameter defaults and
22
+ * computed keys use the enclosing vault because they run before the function
23
+ * body.
24
+ *
25
+ * Directives, module specifiers, import attributes, and non-computed keys are not
26
+ * runtime expressions, so they stay unchanged.
27
+ *
28
+ * This is obfuscation, not encryption. The output contains the decoder and all
29
+ * data needed to recover each string. `cover: 'used'` also leaves one-unit
30
+ * strings in the pool and may reproduce longer text by chance.
31
+ *
32
+ * The example uses `{ cover: 'used' }` to keep the pool short.
33
+ *
34
+ * @example
35
+ * // ◀️ before
36
+ * function greet(name) {
37
+ * return "Hello, " + name;
38
+ * }
39
+ *
40
+ * // ▶️ after
41
+ * const _stringPool = " lo,He",
42
+ * _stringPayload = [0, 4, 7, 2, 2, 6, 2],
43
+ * _stringMetadata = [0, 7, 4, 5],
44
+ * _stringCache = [null],
45
+ * _stringDecoder = function (_entry) {
46
+ * let _cached = _stringCache[_entry];
47
+ * if (_cached !== null) return _cached;
48
+ * let _result = "",
49
+ * _record = _entry * 4,
50
+ * _start = _stringMetadata[_record],
51
+ * _length = _stringMetadata[_record + 1],
52
+ * _stringKey = _stringMetadata[_record + 2],
53
+ * _stringStep = _stringMetadata[_record + 3];
54
+ * for (let _position = 0; _position < _length; _position++) {
55
+ * let _stringMask = _stringKey + _position * _stringStep & 7;
56
+ * _result += _stringPool[_stringPayload[_start + _position] ^ _stringMask];
57
+ * }
58
+ * _stringCache[_entry] = _result;
59
+ * return _result;
60
+ * };
61
+ * function greet(name) {
62
+ * return _stringDecoder(0) + name;
63
+ * }
64
+ */
65
+ export function concealStrings(ast: File, ctx: TransformContext): boolean {
66
+ const options = resolveOptions(ctx.concealStrings)
67
+ const rng = mulberry32(ctx.seed)
68
+ const state: State = { changed: false, groups: new Map(), options }
69
+
70
+ if (options.scope === 'function') {
71
+ normalizeConciseArrows(ast, options)
72
+ }
73
+
74
+ traverse(ast, collectVisitor, undefined, state)
75
+ if (state.groups.size === 0) {
76
+ return false
77
+ }
78
+ const pool = buildSharedPool(state.groups.values(), options.cover, rng)
79
+ for (const group of state.groups.values()) {
80
+ concealGroup(group, options, pool, rng)
81
+ state.changed = true
82
+ }
83
+ return state.changed
84
+ }
85
+
86
+ interface ResolvedOptions {
87
+ cache: boolean
88
+ cover: 'full' | 'regional' | 'used'
89
+ deduplicate: boolean
90
+ scope: 'function' | 'program'
91
+ }
92
+
93
+ type VaultOwner = t.Program | t.Function
94
+ type VaultOwnerPath = NodePath<VaultOwner>
95
+
96
+ interface Occurrence {
97
+ path: NodePath<t.StringLiteral>
98
+ value: string
99
+ }
100
+
101
+ interface VaultGroup {
102
+ occurrences: Occurrence[]
103
+ owner: VaultOwnerPath
104
+ }
105
+
106
+ interface State extends ChangeState {
107
+ groups: Map<VaultOwner, VaultGroup>
108
+ options: ResolvedOptions
109
+ }
110
+
111
+ interface Entry {
112
+ occurrences: Occurrence[]
113
+ value: string
114
+ }
115
+
116
+ interface VaultData {
117
+ indexMask: number
118
+ metadata: number[]
119
+ payload: number[]
120
+ pool: string
121
+ }
122
+
123
+ interface SharedPool {
124
+ indexMask: number
125
+ positions: ReadonlyMap<string, number>
126
+ value: string
127
+ }
128
+
129
+ interface VaultIdentifiers {
130
+ cache: t.Identifier | null
131
+ decoder: t.Identifier
132
+ metadata: t.Identifier
133
+ payload: t.Identifier
134
+ pool: t.Identifier
135
+ }
136
+
137
+ const collectVisitor: Visitor<State> = {
138
+ StringLiteral(path, state) {
139
+ if (!isRuntimeString(path)) {
140
+ return
141
+ }
142
+ const owner = findVaultOwner(path, state.options.scope)
143
+ if (!owner) {
144
+ return
145
+ }
146
+ let group = state.groups.get(owner.node)
147
+ if (!group) {
148
+ group = { occurrences: [], owner }
149
+ state.groups.set(owner.node, group)
150
+ }
151
+ group.occurrences.push({ path, value: path.node.value })
152
+ },
153
+ }
154
+
155
+ function resolveOptions(options: ConcealStringsOptions | undefined): ResolvedOptions {
156
+ const cache = options?.cache ?? true
157
+ const cover = options?.cover ?? 'regional'
158
+ const deduplicate = options?.deduplicate ?? true
159
+ const scope = options?.scope ?? 'program'
160
+ if (typeof cache !== 'boolean') {
161
+ throw new TypeError('concealStrings cache must be a boolean')
162
+ }
163
+ if (cover !== 'full' && cover !== 'regional' && cover !== 'used') {
164
+ throw new TypeError("concealStrings cover must be 'full', 'regional', or 'used'")
165
+ }
166
+ if (typeof deduplicate !== 'boolean') {
167
+ throw new TypeError('concealStrings deduplicate must be a boolean')
168
+ }
169
+ if (scope !== 'function' && scope !== 'program') {
170
+ throw new TypeError("concealStrings scope must be 'function' or 'program'")
171
+ }
172
+ return { cache, cover, deduplicate, scope }
173
+ }
174
+
175
+ function normalizeConciseArrows(ast: File, options: ResolvedOptions): void {
176
+ const marked = new WeakSet<t.ArrowFunctionExpression>()
177
+ traverse(ast, {
178
+ StringLiteral(path) {
179
+ if (!isRuntimeString(path)) {
180
+ return
181
+ }
182
+ const owner = findVaultOwner(path, options.scope)
183
+ if (owner?.isArrowFunctionExpression() && !t.isBlockStatement(owner.node.body)) {
184
+ marked.add(owner.node)
185
+ }
186
+ },
187
+ ArrowFunctionExpression: {
188
+ exit(path) {
189
+ if (!marked.has(path.node) || t.isBlockStatement(path.node.body)) {
190
+ return
191
+ }
192
+ const expression = path.node.body
193
+ path.get('body').replaceWith(t.blockStatement([t.returnStatement(expression)]))
194
+ path.node.expression = false
195
+ },
196
+ },
197
+ })
198
+ }
199
+
200
+ function findVaultOwner(
201
+ path: NodePath<t.StringLiteral>,
202
+ scope: ResolvedOptions['scope'],
203
+ ): VaultOwnerPath | null {
204
+ let child: NodePath = path
205
+ let current: NodePath | null = path.parentPath
206
+ while (current) {
207
+ if (current.isFunction() && child.key === 'body' && scope === 'function') {
208
+ return current as VaultOwnerPath
209
+ }
210
+ if (current.isProgram()) {
211
+ return current
212
+ }
213
+ child = current
214
+ current = current.parentPath
215
+ }
216
+ return null
217
+ }
218
+
219
+ function isRuntimeString(path: NodePath<t.StringLiteral>): boolean {
220
+ const parent = path.parent
221
+ const node = path.node
222
+ if (
223
+ (t.isImportDeclaration(parent) && parent.source === node) ||
224
+ (t.isExportNamedDeclaration(parent) && parent.source === node) ||
225
+ (t.isExportAllDeclaration(parent) && parent.source === node) ||
226
+ t.isImportAttribute(parent) ||
227
+ (t.isImportSpecifier(parent) && parent.imported === node) ||
228
+ (t.isExportSpecifier(parent) && parent.exported === node) ||
229
+ (t.isExportNamespaceSpecifier(parent) && parent.exported === node)
230
+ ) {
231
+ return false
232
+ }
233
+ return !isNonComputedKey(path)
234
+ }
235
+
236
+ function concealGroup(
237
+ group: VaultGroup,
238
+ options: ResolvedOptions,
239
+ pool: SharedPool,
240
+ rng: () => number,
241
+ ): void {
242
+ const entries = makeEntries(group.occurrences, options.deduplicate)
243
+ shuffleInPlace(entries, rng)
244
+ const data = encodeEntries(entries, pool, rng)
245
+ const identifiers = insertVault(statementBody(group.owner), entries.length, data, options.cache)
246
+
247
+ for (let index = 0; index < entries.length; index++) {
248
+ for (const occurrence of entries[index]!.occurrences) {
249
+ occurrence.path.replaceWith(
250
+ t.callExpression(t.identifier(identifiers.decoder.name), [t.numericLiteral(index)]),
251
+ )
252
+ occurrence.path.skip()
253
+ }
254
+ }
255
+ }
256
+
257
+ function makeEntries(occurrences: Occurrence[], deduplicate: boolean): Entry[] {
258
+ if (!deduplicate) {
259
+ return occurrences.map((occurrence) => ({ occurrences: [occurrence], value: occurrence.value }))
260
+ }
261
+ const byValue = new Map<string, Entry>()
262
+ for (const occurrence of occurrences) {
263
+ let entry = byValue.get(occurrence.value)
264
+ if (!entry) {
265
+ entry = { occurrences: [], value: occurrence.value }
266
+ byValue.set(occurrence.value, entry)
267
+ }
268
+ entry.occurrences.push(occurrence)
269
+ }
270
+ return [...byValue.values()]
271
+ }
272
+
273
+ function buildSharedPool(
274
+ groups: Iterable<VaultGroup>,
275
+ cover: ResolvedOptions['cover'],
276
+ rng: () => number,
277
+ ): SharedPool {
278
+ const usedUnits = new Set<string>()
279
+ for (const group of groups) {
280
+ for (const occurrence of group.occurrences) {
281
+ let index = 0
282
+ while (index < occurrence.value.length) {
283
+ usedUnits.add(occurrence.value[index]!)
284
+ index++
285
+ }
286
+ }
287
+ }
288
+
289
+ const poolUnits = makePoolUnits(usedUnits, cover)
290
+ shuffleInPlace(poolUnits, rng)
291
+ return {
292
+ indexMask: coveringBitMask(poolUnits.length),
293
+ positions: indexPoolPositions(poolUnits),
294
+ value: poolUnits.join(''),
295
+ }
296
+ }
297
+
298
+ function encodeEntries(entries: Entry[], pool: SharedPool, rng: () => number): VaultData {
299
+ const metadata: number[] = []
300
+ const payload: number[] = []
301
+
302
+ for (const entry of entries) {
303
+ const key = randomInteger(pool.indexMask + 1, rng)
304
+ const step = randomOddInteger(pool.indexMask, rng)
305
+ metadata.push(payload.length, entry.value.length, key, step)
306
+ for (let position = 0; position < entry.value.length; position++) {
307
+ const truePosition = pool.positions.get(entry.value[position]!)!
308
+ payload.push(truePosition ^ maskPosition(position, key, step, pool.indexMask))
309
+ }
310
+ }
311
+ return { indexMask: pool.indexMask, metadata, payload, pool: pool.value }
312
+ }
313
+
314
+ function makePoolUnits(usedUnits: ReadonlySet<string>, cover: ResolvedOptions['cover']): string[] {
315
+ return cover === 'used' ? [...usedUnits] : coveredPageUnits(usedUnits, cover)
316
+ }
317
+
318
+ function coveredPageUnits(
319
+ usedUnits: ReadonlySet<string>,
320
+ cover: Exclude<ResolvedOptions['cover'], 'used'>,
321
+ ): string[] {
322
+ const pages = new Set<number>()
323
+ if (cover === 'full') {
324
+ for (let page = 0; page < 0x01_00; page++) {
325
+ pages.add(page)
326
+ }
327
+ } else {
328
+ pages.add(0)
329
+ for (const unit of usedUnits) {
330
+ pages.add(unit.codePointAt(0)! >>> 8)
331
+ }
332
+ }
333
+
334
+ const units: string[] = []
335
+ for (const page of pages) {
336
+ const start = page << 8
337
+ for (let offset = 0; offset < 0x01_00; offset++) {
338
+ units.push(String.fromCodePoint(start + offset))
339
+ }
340
+ }
341
+ return units
342
+ }
343
+
344
+ function indexPoolPositions(pool: readonly string[]): Map<string, number> {
345
+ const positions = new Map<string, number>()
346
+ for (let index = 0; index < pool.length; index++) {
347
+ positions.set(pool[index]!, index)
348
+ }
349
+ return positions
350
+ }
351
+
352
+ function coveringBitMask(length: number): number {
353
+ let mask = 0
354
+ while (mask < length - 1) {
355
+ mask = (mask << 1) | 1
356
+ }
357
+ return mask
358
+ }
359
+
360
+ function randomInteger(limit: number, rng: () => number): number {
361
+ return limit === 0 ? 0 : Math.floor(rng() * limit)
362
+ }
363
+
364
+ function randomOddInteger(mask: number, rng: () => number): number {
365
+ return mask === 0 ? 0 : randomInteger((mask + 1) / 2, rng) * 2 + 1
366
+ }
367
+
368
+ function maskPosition(position: number, key: number, step: number, indexMask: number): number {
369
+ return (key + position * step) & indexMask
370
+ }
371
+
372
+ function statementBody(owner: VaultOwnerPath): NodePath<t.Program | t.BlockStatement> {
373
+ if (owner.isProgram()) {
374
+ return owner
375
+ }
376
+ const body = owner.get('body') as NodePath
377
+ if (!body.isBlockStatement()) {
378
+ throw new Error('concealStrings expected a normalized function body')
379
+ }
380
+ return body
381
+ }
382
+
383
+ function insertVault(
384
+ body: NodePath<t.Program | t.BlockStatement>,
385
+ entryCount: number,
386
+ data: VaultData,
387
+ cache: boolean,
388
+ ): VaultIdentifiers {
389
+ const fresh = (purpose: string): t.Identifier => t.identifier(body.scope.generateUid(purpose))
390
+ const identifiers: VaultIdentifiers = {
391
+ cache: cache ? fresh('stringCache') : null,
392
+ decoder: fresh('stringDecoder'),
393
+ metadata: fresh('stringMetadata'),
394
+ payload: fresh('stringPayload'),
395
+ pool: fresh('stringPool'),
396
+ }
397
+ const decoder = t.functionExpression(null, [], t.blockStatement([]))
398
+ const declarations = [
399
+ t.variableDeclarator(identifiers.pool, t.stringLiteral(data.pool)),
400
+ t.variableDeclarator(identifiers.payload, numericArray(data.payload)),
401
+ t.variableDeclarator(identifiers.metadata, numericArray(data.metadata)),
402
+ ]
403
+ if (identifiers.cache) {
404
+ declarations.push(
405
+ t.variableDeclarator(
406
+ identifiers.cache,
407
+ t.arrayExpression(Array.from({ length: entryCount }, () => t.nullLiteral())),
408
+ ),
409
+ )
410
+ }
411
+ declarations.push(t.variableDeclarator(identifiers.decoder, decoder))
412
+
413
+ const declaration = t.variableDeclaration('const', declarations)
414
+ const [declarationPath] = body.unshiftContainer('body', declaration)
415
+ const declaratorPaths = declarationPath!.get('declarations') as NodePath<t.VariableDeclarator>[]
416
+ const decoderPath = declaratorPaths
417
+ .find((path) => path.node.init === decoder)!
418
+ .get('init') as NodePath<t.FunctionExpression>
419
+ populateDecoder(decoderPath, identifiers, data.indexMask)
420
+ return identifiers
421
+ }
422
+
423
+ function populateDecoder(
424
+ decoderPath: NodePath<t.FunctionExpression>,
425
+ vault: VaultIdentifiers,
426
+ indexMask: number,
427
+ ): void {
428
+ const fresh = (purpose: string): t.Identifier =>
429
+ t.identifier(decoderPath.scope.generateUid(purpose))
430
+ const entry = fresh('entry')
431
+ const result = fresh('result')
432
+ const record = fresh('record')
433
+ const start = fresh('start')
434
+ const length = fresh('length')
435
+ const key = fresh('stringKey')
436
+ const step = fresh('stringStep')
437
+ const position = fresh('position')
438
+ const mask = fresh('stringMask')
439
+ const statements: t.Statement[] = []
440
+
441
+ if (vault.cache) {
442
+ const cached = fresh('cached')
443
+ statements.push(
444
+ t.variableDeclaration('let', [t.variableDeclarator(cached, indexed(vault.cache, entry))]),
445
+ t.ifStatement(t.binaryExpression('!==', cached, t.nullLiteral()), t.returnStatement(cached)),
446
+ )
447
+ }
448
+
449
+ statements.push(
450
+ t.variableDeclaration('let', [
451
+ t.variableDeclarator(result, t.stringLiteral('')),
452
+ t.variableDeclarator(record, t.binaryExpression('*', entry, t.numericLiteral(RECORD_WIDTH))),
453
+ t.variableDeclarator(start, indexed(vault.metadata, record)),
454
+ t.variableDeclarator(
455
+ length,
456
+ indexed(vault.metadata, t.binaryExpression('+', record, t.numericLiteral(1))),
457
+ ),
458
+ t.variableDeclarator(
459
+ key,
460
+ indexed(vault.metadata, t.binaryExpression('+', record, t.numericLiteral(2))),
461
+ ),
462
+ t.variableDeclarator(
463
+ step,
464
+ indexed(vault.metadata, t.binaryExpression('+', record, t.numericLiteral(3))),
465
+ ),
466
+ ]),
467
+ t.forStatement(
468
+ t.variableDeclaration('let', [t.variableDeclarator(position, t.numericLiteral(0))]),
469
+ t.binaryExpression('<', position, length),
470
+ t.updateExpression('++', position),
471
+ t.blockStatement([
472
+ t.variableDeclaration('let', [
473
+ t.variableDeclarator(mask, maskExpression(position, key, step, indexMask)),
474
+ ]),
475
+ t.expressionStatement(
476
+ t.assignmentExpression(
477
+ '+=',
478
+ result,
479
+ indexed(
480
+ vault.pool,
481
+ t.binaryExpression(
482
+ '^',
483
+ indexed(vault.payload, t.binaryExpression('+', start, position)),
484
+ mask,
485
+ ),
486
+ ),
487
+ ),
488
+ ),
489
+ ]),
490
+ ),
491
+ )
492
+ if (vault.cache) {
493
+ statements.push(
494
+ t.expressionStatement(t.assignmentExpression('=', indexed(vault.cache, entry), result)),
495
+ )
496
+ }
497
+ statements.push(t.returnStatement(result))
498
+ decoderPath.node.params = [entry]
499
+ decoderPath.node.body.body = statements
500
+ }
501
+
502
+ function maskExpression(
503
+ position: t.Identifier,
504
+ key: t.Identifier,
505
+ step: t.Identifier,
506
+ indexMask: number,
507
+ ): t.Expression {
508
+ return t.binaryExpression(
509
+ '&',
510
+ t.binaryExpression(
511
+ '+',
512
+ t.cloneNode(key),
513
+ t.binaryExpression('*', t.cloneNode(position), t.cloneNode(step)),
514
+ ),
515
+ t.numericLiteral(indexMask),
516
+ )
517
+ }
518
+
519
+ function indexed(object: t.Identifier, property: t.Expression): t.MemberExpression {
520
+ return t.memberExpression(t.cloneNode(object), t.cloneNode(property), true)
521
+ }
522
+
523
+ function numericArray(values: readonly number[]): t.ArrayExpression {
524
+ return t.arrayExpression(values.map((value) => t.numericLiteral(value)))
525
+ }
@@ -3,14 +3,11 @@ import type { NodePath, Scope, Visitor } from '@babel/traverse'
3
3
  import * as t from '@babel/types'
4
4
  import type { File } from '@babel/types'
5
5
 
6
- import { initializerReaches } from '../analysis/document-order'
7
- import { enclosingScopeHasDirectEval, isInsideWith } from '../utils/ast'
8
- import { hasConstantViolation } from '../utils/paths'
9
-
10
6
  /**
11
- * Lifts safe lexical declarations to function-scoped `var`, renaming collisions
12
- * caused by hoisting. TDZ reads, reassigned constants, captured loop bindings,
13
- * uninitialized loop heads, program scope, `with`, and direct eval are refused.
7
+ * Converts `let` and `const` declarations to `var` and renames bindings that
8
+ * would conflict after hoisting. It leaves loop declarations unchanged when
9
+ * `var` would lose a captured per-iteration binding or reuse the previous
10
+ * iteration's value.
14
11
  *
15
12
  * @example
16
13
  * // ◀️ before
@@ -33,28 +30,12 @@ export function constLetToVar(ast: File): boolean {
33
30
  if (kind !== 'let' && kind !== 'const') {
34
31
  return
35
32
  }
36
- // Babel cannot see names supplied by `with` or direct eval.
37
- if (isInsideWith(path) || enclosingScopeHasDirectEval(path)) {
38
- return
39
- }
40
- // A `var` write would lose the `const` TypeError.
41
- if (kind === 'const' && hasConstantViolation(path)) {
42
- return
43
- }
44
- // Hoisted `var` reads `undefined` where a lexical binding throws in TDZ.
45
- if (hasTdzRiskyReference(path)) {
46
- return
47
- }
48
33
  // An uninitialized lexical loop head resets on each entry. `var` does not.
49
34
  if (forHeadMissingInitializer(path)) {
50
35
  return
51
36
  }
52
- // Top-level `var` creates a global property. Lexical declarations do not.
53
37
  const blockScope = path.scope
54
38
  const varScope = blockScope.getFunctionParent() ?? blockScope.getProgramParent()
55
- if (varScope.path.isProgram()) {
56
- return
57
- }
58
39
  // One `var` cannot preserve per-iteration bindings captured by closures.
59
40
  if (capturedInEnclosingLoop(path)) {
60
41
  return
@@ -68,10 +49,6 @@ export function constLetToVar(ast: File): boolean {
68
49
  if (!id) {
69
50
  return
70
51
  }
71
- if (isInsideWith(path) || enclosingScopeHasDirectEval(path)) {
72
- return
73
- }
74
-
75
52
  const scope = path.parentPath.scope
76
53
  if (!isVarScope(scope) && scope.parent!.hasBinding(id.name, { noUids: true })) {
77
54
  path.scope.rename(id.name)
@@ -99,35 +76,6 @@ const conflictingFunctionsVisitor: Visitor<{ names: string[] }> = {
99
76
  },
100
77
  }
101
78
 
102
- // Same-scope uses need a reachable initializer. Nested uses also need their
103
- // function value created after initialization.
104
- function hasTdzRiskyReference(path: NodePath<t.VariableDeclaration>): boolean {
105
- const declHome = nearestFunctionOrProgram(path)
106
- for (const name of Object.keys(path.getBindingIdentifiers())) {
107
- const binding = path.scope.getBinding(name)
108
- if (!binding) {
109
- continue
110
- }
111
- for (const usage of [...binding.referencePaths, ...binding.constantViolations]) {
112
- if (usage.findParent((ancestor) => ancestor === path)) {
113
- return true
114
- }
115
- const usageHome = nearestFunctionOrProgram(usage)
116
- if (usageHome === declHome) {
117
- if (!initializerReaches(path, usage)) {
118
- return true
119
- }
120
- continue
121
- }
122
- const nested = outermostNestedFunction(usage, declHome)
123
- if (!nested || nested.isFunctionDeclaration() || !initializerReaches(path, nested)) {
124
- return true
125
- }
126
- }
127
- }
128
- return false
129
- }
130
-
131
79
  function forHeadMissingInitializer(path: NodePath<t.VariableDeclaration>): boolean {
132
80
  if (!t.isForStatement(path.parent) || path.key !== 'init') {
133
81
  return false
@@ -146,7 +94,11 @@ function capturedInEnclosingLoop(path: NodePath<t.VariableDeclaration>): boolean
146
94
  if (!binding) {
147
95
  continue
148
96
  }
149
- for (const usage of [...binding.referencePaths, ...binding.constantViolations]) {
97
+ const usages =
98
+ binding.kind === 'const'
99
+ ? binding.referencePaths
100
+ : [...binding.referencePaths, ...binding.constantViolations]
101
+ for (const usage of usages) {
150
102
  for (
151
103
  let current = usage.parentPath;
152
104
  current && current !== loop;
@@ -204,21 +156,6 @@ function transformBlockScopedVariable(
204
156
  })
205
157
  }
206
158
 
207
- function nearestFunctionOrProgram(path: NodePath): NodePath {
208
- return path.findParent((ancestor) => ancestor.isFunction() || ancestor.isProgram())!
209
- }
210
-
211
- // The shallowest nested function decides when this use can run.
212
- function outermostNestedFunction(usage: NodePath, home: NodePath): NodePath | null {
213
- let result: NodePath | null = null
214
- for (let current = usage.parentPath; current && current !== home; current = current.parentPath) {
215
- if (current.isFunction()) {
216
- result = current
217
- }
218
- }
219
- return result
220
- }
221
-
222
159
  function enclosingLoop(path: NodePath): NodePath<t.Loop> | null {
223
160
  for (let current = path.parentPath; current; current = current.parentPath) {
224
161
  if (current.isFunction()) {
@@ -1,15 +1,11 @@
1
1
  import type { Visitor } from '@babel/traverse'
2
2
  import type { File } from '@babel/types'
3
3
 
4
- import { enclosingScopeHasDirectEval } from '../utils/ast'
5
4
  import { traverseForChanges } from '../utils/change-tracking'
6
5
  import type { ChangeState } from '../utils/change-tracking'
7
- import { hasConstantViolation } from '../utils/paths'
8
6
 
9
7
  /**
10
- * Changes `const` to `let` when no write can observe the lost TypeError. Direct
11
- * eval and recorded constant violations keep the declaration unchanged. Scope
12
- * and TDZ behavior are otherwise identical.
8
+ * Changes `const` declarations to `let`.
13
9
  *
14
10
  * @example
15
11
  * // ◀️ before
@@ -29,9 +25,6 @@ const visitor: Visitor<ChangeState> = {
29
25
  if (path.node.kind !== 'const') {
30
26
  return
31
27
  }
32
- if (enclosingScopeHasDirectEval(path) || hasConstantViolation(path)) {
33
- return
34
- }
35
28
  path.node.kind = 'let'
36
29
  state.changed = true
37
30
  },