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