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
|
@@ -1,18 +1,36 @@
|
|
|
1
|
-
import type { Program, Expression } from "luau-parser"
|
|
1
|
+
import type { Program, Expression, Statement } from "luau-parser"
|
|
2
2
|
import { transformExpressions } from "./walk"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
numberLiteral, binary, call, member, identifier, table, positionalField, index,
|
|
5
|
+
localStatement, localFunctionStatement, functionParam, functionBody, functionExpression,
|
|
6
|
+
block, ifStatement, ifClause, returnStatement, paren, unary, stringLiteral,
|
|
7
|
+
numericForStatement, assignmentStatement,
|
|
8
|
+
} from "./nodeFactory"
|
|
4
9
|
|
|
5
10
|
export interface StringsToExpressionsOptions {
|
|
6
11
|
/** 청크 하나에 들어가는 바이트 개수 최소값 */
|
|
7
12
|
min: number
|
|
8
13
|
/** 청크 하나에 들어가는 바이트 개수 최대값 */
|
|
9
14
|
max: number
|
|
15
|
+
/**
|
|
16
|
+
* 0~1. 청크를 소스 순서 그대로 이어붙이는 대신, 순서를 섞어 저장해두고
|
|
17
|
+
* for 루프나 재귀 함수로 원래 순서를 되짚어가며 런타임에 조립할 확률.
|
|
18
|
+
* 소스에 보이는 조각 순서와 실제 결합 순서가 달라지므로, 단순 텍스트 상의
|
|
19
|
+
* concat 체인 스캔만으로는 원문을 복원할 수 없다. 기본값 0.6.
|
|
20
|
+
*/
|
|
21
|
+
controlFlowProbability?: number
|
|
10
22
|
}
|
|
11
23
|
|
|
24
|
+
const CONTROL_FLOW_PROBABILITY_DEFAULT = 0.6
|
|
25
|
+
|
|
12
26
|
function randomInt(min: number, max: number): number {
|
|
13
27
|
return Math.floor(Math.random() * (max - min + 1)) + min
|
|
14
28
|
}
|
|
15
29
|
|
|
30
|
+
function randomName(): string {
|
|
31
|
+
return "_" + globalThis.crypto.randomUUID().replace(/-/g, "")
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
/** Luau 소스는 UTF-8 바이트 스트림이므로 charCodeAt이 아니라 실제 UTF-8 바이트로 변환.
|
|
17
35
|
* Buffer(Node 전용) 대신 표준 TextEncoder 사용 — 브라우저/번들러 환경에서도 동작. */
|
|
18
36
|
function toUtf8Bytes(value: string): number[] {
|
|
@@ -35,22 +53,193 @@ function stringCharCall(bytes: number[]): Expression {
|
|
|
35
53
|
return call(member(identifier("string"), "char"), bytes.map(numberLiteral))
|
|
36
54
|
}
|
|
37
55
|
|
|
56
|
+
/**
|
|
57
|
+
* 청크를 소스에 보이는 순서 그대로 ".."로 이어붙인다.
|
|
58
|
+
*
|
|
59
|
+
* 예전에는 왼쪽으로만 쌓는 선형 체인(`(((a..b)..c)..d)...`)이었는데, 이러면
|
|
60
|
+
* depth == chunks.length라서 큰 문자열 리터럴(수백만 바이트) 하나만 있어도
|
|
61
|
+
* 표현식 중첩 깊이가 수백만까지 치솟는다. 이 트랜스파일러 자체의 스택뿐 아니라
|
|
62
|
+
* 실제 Luau/Lua 컴파일러도 문법 중첩 레벨에 제한(대략 200선)이 있어서, 그 한계를
|
|
63
|
+
* 넘는 순간 우리 쪽이 아니라 Roblox/Luau 파서에서 컴파일이 실패한다.
|
|
64
|
+
* 그래서 균형 이진 트리로 묶어 depth를 O(log n)으로 낮춘다 — 결합 순서만 바뀔 뿐
|
|
65
|
+
* ".."는 결합법칙이 성립하므로 결과 문자열은 동일하다.
|
|
66
|
+
*/
|
|
38
67
|
function buildConcatChain(chunks: number[][]): Expression {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
function build(lo: number, hi: number): Expression {
|
|
69
|
+
if (hi - lo === 1) return stringCharCall(chunks[lo])
|
|
70
|
+
const mid = lo + Math.floor((hi - lo) / 2)
|
|
71
|
+
return binary("..", build(lo, mid), build(mid, hi))
|
|
72
|
+
}
|
|
73
|
+
return build(0, chunks.length)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** [0, n)의 무작위 순열 (Fisher-Yates) */
|
|
77
|
+
function shuffledIndices(n: number): number[] {
|
|
78
|
+
const arr = Array.from({ length: n }, (_, i) => i)
|
|
79
|
+
for (let i = n - 1; i > 0; i--) {
|
|
80
|
+
const j = randomInt(0, i)
|
|
81
|
+
;[arr[i], arr[j]] = [arr[j], arr[i]]
|
|
82
|
+
}
|
|
83
|
+
return arr
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** (function() ... end)() — 문(statement)들을 하나의 표현식으로 감싸는 즉시실행 함수. */
|
|
87
|
+
function iife(statements: Statement[]): Expression {
|
|
88
|
+
return call(paren(functionExpression(functionBody([], block(statements)))), [])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 청크들을 뒤섞어 store 테이블에 저장하고, 원래 순서를 가리키는 order 테이블을 따로 둔다.
|
|
93
|
+
* store에 나타나는 물리적 순서 == 원문 순서가 아니므로, 소스를 눈으로 읽거나
|
|
94
|
+
* 텍스트 패턴만으로 concat해서는 원문이 나오지 않는다 — order를 통해 런타임에
|
|
95
|
+
* 역참조해야만 올바른 순서가 나온다.
|
|
96
|
+
*/
|
|
97
|
+
function buildShuffledTables(chunks: number[][]): { storeVar: string; orderVar: string; stmts: Statement[] } {
|
|
98
|
+
const n = chunks.length
|
|
99
|
+
const storageOrder = shuffledIndices(n) // storageOrder[slot] = 원본 청크 인덱스
|
|
100
|
+
const storeVar = randomName()
|
|
101
|
+
const orderVar = randomName()
|
|
102
|
+
|
|
103
|
+
const storeFields = storageOrder.map((originalIdx) => positionalField(stringCharCall(chunks[originalIdx])))
|
|
104
|
+
|
|
105
|
+
// order[originalIdx] = slot(1-based) — 원본 위치 k에 해당하는 조각이 store의 몇 번째에 있는지
|
|
106
|
+
const order = new Array<number>(n)
|
|
107
|
+
for (let slot = 0; slot < n; slot++) {
|
|
108
|
+
order[storageOrder[slot]] = slot + 1
|
|
109
|
+
}
|
|
110
|
+
const orderFields = order.map((slot) => positionalField(numberLiteral(slot)))
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
storeVar,
|
|
114
|
+
orderVar,
|
|
115
|
+
stmts: [
|
|
116
|
+
localStatement(storeVar, table(storeFields)),
|
|
117
|
+
localStatement(orderVar, table(orderFields)),
|
|
118
|
+
],
|
|
42
119
|
}
|
|
43
|
-
return expr
|
|
44
120
|
}
|
|
45
121
|
|
|
122
|
+
/**
|
|
123
|
+
* (function()
|
|
124
|
+
* local store = { ... 뒤섞인 순서의 string.char(...) 조각들 ... }
|
|
125
|
+
* local order = { ... }
|
|
126
|
+
* local acc = ""
|
|
127
|
+
* for i = 1, #order do
|
|
128
|
+
* acc = acc .. store[order[i]]
|
|
129
|
+
* end
|
|
130
|
+
* return acc
|
|
131
|
+
* end)()
|
|
132
|
+
*/
|
|
133
|
+
function buildForLoopStringExpr(chunks: number[][]): Expression {
|
|
134
|
+
const { storeVar, orderVar, stmts } = buildShuffledTables(chunks)
|
|
135
|
+
const accVar = randomName()
|
|
136
|
+
const iVar = randomName()
|
|
137
|
+
|
|
138
|
+
return iife([
|
|
139
|
+
...stmts,
|
|
140
|
+
localStatement(accVar, stringLiteral("")),
|
|
141
|
+
numericForStatement(
|
|
142
|
+
iVar,
|
|
143
|
+
numberLiteral(1),
|
|
144
|
+
unary("#", identifier(orderVar)),
|
|
145
|
+
block([
|
|
146
|
+
assignmentStatement(
|
|
147
|
+
[identifier(accVar)],
|
|
148
|
+
[
|
|
149
|
+
binary(
|
|
150
|
+
"..",
|
|
151
|
+
identifier(accVar),
|
|
152
|
+
index(identifier(storeVar), index(identifier(orderVar), identifier(iVar))),
|
|
153
|
+
),
|
|
154
|
+
],
|
|
155
|
+
),
|
|
156
|
+
]),
|
|
157
|
+
),
|
|
158
|
+
returnStatement([identifier(accVar)]),
|
|
159
|
+
])
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* (function()
|
|
164
|
+
* local store = { ... }
|
|
165
|
+
* local order = { ... }
|
|
166
|
+
* local function build(i, acc)
|
|
167
|
+
* if i > #order then
|
|
168
|
+
* return acc
|
|
169
|
+
* end
|
|
170
|
+
* return build(i + 1, acc .. store[order[i]])
|
|
171
|
+
* end
|
|
172
|
+
* return build(1, "")
|
|
173
|
+
* end)()
|
|
174
|
+
*/
|
|
175
|
+
function buildRecursiveStringExpr(chunks: number[][]): Expression {
|
|
176
|
+
const { storeVar, orderVar, stmts } = buildShuffledTables(chunks)
|
|
177
|
+
const recName = randomName()
|
|
178
|
+
const iParam = randomName()
|
|
179
|
+
const accParam = randomName()
|
|
180
|
+
|
|
181
|
+
const body = block([
|
|
182
|
+
ifStatement([
|
|
183
|
+
ifClause(
|
|
184
|
+
binary(">", identifier(iParam), unary("#", identifier(orderVar))),
|
|
185
|
+
block([returnStatement([identifier(accParam)])]),
|
|
186
|
+
),
|
|
187
|
+
]),
|
|
188
|
+
returnStatement([
|
|
189
|
+
call(identifier(recName), [
|
|
190
|
+
binary("+", identifier(iParam), numberLiteral(1)),
|
|
191
|
+
binary(
|
|
192
|
+
"..",
|
|
193
|
+
identifier(accParam),
|
|
194
|
+
index(identifier(storeVar), index(identifier(orderVar), identifier(iParam))),
|
|
195
|
+
),
|
|
196
|
+
]),
|
|
197
|
+
]),
|
|
198
|
+
])
|
|
199
|
+
|
|
200
|
+
return iife([
|
|
201
|
+
...stmts,
|
|
202
|
+
localFunctionStatement(recName, functionBody([functionParam(iParam), functionParam(accParam)], body)),
|
|
203
|
+
returnStatement([call(identifier(recName), [numberLiteral(1), stringLiteral("")])]),
|
|
204
|
+
])
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
type ControlFlowStrategy = "forLoop" | "recursive"
|
|
208
|
+
|
|
209
|
+
function pickControlFlowStrategy(): ControlFlowStrategy {
|
|
210
|
+
return Math.random() < 0.5 ? "forLoop" : "recursive"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 바이트 단위로 쪼개면 청크 하나당 AST 노드(CallExpression+MemberExpression+인자들)가
|
|
214
|
+
// 하나씩 생긴다. 수 MB짜리 문자열 리터럴(예: 임베드된 데이터 블롭)을 5~10바이트 청크로
|
|
215
|
+
// 쪼개면 수십~수백만 개 노드가 나와서 이 트랜스파일러 자체가 OOM으로 죽는다.
|
|
216
|
+
// 그 크기의 문자열은 바이트 단위로 흩뿌려봐야 어차피 노출 표면이 커서 난독화 효과도
|
|
217
|
+
// 떨어지므로, 청크 개수가 이 상한을 넘으면 통째로 하나의 string.char(...)/원본 리터럴로
|
|
218
|
+
// 두고 건드리지 않는다 (EncryptStrings 등 다른 패스가 필요하면 별도로 암호화하면 됨).
|
|
219
|
+
const MAX_CHUNKS_PER_STRING = 20000
|
|
220
|
+
|
|
46
221
|
export function runStringsToExpressions(program: Program, options: StringsToExpressionsOptions): void {
|
|
222
|
+
const controlFlowProbability = options.controlFlowProbability ?? CONTROL_FLOW_PROBABILITY_DEFAULT
|
|
223
|
+
|
|
47
224
|
transformExpressions(program, (expr) => {
|
|
48
225
|
if (expr.type !== "StringLiteral") return
|
|
49
226
|
if (expr.value.length === 0) return
|
|
50
227
|
|
|
51
228
|
const bytes = toUtf8Bytes(expr.value)
|
|
229
|
+
|
|
230
|
+
// 너무 큰 문자열은 청크 단위 폭발을 피하기 위해 손대지 않는다.
|
|
231
|
+
const estimatedChunks = Math.ceil(bytes.length / Math.max(1, options.min))
|
|
232
|
+
if (estimatedChunks > MAX_CHUNKS_PER_STRING) return
|
|
233
|
+
|
|
52
234
|
const chunks = splitByteChunks(bytes, options.min, options.max)
|
|
53
235
|
|
|
236
|
+
// 청크가 하나뿐이면 순서를 흩뜨려도 얻는 게 없으니 그냥 원래 방식대로.
|
|
237
|
+
if (chunks.length > 1 && Math.random() < controlFlowProbability) {
|
|
238
|
+
return pickControlFlowStrategy() === "forLoop"
|
|
239
|
+
? buildForLoopStringExpr(chunks)
|
|
240
|
+
: buildRecursiveStringExpr(chunks)
|
|
241
|
+
}
|
|
242
|
+
|
|
54
243
|
return buildConcatChain(chunks)
|
|
55
244
|
})
|
|
56
245
|
}
|
package/src/passes/StripTypes.ts
CHANGED
|
@@ -1,192 +1,192 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Program, Block, Statement, Expression, FunctionBody,
|
|
3
|
-
} from "luau-parser"
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 파이프라인 맨 앞에서 실행되어야 하는 패스.
|
|
7
|
-
*
|
|
8
|
-
* 이후 패스들(GlobalMapping, RenameVariables 등)은 값 트리만 순회하고
|
|
9
|
-
* `typeof(x)`, `x :: T` 같은 타입 노드 내부의 expression은 건드리지 않는다.
|
|
10
|
-
* 타입을 나중까지 들고 있으면 값 쪽 식별자는 바뀌는데 타입 쪽 참조는 원래
|
|
11
|
-
* 이름 그대로 남아 출력물이 타입체크 불가능한 상태가 된다
|
|
12
|
-
* (`typeof(runtimeValue)`가 obfuscate 이후에도 원본 이름을 참조하는 것이 그 예).
|
|
13
|
-
*
|
|
14
|
-
* 그래서 타입 정보 자체를 파이프라인 시작 시점에 전부 지워서 이 클래스의
|
|
15
|
-
* 버그를 원천 차단한다:
|
|
16
|
-
* - TypeAliasStatement / ExportTypeAliasStatement 문 자체를 블록에서 제거
|
|
17
|
-
* - TypedIdentifier / FunctionParameter 의 typeAnnotation 제거
|
|
18
|
-
* - FunctionBody 의 generics / varargTypeAnnotation / returnType 제거
|
|
19
|
-
* - `expr :: T` (TypeAssertionExpression) 은 T를 버리고 expr로 치환
|
|
20
|
-
*/
|
|
21
|
-
export function runStripTypes(program: Program): void {
|
|
22
|
-
stripBlock(program.body)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function stripBlock(block: Block): void {
|
|
26
|
-
const kept: Statement[] = []
|
|
27
|
-
for (const stmt of block.statements) {
|
|
28
|
-
if (stmt.type === "TypeAliasStatement" || stmt.type === "ExportTypeAliasStatement") {
|
|
29
|
-
continue
|
|
30
|
-
}
|
|
31
|
-
stripStatement(stmt)
|
|
32
|
-
kept.push(stmt)
|
|
33
|
-
}
|
|
34
|
-
block.statements = kept
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function stripFunctionBody(func: FunctionBody): void {
|
|
38
|
-
func.generics = []
|
|
39
|
-
for (const param of func.params) delete param.typeAnnotation
|
|
40
|
-
delete func.varargTypeAnnotation
|
|
41
|
-
delete func.returnType
|
|
42
|
-
stripBlock(func.body)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function stripStatement(stmt: Statement): void {
|
|
46
|
-
switch (stmt.type) {
|
|
47
|
-
case "LocalStatement":
|
|
48
|
-
for (const name of stmt.names) delete name.typeAnnotation
|
|
49
|
-
for (let i = 0; i < stmt.init.length; i++) stmt.init[i] = stripExpr(stmt.init[i])
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
case "LocalFunctionStatement":
|
|
53
|
-
stripFunctionBody(stmt.func)
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
case "FunctionDeclarationStatement":
|
|
57
|
-
stripFunctionBody(stmt.func)
|
|
58
|
-
return
|
|
59
|
-
|
|
60
|
-
case "AssignmentStatement":
|
|
61
|
-
for (let i = 0; i < stmt.targets.length; i++) stmt.targets[i] = stripExpr(stmt.targets[i])
|
|
62
|
-
for (let i = 0; i < stmt.values.length; i++) stmt.values[i] = stripExpr(stmt.values[i])
|
|
63
|
-
return
|
|
64
|
-
|
|
65
|
-
case "CompoundAssignmentStatement":
|
|
66
|
-
stmt.target = stripExpr(stmt.target)
|
|
67
|
-
stmt.value = stripExpr(stmt.value)
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
case "CallStatement":
|
|
71
|
-
stmt.expression = stripExpr(stmt.expression) as typeof stmt.expression
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
case "DoStatement":
|
|
75
|
-
stripBlock(stmt.body)
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
case "WhileStatement":
|
|
79
|
-
stmt.condition = stripExpr(stmt.condition)
|
|
80
|
-
stripBlock(stmt.body)
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
case "RepeatStatement":
|
|
84
|
-
stripBlock(stmt.body)
|
|
85
|
-
stmt.condition = stripExpr(stmt.condition)
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
case "IfStatement":
|
|
89
|
-
for (const clause of stmt.clauses) {
|
|
90
|
-
clause.condition = stripExpr(clause.condition)
|
|
91
|
-
stripBlock(clause.body)
|
|
92
|
-
}
|
|
93
|
-
if (stmt.alternate) stripBlock(stmt.alternate)
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
case "NumericForStatement":
|
|
97
|
-
delete stmt.variable.typeAnnotation
|
|
98
|
-
stmt.start = stripExpr(stmt.start)
|
|
99
|
-
stmt.end = stripExpr(stmt.end)
|
|
100
|
-
if (stmt.step) stmt.step = stripExpr(stmt.step)
|
|
101
|
-
stripBlock(stmt.body)
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
case "GenericForStatement":
|
|
105
|
-
for (const v of stmt.variables) delete v.typeAnnotation
|
|
106
|
-
for (let i = 0; i < stmt.iterators.length; i++) stmt.iterators[i] = stripExpr(stmt.iterators[i])
|
|
107
|
-
stripBlock(stmt.body)
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
case "ReturnStatement":
|
|
111
|
-
for (let i = 0; i < stmt.arguments.length; i++) stmt.arguments[i] = stripExpr(stmt.arguments[i])
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
case "BreakStatement":
|
|
115
|
-
case "ContinueStatement":
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function stripExpr(expr: Expression): Expression {
|
|
121
|
-
switch (expr.type) {
|
|
122
|
-
case "InterpolatedStringExpression":
|
|
123
|
-
for (const part of expr.parts) {
|
|
124
|
-
if (part.kind === "expression") part.expression = stripExpr(part.expression)
|
|
125
|
-
}
|
|
126
|
-
return expr
|
|
127
|
-
|
|
128
|
-
case "FunctionExpression":
|
|
129
|
-
stripFunctionBody(expr.func)
|
|
130
|
-
return expr
|
|
131
|
-
|
|
132
|
-
case "TableExpression":
|
|
133
|
-
for (const field of expr.fields) {
|
|
134
|
-
if (field.type === "TableFieldPositional") {
|
|
135
|
-
field.value = stripExpr(field.value)
|
|
136
|
-
} else if (field.type === "TableFieldNamed") {
|
|
137
|
-
field.value = stripExpr(field.value)
|
|
138
|
-
} else if (field.type === "TableFieldComputed") {
|
|
139
|
-
field.key = stripExpr(field.key)
|
|
140
|
-
field.value = stripExpr(field.value)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return expr
|
|
144
|
-
|
|
145
|
-
case "BinaryExpression":
|
|
146
|
-
expr.left = stripExpr(expr.left)
|
|
147
|
-
expr.right = stripExpr(expr.right)
|
|
148
|
-
return expr
|
|
149
|
-
|
|
150
|
-
case "UnaryExpression":
|
|
151
|
-
expr.argument = stripExpr(expr.argument)
|
|
152
|
-
return expr
|
|
153
|
-
|
|
154
|
-
case "MemberExpression":
|
|
155
|
-
expr.object = stripExpr(expr.object)
|
|
156
|
-
return expr
|
|
157
|
-
|
|
158
|
-
case "IndexExpression":
|
|
159
|
-
expr.object = stripExpr(expr.object)
|
|
160
|
-
expr.index = stripExpr(expr.index)
|
|
161
|
-
return expr
|
|
162
|
-
|
|
163
|
-
case "CallExpression":
|
|
164
|
-
expr.callee = stripExpr(expr.callee)
|
|
165
|
-
for (let i = 0; i < expr.arguments.length; i++) expr.arguments[i] = stripExpr(expr.arguments[i])
|
|
166
|
-
return expr
|
|
167
|
-
|
|
168
|
-
case "MethodCallExpression":
|
|
169
|
-
expr.object = stripExpr(expr.object)
|
|
170
|
-
for (let i = 0; i < expr.arguments.length; i++) expr.arguments[i] = stripExpr(expr.arguments[i])
|
|
171
|
-
return expr
|
|
172
|
-
|
|
173
|
-
case "ParenthesizedExpression":
|
|
174
|
-
expr.expression = stripExpr(expr.expression)
|
|
175
|
-
return expr
|
|
176
|
-
|
|
177
|
-
case "TypeAssertionExpression":
|
|
178
|
-
// `expr :: T` -> T를 버리고 expr만 남김 (괄호로 감싸서 우선순위 보존)
|
|
179
|
-
return stripExpr(expr.expression)
|
|
180
|
-
|
|
181
|
-
case "IfElseExpression":
|
|
182
|
-
for (const clause of expr.clauses) {
|
|
183
|
-
clause.condition = stripExpr(clause.condition)
|
|
184
|
-
clause.body = stripExpr(clause.body)
|
|
185
|
-
}
|
|
186
|
-
expr.alternate = stripExpr(expr.alternate)
|
|
187
|
-
return expr
|
|
188
|
-
|
|
189
|
-
default:
|
|
190
|
-
return expr
|
|
191
|
-
}
|
|
1
|
+
import type {
|
|
2
|
+
Program, Block, Statement, Expression, FunctionBody,
|
|
3
|
+
} from "luau-parser"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 파이프라인 맨 앞에서 실행되어야 하는 패스.
|
|
7
|
+
*
|
|
8
|
+
* 이후 패스들(GlobalMapping, RenameVariables 등)은 값 트리만 순회하고
|
|
9
|
+
* `typeof(x)`, `x :: T` 같은 타입 노드 내부의 expression은 건드리지 않는다.
|
|
10
|
+
* 타입을 나중까지 들고 있으면 값 쪽 식별자는 바뀌는데 타입 쪽 참조는 원래
|
|
11
|
+
* 이름 그대로 남아 출력물이 타입체크 불가능한 상태가 된다
|
|
12
|
+
* (`typeof(runtimeValue)`가 obfuscate 이후에도 원본 이름을 참조하는 것이 그 예).
|
|
13
|
+
*
|
|
14
|
+
* 그래서 타입 정보 자체를 파이프라인 시작 시점에 전부 지워서 이 클래스의
|
|
15
|
+
* 버그를 원천 차단한다:
|
|
16
|
+
* - TypeAliasStatement / ExportTypeAliasStatement 문 자체를 블록에서 제거
|
|
17
|
+
* - TypedIdentifier / FunctionParameter 의 typeAnnotation 제거
|
|
18
|
+
* - FunctionBody 의 generics / varargTypeAnnotation / returnType 제거
|
|
19
|
+
* - `expr :: T` (TypeAssertionExpression) 은 T를 버리고 expr로 치환
|
|
20
|
+
*/
|
|
21
|
+
export function runStripTypes(program: Program): void {
|
|
22
|
+
stripBlock(program.body)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function stripBlock(block: Block): void {
|
|
26
|
+
const kept: Statement[] = []
|
|
27
|
+
for (const stmt of block.statements) {
|
|
28
|
+
if (stmt.type === "TypeAliasStatement" || stmt.type === "ExportTypeAliasStatement") {
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
stripStatement(stmt)
|
|
32
|
+
kept.push(stmt)
|
|
33
|
+
}
|
|
34
|
+
block.statements = kept
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stripFunctionBody(func: FunctionBody): void {
|
|
38
|
+
func.generics = []
|
|
39
|
+
for (const param of func.params) delete param.typeAnnotation
|
|
40
|
+
delete func.varargTypeAnnotation
|
|
41
|
+
delete func.returnType
|
|
42
|
+
stripBlock(func.body)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function stripStatement(stmt: Statement): void {
|
|
46
|
+
switch (stmt.type) {
|
|
47
|
+
case "LocalStatement":
|
|
48
|
+
for (const name of stmt.names) delete name.typeAnnotation
|
|
49
|
+
for (let i = 0; i < stmt.init.length; i++) stmt.init[i] = stripExpr(stmt.init[i])
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
case "LocalFunctionStatement":
|
|
53
|
+
stripFunctionBody(stmt.func)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
case "FunctionDeclarationStatement":
|
|
57
|
+
stripFunctionBody(stmt.func)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
case "AssignmentStatement":
|
|
61
|
+
for (let i = 0; i < stmt.targets.length; i++) stmt.targets[i] = stripExpr(stmt.targets[i])
|
|
62
|
+
for (let i = 0; i < stmt.values.length; i++) stmt.values[i] = stripExpr(stmt.values[i])
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
case "CompoundAssignmentStatement":
|
|
66
|
+
stmt.target = stripExpr(stmt.target)
|
|
67
|
+
stmt.value = stripExpr(stmt.value)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
case "CallStatement":
|
|
71
|
+
stmt.expression = stripExpr(stmt.expression) as typeof stmt.expression
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
case "DoStatement":
|
|
75
|
+
stripBlock(stmt.body)
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
case "WhileStatement":
|
|
79
|
+
stmt.condition = stripExpr(stmt.condition)
|
|
80
|
+
stripBlock(stmt.body)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
case "RepeatStatement":
|
|
84
|
+
stripBlock(stmt.body)
|
|
85
|
+
stmt.condition = stripExpr(stmt.condition)
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
case "IfStatement":
|
|
89
|
+
for (const clause of stmt.clauses) {
|
|
90
|
+
clause.condition = stripExpr(clause.condition)
|
|
91
|
+
stripBlock(clause.body)
|
|
92
|
+
}
|
|
93
|
+
if (stmt.alternate) stripBlock(stmt.alternate)
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
case "NumericForStatement":
|
|
97
|
+
delete stmt.variable.typeAnnotation
|
|
98
|
+
stmt.start = stripExpr(stmt.start)
|
|
99
|
+
stmt.end = stripExpr(stmt.end)
|
|
100
|
+
if (stmt.step) stmt.step = stripExpr(stmt.step)
|
|
101
|
+
stripBlock(stmt.body)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
case "GenericForStatement":
|
|
105
|
+
for (const v of stmt.variables) delete v.typeAnnotation
|
|
106
|
+
for (let i = 0; i < stmt.iterators.length; i++) stmt.iterators[i] = stripExpr(stmt.iterators[i])
|
|
107
|
+
stripBlock(stmt.body)
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
case "ReturnStatement":
|
|
111
|
+
for (let i = 0; i < stmt.arguments.length; i++) stmt.arguments[i] = stripExpr(stmt.arguments[i])
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
case "BreakStatement":
|
|
115
|
+
case "ContinueStatement":
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function stripExpr(expr: Expression): Expression {
|
|
121
|
+
switch (expr.type) {
|
|
122
|
+
case "InterpolatedStringExpression":
|
|
123
|
+
for (const part of expr.parts) {
|
|
124
|
+
if (part.kind === "expression") part.expression = stripExpr(part.expression)
|
|
125
|
+
}
|
|
126
|
+
return expr
|
|
127
|
+
|
|
128
|
+
case "FunctionExpression":
|
|
129
|
+
stripFunctionBody(expr.func)
|
|
130
|
+
return expr
|
|
131
|
+
|
|
132
|
+
case "TableExpression":
|
|
133
|
+
for (const field of expr.fields) {
|
|
134
|
+
if (field.type === "TableFieldPositional") {
|
|
135
|
+
field.value = stripExpr(field.value)
|
|
136
|
+
} else if (field.type === "TableFieldNamed") {
|
|
137
|
+
field.value = stripExpr(field.value)
|
|
138
|
+
} else if (field.type === "TableFieldComputed") {
|
|
139
|
+
field.key = stripExpr(field.key)
|
|
140
|
+
field.value = stripExpr(field.value)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return expr
|
|
144
|
+
|
|
145
|
+
case "BinaryExpression":
|
|
146
|
+
expr.left = stripExpr(expr.left)
|
|
147
|
+
expr.right = stripExpr(expr.right)
|
|
148
|
+
return expr
|
|
149
|
+
|
|
150
|
+
case "UnaryExpression":
|
|
151
|
+
expr.argument = stripExpr(expr.argument)
|
|
152
|
+
return expr
|
|
153
|
+
|
|
154
|
+
case "MemberExpression":
|
|
155
|
+
expr.object = stripExpr(expr.object)
|
|
156
|
+
return expr
|
|
157
|
+
|
|
158
|
+
case "IndexExpression":
|
|
159
|
+
expr.object = stripExpr(expr.object)
|
|
160
|
+
expr.index = stripExpr(expr.index)
|
|
161
|
+
return expr
|
|
162
|
+
|
|
163
|
+
case "CallExpression":
|
|
164
|
+
expr.callee = stripExpr(expr.callee)
|
|
165
|
+
for (let i = 0; i < expr.arguments.length; i++) expr.arguments[i] = stripExpr(expr.arguments[i])
|
|
166
|
+
return expr
|
|
167
|
+
|
|
168
|
+
case "MethodCallExpression":
|
|
169
|
+
expr.object = stripExpr(expr.object)
|
|
170
|
+
for (let i = 0; i < expr.arguments.length; i++) expr.arguments[i] = stripExpr(expr.arguments[i])
|
|
171
|
+
return expr
|
|
172
|
+
|
|
173
|
+
case "ParenthesizedExpression":
|
|
174
|
+
expr.expression = stripExpr(expr.expression)
|
|
175
|
+
return expr
|
|
176
|
+
|
|
177
|
+
case "TypeAssertionExpression":
|
|
178
|
+
// `expr :: T` -> T를 버리고 expr만 남김 (괄호로 감싸서 우선순위 보존)
|
|
179
|
+
return stripExpr(expr.expression)
|
|
180
|
+
|
|
181
|
+
case "IfElseExpression":
|
|
182
|
+
for (const clause of expr.clauses) {
|
|
183
|
+
clause.condition = stripExpr(clause.condition)
|
|
184
|
+
clause.body = stripExpr(clause.body)
|
|
185
|
+
}
|
|
186
|
+
expr.alternate = stripExpr(expr.alternate)
|
|
187
|
+
return expr
|
|
188
|
+
|
|
189
|
+
default:
|
|
190
|
+
return expr
|
|
191
|
+
}
|
|
192
192
|
}
|