luau-obfuscator 1.0.0 → 1.0.2
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/.github/workflows/release.yml +56 -56
- package/dist/index.cjs +2186 -95
- package/dist/index.d.cts +34 -2
- package/dist/index.d.ts +34 -2
- package/dist/index.js +2186 -95
- package/generated/grammers.luau +13643 -0
- package/generated/tests.luau +5567 -0
- package/package.json +2 -2
- package/scripts/test.js +24 -8
- package/{scripts/example.luau → smoketest/grammers.luau} +1174 -1174
- package/smoketest/tests.luau +115 -0
- package/src/config.ts +2 -1
- package/src/passes/ConstantArray.ts +90 -89
- package/src/passes/EncryptNumbers.ts +122 -65
- package/src/passes/EncryptStrings.ts +162 -82
- package/src/passes/GlobalMapping.ts +193 -193
- package/src/passes/InsertJunk.ts +184 -184
- package/src/passes/Minify.ts +5 -5
- package/src/passes/NumbersToExpressions.ts +183 -2
- package/src/passes/RenameVariables.ts +2 -9
- package/src/passes/StringsToExpressions.ts +195 -6
- package/src/passes/StripTypes.ts +191 -191
- package/src/passes/Vmify.ts +87 -0
- package/src/passes/WrapInFunction.ts +31 -31
- package/src/passes/nodeFactory.ts +36 -1
- package/src/passes/vmify/chunk.ts +54 -0
- package/src/passes/vmify/compiler.ts +1026 -0
- package/src/passes/vmify/names.ts +72 -0
- package/src/passes/vmify/opcodes.ts +80 -0
- package/src/passes/vmify/registers.ts +63 -0
- package/src/passes/vmify/runtime.ts +334 -0
- package/src/passes/vmify/scope-walk.ts +175 -0
- package/src/passes/vmify/serialize.ts +68 -0
- package/src/passes/walk.ts +141 -43
- package/src/pipeline.ts +5 -6
- package/tsconfig.json +20 -20
- package/tsup.config.ts +9 -9
- package/generated/final.luau +0 -1029
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Program, Block, Statement, Expression, FunctionBody, Identifier,
|
|
3
|
+
} from "luau-parser"
|
|
4
|
+
|
|
5
|
+
/** 함수 하나(최상위 프로그램 포함)를 가리키는 식별용 토큰. 객체 identity만 씀. */
|
|
6
|
+
export type FuncMarker = { readonly tag: "top" } | { readonly tag: "fn"; readonly node: FunctionBody }
|
|
7
|
+
|
|
8
|
+
export interface EnclosingFunctionMap {
|
|
9
|
+
/** 식별자 사용/선언 노드 하나가 어느 함수 안에 있는지. */
|
|
10
|
+
ownerOf: Map<object, FuncMarker>
|
|
11
|
+
/** 함수 마커 -> 그 함수를 감싸는 바깥 함수 마커(최상위는 undefined). */
|
|
12
|
+
parentOf: Map<FuncMarker, FuncMarker | undefined>
|
|
13
|
+
topMarker: FuncMarker
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* analyzeScopes()가 주는 건 "이 식별자가 어떤 binding을 가리키는가"까지고,
|
|
18
|
+
* "그 식별자가 물리적으로 어느 함수 몸통 안에 있는가"는 안 알려주기 때문에
|
|
19
|
+
* upvalue 캡처를 판단하려면 이 정보를 직접 한 번 더 walk해서 구해야 한다.
|
|
20
|
+
* (binding 선언 위치의 함수 != 어떤 참조 위치의 함수 라면 그 참조는 upvalue를 필요로 함)
|
|
21
|
+
*/
|
|
22
|
+
export function buildEnclosingFunctionMap(program: Program): EnclosingFunctionMap {
|
|
23
|
+
const ownerOf = new Map<object, FuncMarker>()
|
|
24
|
+
const parentOf = new Map<FuncMarker, FuncMarker | undefined>()
|
|
25
|
+
const topMarker: FuncMarker = { tag: "top" }
|
|
26
|
+
parentOf.set(topMarker, undefined)
|
|
27
|
+
|
|
28
|
+
function mark(node: object, fn: FuncMarker): void {
|
|
29
|
+
ownerOf.set(node, fn)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function walkBlock(block: Block, fn: FuncMarker): void {
|
|
33
|
+
for (const stmt of block.statements) walkStatement(stmt, fn)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function walkFunctionBody(fb: FunctionBody, parent: FuncMarker): void {
|
|
37
|
+
const self: FuncMarker = { tag: "fn", node: fb }
|
|
38
|
+
parentOf.set(self, parent)
|
|
39
|
+
mark(fb, self)
|
|
40
|
+
for (const p of fb.params) mark(p, self)
|
|
41
|
+
walkBlock(fb.body, self)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function walkExpr(expr: Expression, fn: FuncMarker): void {
|
|
45
|
+
mark(expr, fn)
|
|
46
|
+
switch (expr.type) {
|
|
47
|
+
case "Identifier":
|
|
48
|
+
mark(expr, fn)
|
|
49
|
+
break
|
|
50
|
+
case "InterpolatedStringExpression":
|
|
51
|
+
for (const part of expr.parts) if (part.kind === "expression") walkExpr(part.expression, fn)
|
|
52
|
+
break
|
|
53
|
+
case "FunctionExpression":
|
|
54
|
+
walkFunctionBody(expr.func, fn)
|
|
55
|
+
break
|
|
56
|
+
case "TableExpression":
|
|
57
|
+
for (const field of expr.fields) {
|
|
58
|
+
if (field.type === "TableFieldPositional") walkExpr(field.value, fn)
|
|
59
|
+
else if (field.type === "TableFieldNamed") walkExpr(field.value, fn)
|
|
60
|
+
else {
|
|
61
|
+
walkExpr(field.key, fn)
|
|
62
|
+
walkExpr(field.value, fn)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
break
|
|
66
|
+
case "BinaryExpression":
|
|
67
|
+
walkExpr(expr.left, fn)
|
|
68
|
+
walkExpr(expr.right, fn)
|
|
69
|
+
break
|
|
70
|
+
case "UnaryExpression":
|
|
71
|
+
walkExpr(expr.argument, fn)
|
|
72
|
+
break
|
|
73
|
+
case "MemberExpression":
|
|
74
|
+
walkExpr(expr.object, fn)
|
|
75
|
+
break
|
|
76
|
+
case "IndexExpression":
|
|
77
|
+
walkExpr(expr.object, fn)
|
|
78
|
+
walkExpr(expr.index, fn)
|
|
79
|
+
break
|
|
80
|
+
case "CallExpression":
|
|
81
|
+
walkExpr(expr.callee, fn)
|
|
82
|
+
for (const a of expr.arguments) walkExpr(a, fn)
|
|
83
|
+
break
|
|
84
|
+
case "MethodCallExpression":
|
|
85
|
+
walkExpr(expr.object, fn)
|
|
86
|
+
for (const a of expr.arguments) walkExpr(a, fn)
|
|
87
|
+
break
|
|
88
|
+
case "ParenthesizedExpression":
|
|
89
|
+
case "TypeAssertionExpression":
|
|
90
|
+
walkExpr(expr.expression, fn)
|
|
91
|
+
break
|
|
92
|
+
case "IfElseExpression":
|
|
93
|
+
for (const c of expr.clauses) {
|
|
94
|
+
walkExpr(c.condition, fn)
|
|
95
|
+
walkExpr(c.body, fn)
|
|
96
|
+
}
|
|
97
|
+
walkExpr(expr.alternate, fn)
|
|
98
|
+
break
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function walkStatement(stmt: Statement, fn: FuncMarker): void {
|
|
103
|
+
mark(stmt, fn)
|
|
104
|
+
switch (stmt.type) {
|
|
105
|
+
case "LocalStatement":
|
|
106
|
+
for (const n of stmt.names) mark(n, fn)
|
|
107
|
+
for (const e of stmt.init) walkExpr(e, fn)
|
|
108
|
+
return
|
|
109
|
+
case "LocalFunctionStatement":
|
|
110
|
+
mark(stmt.name, fn)
|
|
111
|
+
walkFunctionBody(stmt.func, fn)
|
|
112
|
+
return
|
|
113
|
+
case "FunctionDeclarationStatement":
|
|
114
|
+
mark(stmt.target.base, fn)
|
|
115
|
+
walkFunctionBody(stmt.func, fn)
|
|
116
|
+
return
|
|
117
|
+
case "AssignmentStatement":
|
|
118
|
+
for (const t of stmt.targets) walkExpr(t, fn)
|
|
119
|
+
for (const v of stmt.values) walkExpr(v, fn)
|
|
120
|
+
return
|
|
121
|
+
case "CompoundAssignmentStatement":
|
|
122
|
+
walkExpr(stmt.target, fn)
|
|
123
|
+
walkExpr(stmt.value, fn)
|
|
124
|
+
return
|
|
125
|
+
case "CallStatement":
|
|
126
|
+
walkExpr(stmt.expression, fn)
|
|
127
|
+
return
|
|
128
|
+
case "DoStatement":
|
|
129
|
+
walkBlock(stmt.body, fn)
|
|
130
|
+
return
|
|
131
|
+
case "WhileStatement":
|
|
132
|
+
walkExpr(stmt.condition, fn)
|
|
133
|
+
walkBlock(stmt.body, fn)
|
|
134
|
+
return
|
|
135
|
+
case "RepeatStatement":
|
|
136
|
+
walkBlock(stmt.body, fn)
|
|
137
|
+
walkExpr(stmt.condition, fn)
|
|
138
|
+
return
|
|
139
|
+
case "IfStatement":
|
|
140
|
+
for (const c of stmt.clauses) {
|
|
141
|
+
walkExpr(c.condition, fn)
|
|
142
|
+
walkBlock(c.body, fn)
|
|
143
|
+
}
|
|
144
|
+
if (stmt.alternate) walkBlock(stmt.alternate, fn)
|
|
145
|
+
return
|
|
146
|
+
case "NumericForStatement":
|
|
147
|
+
mark(stmt.variable, fn)
|
|
148
|
+
walkExpr(stmt.start, fn)
|
|
149
|
+
walkExpr(stmt.end, fn)
|
|
150
|
+
if (stmt.step) walkExpr(stmt.step, fn)
|
|
151
|
+
walkBlock(stmt.body, fn)
|
|
152
|
+
return
|
|
153
|
+
case "GenericForStatement":
|
|
154
|
+
for (const v of stmt.variables) mark(v, fn)
|
|
155
|
+
for (const it of stmt.iterators) walkExpr(it, fn)
|
|
156
|
+
walkBlock(stmt.body, fn)
|
|
157
|
+
return
|
|
158
|
+
case "ReturnStatement":
|
|
159
|
+
for (const a of stmt.arguments) walkExpr(a, fn)
|
|
160
|
+
return
|
|
161
|
+
default:
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
walkBlock(program.body, topMarker)
|
|
167
|
+
return { ownerOf, parentOf, topMarker }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Identifier 참조 하나가 속한 함수를 찾는다(선언/참조 둘 다 이 맵에 들어있음). */
|
|
171
|
+
export function ownerFunctionOf(map: EnclosingFunctionMap, node: object): FuncMarker {
|
|
172
|
+
const fn = map.ownerOf.get(node)
|
|
173
|
+
if (!fn) throw new Error("scope-walk: node was not visited — walker/compiler are out of sync")
|
|
174
|
+
return fn
|
|
175
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Expression, TableField } from "luau-parser"
|
|
2
|
+
import type { Proto, ConstValue } from "./chunk"
|
|
3
|
+
import { Opcode } from "./opcodes"
|
|
4
|
+
import type { VmNames } from "./names"
|
|
5
|
+
import {
|
|
6
|
+
table, positionalField, namedField, numberLiteral, vmNumberLiteral, stringLiteral, booleanLiteral, nilLiteral,
|
|
7
|
+
} from "../nodeFactory"
|
|
8
|
+
|
|
9
|
+
/** proto.consts[]를 Luau 배열 리터럴로: { [1] = v1, [2] = v2, ... } (1-based는 그대로 유지) */
|
|
10
|
+
function serializeConsts(consts: ConstValue[]): Expression {
|
|
11
|
+
const fields: TableField[] = consts.map((v) => positionalField(serializeConstValue(v)))
|
|
12
|
+
return table(fields)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function serializeConstValue(v: ConstValue): Expression {
|
|
16
|
+
if (v === null) return nilLiteral()
|
|
17
|
+
if (typeof v === "string") return stringLiteral(v)
|
|
18
|
+
if (typeof v === "number") return numberLiteral(v)
|
|
19
|
+
return booleanLiteral(v)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Instr/Proto를 Luau 테이블 리터럴로 찍을 때 쓰는 필드 키는 고정 문자열("op","a","b","c",
|
|
24
|
+
* "numParams", ...)이 아니라 names(names.ts)에서 빌드마다 새로 뽑은 이름을 쓴다 —
|
|
25
|
+
* 이 키 이름이 고정이면 산출물에서 `{op=6, a=0, ...}` 같은 패턴만 grep해도 VM 청크
|
|
26
|
+
* 테이블을 바로 찾아낼 수 있기 때문. runtime.ts가 같은 names로 instr.<key>를 읽으므로
|
|
27
|
+
* 반드시 컴파일 시점에 쓴 것과 동일한 VmNames 인스턴스를 넘겨야 한다.
|
|
28
|
+
*/
|
|
29
|
+
function serializeInstrs(proto: Proto): Expression {
|
|
30
|
+
const fields: TableField[] = proto.code.map((instr) =>
|
|
31
|
+
positionalField(
|
|
32
|
+
table([
|
|
33
|
+
positionalField(vmNumberLiteral(instr.op as number)),
|
|
34
|
+
positionalField(vmNumberLiteral(instr.a)),
|
|
35
|
+
positionalField(vmNumberLiteral(instr.b)),
|
|
36
|
+
positionalField(vmNumberLiteral(instr.c)),
|
|
37
|
+
]),
|
|
38
|
+
))
|
|
39
|
+
return table(fields)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function serializeUpvalDescs(proto: Proto, names: VmNames): Expression {
|
|
43
|
+
const fields: TableField[] = proto.upvalDescs.map((d) =>
|
|
44
|
+
positionalField(
|
|
45
|
+
table([
|
|
46
|
+
namedField(names.kind, stringLiteral(d.kind)),
|
|
47
|
+
namedField(names.index, vmNumberLiteral(d.index)),
|
|
48
|
+
]),
|
|
49
|
+
))
|
|
50
|
+
return table(fields)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Proto 하나를 { numParams=.., hasVarargs=.., maxRegs=.., code={...}, consts={...}, upvalDescs={...}, protos={...} }로
|
|
54
|
+
* (필드 키는 전부 names에서 온 무작위 이름). */
|
|
55
|
+
export function serializeProto(proto: Proto, names: VmNames): Expression {
|
|
56
|
+
const fields: TableField[] = [
|
|
57
|
+
namedField(names.numParams, vmNumberLiteral(proto.numParams)),
|
|
58
|
+
namedField(names.hasVarargs, booleanLiteral(proto.hasVarargs)),
|
|
59
|
+
namedField(names.maxRegs, vmNumberLiteral(proto.maxRegs)),
|
|
60
|
+
namedField(names.code, serializeInstrs(proto)),
|
|
61
|
+
namedField(names.consts, serializeConsts(proto.consts)),
|
|
62
|
+
namedField(names.upvalDescs, serializeUpvalDescs(proto, names)),
|
|
63
|
+
namedField(names.protos, table(proto.protos.map((p) => positionalField(serializeProto(p, names))))),
|
|
64
|
+
]
|
|
65
|
+
return table(fields)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { Opcode }
|
package/src/passes/walk.ts
CHANGED
|
@@ -10,79 +10,177 @@ function visitArray(arr: Expression[], visitor: ExpressionVisitor): void {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
interface ChildAccessor {
|
|
14
|
+
get(): Expression
|
|
15
|
+
set(v: Expression): void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* expr의 "자식 Expression"들에 대한 getter/setter 목록을 반환한다.
|
|
20
|
+
* (FunctionExpression은 별도 처리하므로 여기서는 빈 배열을 반환한다.)
|
|
21
|
+
*/
|
|
22
|
+
function getExpressionChildren(expr: Expression): ChildAccessor[] {
|
|
14
23
|
switch (expr.type) {
|
|
15
|
-
case "InterpolatedStringExpression":
|
|
24
|
+
case "InterpolatedStringExpression": {
|
|
25
|
+
const children: ChildAccessor[] = []
|
|
16
26
|
for (const part of expr.parts) {
|
|
17
27
|
if (part.kind === "expression") {
|
|
18
|
-
|
|
28
|
+
children.push({
|
|
29
|
+
get: () => part.expression,
|
|
30
|
+
set: (v) => { part.expression = v },
|
|
31
|
+
})
|
|
19
32
|
}
|
|
20
33
|
}
|
|
21
|
-
|
|
34
|
+
return children
|
|
35
|
+
}
|
|
22
36
|
|
|
23
37
|
case "FunctionExpression":
|
|
24
|
-
|
|
25
|
-
break
|
|
38
|
+
return []
|
|
26
39
|
|
|
27
|
-
case "TableExpression":
|
|
40
|
+
case "TableExpression": {
|
|
41
|
+
const children: ChildAccessor[] = []
|
|
28
42
|
for (const field of expr.fields) {
|
|
29
43
|
if (field.type === "TableFieldPositional") {
|
|
30
|
-
field.value
|
|
44
|
+
children.push({ get: () => field.value, set: (v) => { field.value = v } })
|
|
31
45
|
} else if (field.type === "TableFieldNamed") {
|
|
32
|
-
field.value
|
|
46
|
+
children.push({ get: () => field.value, set: (v) => { field.value = v } })
|
|
33
47
|
} else if (field.type === "TableFieldComputed") {
|
|
34
|
-
field.key
|
|
35
|
-
field.value
|
|
48
|
+
children.push({ get: () => field.key, set: (v) => { field.key = v } })
|
|
49
|
+
children.push({ get: () => field.value, set: (v) => { field.value = v } })
|
|
36
50
|
}
|
|
37
51
|
}
|
|
38
|
-
|
|
52
|
+
return children
|
|
53
|
+
}
|
|
39
54
|
|
|
40
55
|
case "BinaryExpression":
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
return [
|
|
57
|
+
{ get: () => expr.left, set: (v) => { expr.left = v } },
|
|
58
|
+
{ get: () => expr.right, set: (v) => { expr.right = v } },
|
|
59
|
+
]
|
|
44
60
|
|
|
45
61
|
case "UnaryExpression":
|
|
46
|
-
expr.argument
|
|
47
|
-
break
|
|
62
|
+
return [{ get: () => expr.argument, set: (v) => { expr.argument = v } }]
|
|
48
63
|
|
|
49
64
|
case "MemberExpression":
|
|
50
|
-
expr.object
|
|
51
|
-
break
|
|
65
|
+
return [{ get: () => expr.object, set: (v) => { expr.object = v } }]
|
|
52
66
|
|
|
53
67
|
case "IndexExpression":
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
return [
|
|
69
|
+
{ get: () => expr.object, set: (v) => { expr.object = v } },
|
|
70
|
+
{ get: () => expr.index, set: (v) => { expr.index = v } },
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
case "CallExpression": {
|
|
74
|
+
const children: ChildAccessor[] = [
|
|
75
|
+
{ get: () => expr.callee, set: (v) => { expr.callee = v } },
|
|
76
|
+
]
|
|
77
|
+
for (let i = 0; i < expr.arguments.length; i++) {
|
|
78
|
+
const idx = i
|
|
79
|
+
children.push({
|
|
80
|
+
get: () => expr.arguments[idx],
|
|
81
|
+
set: (v) => { expr.arguments[idx] = v },
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
return children
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case "MethodCallExpression": {
|
|
88
|
+
const children: ChildAccessor[] = [
|
|
89
|
+
{ get: () => expr.object, set: (v) => { expr.object = v } },
|
|
90
|
+
]
|
|
91
|
+
for (let i = 0; i < expr.arguments.length; i++) {
|
|
92
|
+
const idx = i
|
|
93
|
+
children.push({
|
|
94
|
+
get: () => expr.arguments[idx],
|
|
95
|
+
set: (v) => { expr.arguments[idx] = v },
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
return children
|
|
99
|
+
}
|
|
67
100
|
|
|
68
101
|
case "ParenthesizedExpression":
|
|
69
|
-
expr.expression
|
|
70
|
-
break
|
|
102
|
+
return [{ get: () => expr.expression, set: (v) => { expr.expression = v } }]
|
|
71
103
|
|
|
72
104
|
case "TypeAssertionExpression":
|
|
73
|
-
expr.expression
|
|
74
|
-
break
|
|
105
|
+
return [{ get: () => expr.expression, set: (v) => { expr.expression = v } }]
|
|
75
106
|
|
|
76
|
-
case "IfElseExpression":
|
|
107
|
+
case "IfElseExpression": {
|
|
108
|
+
const children: ChildAccessor[] = []
|
|
77
109
|
for (const clause of expr.clauses) {
|
|
78
|
-
clause.condition
|
|
79
|
-
clause.body
|
|
110
|
+
children.push({ get: () => clause.condition, set: (v) => { clause.condition = v } })
|
|
111
|
+
children.push({ get: () => clause.body, set: (v) => { clause.body = v } })
|
|
112
|
+
}
|
|
113
|
+
children.push({ get: () => expr.alternate, set: (v) => { expr.alternate = v } })
|
|
114
|
+
return children
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
default:
|
|
118
|
+
return []
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface WalkFrame {
|
|
123
|
+
expr: Expression
|
|
124
|
+
children: ChildAccessor[]
|
|
125
|
+
index: number
|
|
126
|
+
setInParent: ((v: Expression) => void) | null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Expression 트리를 후위순회(post-order)로 변형한다.
|
|
131
|
+
*
|
|
132
|
+
* 원래는 재귀 함수였지만, 문자열/숫자 난독화 패스가 만들어내는 매우 깊은
|
|
133
|
+
* BinaryExpression 체인(예: 수천 개의 `..` concat)을 순회할 때 네이티브
|
|
134
|
+
* 콜스택이 트리 깊이만큼 쌓여 "Maximum call stack size exceeded"가
|
|
135
|
+
* 발생했다. 그래서 재귀 대신 힙에 할당되는 명시적 스택을 사용해
|
|
136
|
+
* 트리 깊이와 무관하게 동작하도록 구현했다.
|
|
137
|
+
*/
|
|
138
|
+
export function mapExpression(root: Expression, visitor: ExpressionVisitor): Expression {
|
|
139
|
+
// FunctionExpression은 본문(statement 트리)을 별도로 순회해야 하므로 특별 취급한다.
|
|
140
|
+
// 함수 중첩 깊이는 보통 얕아서(재귀해도 안전) 그대로 재귀를 사용한다.
|
|
141
|
+
if (root.type === "FunctionExpression") {
|
|
142
|
+
walkFunctionBody(root.func, visitor)
|
|
143
|
+
return visitor(root) ?? root
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let result: Expression = root
|
|
147
|
+
const stack: WalkFrame[] = [{
|
|
148
|
+
expr: root,
|
|
149
|
+
children: getExpressionChildren(root),
|
|
150
|
+
index: 0,
|
|
151
|
+
setInParent: (v) => { result = v },
|
|
152
|
+
}]
|
|
153
|
+
|
|
154
|
+
while (stack.length > 0) {
|
|
155
|
+
const frame = stack[stack.length - 1]
|
|
156
|
+
|
|
157
|
+
if (frame.index < frame.children.length) {
|
|
158
|
+
const child = frame.children[frame.index]
|
|
159
|
+
frame.index++
|
|
160
|
+
const childExpr = child.get()
|
|
161
|
+
|
|
162
|
+
if (childExpr.type === "FunctionExpression") {
|
|
163
|
+
walkFunctionBody(childExpr.func, visitor)
|
|
164
|
+
child.set(visitor(childExpr) ?? childExpr)
|
|
165
|
+
continue
|
|
80
166
|
}
|
|
81
|
-
|
|
82
|
-
|
|
167
|
+
|
|
168
|
+
stack.push({
|
|
169
|
+
expr: childExpr,
|
|
170
|
+
children: getExpressionChildren(childExpr),
|
|
171
|
+
index: 0,
|
|
172
|
+
setInParent: child.set,
|
|
173
|
+
})
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 모든 자식 처리가 끝났으면 visitor를 실행하고, 부모에 반영한 뒤 pop한다.
|
|
178
|
+
const finished = visitor(frame.expr) ?? frame.expr
|
|
179
|
+
frame.setInParent?.(finished)
|
|
180
|
+
stack.pop()
|
|
83
181
|
}
|
|
84
182
|
|
|
85
|
-
return
|
|
183
|
+
return result
|
|
86
184
|
}
|
|
87
185
|
|
|
88
186
|
function walkFunctionBody(func: FunctionBody, visitor: ExpressionVisitor): void {
|
|
@@ -170,4 +268,4 @@ function walkStatement(stmt: Statement, visitor: ExpressionVisitor): void {
|
|
|
170
268
|
/** Program 전체를 순회하며 모든 Expression 노드에 visitor를 적용(제자리 변형). */
|
|
171
269
|
export function transformExpressions(program: Program, visitor: ExpressionVisitor): void {
|
|
172
270
|
walkBlock(program.body, visitor)
|
|
173
|
-
}
|
|
271
|
+
}
|
package/src/pipeline.ts
CHANGED
|
@@ -10,19 +10,20 @@ import { runEncryptStrings } from "./passes/EncryptStrings"
|
|
|
10
10
|
import { runEncryptNumbers } from "./passes/EncryptNumbers"
|
|
11
11
|
import { runInsertJunk } from "./passes/InsertJunk"
|
|
12
12
|
import { runWrapInFunction } from "./passes/WrapInFunction"
|
|
13
|
+
import { runVmify } from "./passes/Vmify"
|
|
13
14
|
|
|
14
15
|
type PassFn = (program: Program, options: any) => void
|
|
15
16
|
|
|
16
17
|
export const PASS_ORDER: (keyof ObfuscateConfig)[] = [
|
|
18
|
+
"Vmify",
|
|
17
19
|
"GlobalMapping",
|
|
18
|
-
"StringsToExpressions",
|
|
19
|
-
"NumbersToExpressions",
|
|
20
20
|
"RenameVariables",
|
|
21
21
|
"ConstantArray",
|
|
22
|
+
"StringsToExpressions",
|
|
23
|
+
"NumbersToExpressions",
|
|
22
24
|
"EncryptStrings",
|
|
23
25
|
"EncryptNumbers",
|
|
24
26
|
"InsertJunk",
|
|
25
|
-
"Vmify",
|
|
26
27
|
"WrapInFunction",
|
|
27
28
|
"Minify",
|
|
28
29
|
]
|
|
@@ -37,12 +38,10 @@ export const PASS_MAP: Partial<Record<keyof ObfuscateConfig, PassFn>> = {
|
|
|
37
38
|
EncryptNumbers: runEncryptNumbers,
|
|
38
39
|
InsertJunk: runInsertJunk,
|
|
39
40
|
WrapInFunction: runWrapInFunction,
|
|
41
|
+
Vmify: runVmify,
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export function runPipeline(program: Program, config: ObfuscateConfig): void {
|
|
43
|
-
// 타입 주석/별칭은 값 트리와 독립적으로 원본 이름을 참조하고 있어서
|
|
44
|
-
// (예: typeof(x)) 어떤 pass보다도 먼저 지워야 함. 끄면 무조건 깨지므로
|
|
45
|
-
// config로 조절 가능한 옵션이 아니라 파이프라인 진입 시 항상 실행.
|
|
46
45
|
runStripTypes(program)
|
|
47
46
|
|
|
48
47
|
for (const key of PASS_ORDER) {
|
package/tsconfig.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ESNext",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
6
|
-
"declaration": false,
|
|
7
|
-
"declarationMap": false,
|
|
8
|
-
"sourceMap": false,
|
|
9
|
-
"outDir": "./dist",
|
|
10
|
-
"rootDir": "./src",
|
|
11
|
-
"moduleResolution": "bundler",
|
|
12
|
-
"allowSyntheticDefaultImports": true,
|
|
13
|
-
"esModuleInterop": true,
|
|
14
|
-
"forceConsistentCasingInFileNames": true,
|
|
15
|
-
"strict": true,
|
|
16
|
-
"skipLibCheck": true,
|
|
17
|
-
"stripInternal": true,
|
|
18
|
-
},
|
|
19
|
-
"include": ["src/**/*"],
|
|
20
|
-
"exclude": ["node_modules", "dist"]
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
6
|
+
"declaration": false,
|
|
7
|
+
"declarationMap": false,
|
|
8
|
+
"sourceMap": false,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "./src",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowSyntheticDefaultImports": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"strict": true,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"stripInternal": true,
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*"],
|
|
20
|
+
"exclude": ["node_modules", "dist"]
|
|
21
21
|
}
|
package/tsup.config.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { defineConfig } from "tsup";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
entry: ["src/index.ts"],
|
|
5
|
-
format: ["esm", "cjs"],
|
|
6
|
-
dts: true,
|
|
7
|
-
clean: true,
|
|
8
|
-
sourcemap: false,
|
|
9
|
-
target: "esnext",
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ["src/index.ts"],
|
|
5
|
+
format: ["esm", "cjs"],
|
|
6
|
+
dts: true,
|
|
7
|
+
clean: true,
|
|
8
|
+
sourcemap: false,
|
|
9
|
+
target: "esnext",
|
|
10
10
|
});
|