luau-obfuscator 1.0.0 → 1.0.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.
- package/.github/workflows/release.yml +56 -56
- package/dist/index.cjs +2014 -44
- package/dist/index.d.cts +34 -2
- package/dist/index.d.ts +34 -2
- package/dist/index.js +2014 -44
- package/generated/final.luau +63004 -817
- package/generated/smoketest.luau +24339 -0
- package/package.json +2 -2
- package/scripts/example.luau +1174 -1174
- package/scripts/smoketest.luau +115 -0
- package/scripts/test.js +16 -8
- package/src/config.ts +2 -1
- package/src/passes/ConstantArray.ts +89 -89
- package/src/passes/EncryptNumbers.ts +121 -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 +180 -2
- package/src/passes/RenameVariables.ts +2 -9
- package/src/passes/StringsToExpressions.ts +167 -2
- package/src/passes/StripTypes.ts +191 -191
- package/src/passes/Vmify.ts +73 -0
- package/src/passes/WrapInFunction.ts +31 -31
- package/src/passes/nodeFactory.ts +17 -1
- package/src/passes/vmify/chunk.ts +50 -0
- package/src/passes/vmify/compiler.ts +1026 -0
- package/src/passes/vmify/names.ts +78 -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/pipeline.ts +3 -1
- package/tsconfig.json +20 -20
- package/tsup.config.ts +9 -9
|
@@ -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,6 +53,7 @@ function stringCharCall(bytes: number[]): Expression {
|
|
|
35
53
|
return call(member(identifier("string"), "char"), bytes.map(numberLiteral))
|
|
36
54
|
}
|
|
37
55
|
|
|
56
|
+
/** 기존 방식: 청크를 소스에 보이는 순서 그대로 ".."로 이어붙임. */
|
|
38
57
|
function buildConcatChain(chunks: number[][]): Expression {
|
|
39
58
|
let expr: Expression = stringCharCall(chunks[0])
|
|
40
59
|
for (let i = 1; i < chunks.length; i++) {
|
|
@@ -43,7 +62,146 @@ function buildConcatChain(chunks: number[][]): Expression {
|
|
|
43
62
|
return expr
|
|
44
63
|
}
|
|
45
64
|
|
|
65
|
+
/** [0, n)의 무작위 순열 (Fisher-Yates) */
|
|
66
|
+
function shuffledIndices(n: number): number[] {
|
|
67
|
+
const arr = Array.from({ length: n }, (_, i) => i)
|
|
68
|
+
for (let i = n - 1; i > 0; i--) {
|
|
69
|
+
const j = randomInt(0, i)
|
|
70
|
+
;[arr[i], arr[j]] = [arr[j], arr[i]]
|
|
71
|
+
}
|
|
72
|
+
return arr
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** (function() ... end)() — 문(statement)들을 하나의 표현식으로 감싸는 즉시실행 함수. */
|
|
76
|
+
function iife(statements: Statement[]): Expression {
|
|
77
|
+
return call(paren(functionExpression(functionBody([], block(statements)))), [])
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 청크들을 뒤섞어 store 테이블에 저장하고, 원래 순서를 가리키는 order 테이블을 따로 둔다.
|
|
82
|
+
* store에 나타나는 물리적 순서 == 원문 순서가 아니므로, 소스를 눈으로 읽거나
|
|
83
|
+
* 텍스트 패턴만으로 concat해서는 원문이 나오지 않는다 — order를 통해 런타임에
|
|
84
|
+
* 역참조해야만 올바른 순서가 나온다.
|
|
85
|
+
*/
|
|
86
|
+
function buildShuffledTables(chunks: number[][]): { storeVar: string; orderVar: string; stmts: Statement[] } {
|
|
87
|
+
const n = chunks.length
|
|
88
|
+
const storageOrder = shuffledIndices(n) // storageOrder[slot] = 원본 청크 인덱스
|
|
89
|
+
const storeVar = randomName()
|
|
90
|
+
const orderVar = randomName()
|
|
91
|
+
|
|
92
|
+
const storeFields = storageOrder.map((originalIdx) => positionalField(stringCharCall(chunks[originalIdx])))
|
|
93
|
+
|
|
94
|
+
// order[originalIdx] = slot(1-based) — 원본 위치 k에 해당하는 조각이 store의 몇 번째에 있는지
|
|
95
|
+
const order = new Array<number>(n)
|
|
96
|
+
for (let slot = 0; slot < n; slot++) {
|
|
97
|
+
order[storageOrder[slot]] = slot + 1
|
|
98
|
+
}
|
|
99
|
+
const orderFields = order.map((slot) => positionalField(numberLiteral(slot)))
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
storeVar,
|
|
103
|
+
orderVar,
|
|
104
|
+
stmts: [
|
|
105
|
+
localStatement(storeVar, table(storeFields)),
|
|
106
|
+
localStatement(orderVar, table(orderFields)),
|
|
107
|
+
],
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* (function()
|
|
113
|
+
* local store = { ... 뒤섞인 순서의 string.char(...) 조각들 ... }
|
|
114
|
+
* local order = { ... }
|
|
115
|
+
* local acc = ""
|
|
116
|
+
* for i = 1, #order do
|
|
117
|
+
* acc = acc .. store[order[i]]
|
|
118
|
+
* end
|
|
119
|
+
* return acc
|
|
120
|
+
* end)()
|
|
121
|
+
*/
|
|
122
|
+
function buildForLoopStringExpr(chunks: number[][]): Expression {
|
|
123
|
+
const { storeVar, orderVar, stmts } = buildShuffledTables(chunks)
|
|
124
|
+
const accVar = randomName()
|
|
125
|
+
const iVar = randomName()
|
|
126
|
+
|
|
127
|
+
return iife([
|
|
128
|
+
...stmts,
|
|
129
|
+
localStatement(accVar, stringLiteral("")),
|
|
130
|
+
numericForStatement(
|
|
131
|
+
iVar,
|
|
132
|
+
numberLiteral(1),
|
|
133
|
+
unary("#", identifier(orderVar)),
|
|
134
|
+
block([
|
|
135
|
+
assignmentStatement(
|
|
136
|
+
[identifier(accVar)],
|
|
137
|
+
[
|
|
138
|
+
binary(
|
|
139
|
+
"..",
|
|
140
|
+
identifier(accVar),
|
|
141
|
+
index(identifier(storeVar), index(identifier(orderVar), identifier(iVar))),
|
|
142
|
+
),
|
|
143
|
+
],
|
|
144
|
+
),
|
|
145
|
+
]),
|
|
146
|
+
),
|
|
147
|
+
returnStatement([identifier(accVar)]),
|
|
148
|
+
])
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* (function()
|
|
153
|
+
* local store = { ... }
|
|
154
|
+
* local order = { ... }
|
|
155
|
+
* local function build(i, acc)
|
|
156
|
+
* if i > #order then
|
|
157
|
+
* return acc
|
|
158
|
+
* end
|
|
159
|
+
* return build(i + 1, acc .. store[order[i]])
|
|
160
|
+
* end
|
|
161
|
+
* return build(1, "")
|
|
162
|
+
* end)()
|
|
163
|
+
*/
|
|
164
|
+
function buildRecursiveStringExpr(chunks: number[][]): Expression {
|
|
165
|
+
const { storeVar, orderVar, stmts } = buildShuffledTables(chunks)
|
|
166
|
+
const recName = randomName()
|
|
167
|
+
const iParam = randomName()
|
|
168
|
+
const accParam = randomName()
|
|
169
|
+
|
|
170
|
+
const body = block([
|
|
171
|
+
ifStatement([
|
|
172
|
+
ifClause(
|
|
173
|
+
binary(">", identifier(iParam), unary("#", identifier(orderVar))),
|
|
174
|
+
block([returnStatement([identifier(accParam)])]),
|
|
175
|
+
),
|
|
176
|
+
]),
|
|
177
|
+
returnStatement([
|
|
178
|
+
call(identifier(recName), [
|
|
179
|
+
binary("+", identifier(iParam), numberLiteral(1)),
|
|
180
|
+
binary(
|
|
181
|
+
"..",
|
|
182
|
+
identifier(accParam),
|
|
183
|
+
index(identifier(storeVar), index(identifier(orderVar), identifier(iParam))),
|
|
184
|
+
),
|
|
185
|
+
]),
|
|
186
|
+
]),
|
|
187
|
+
])
|
|
188
|
+
|
|
189
|
+
return iife([
|
|
190
|
+
...stmts,
|
|
191
|
+
localFunctionStatement(recName, functionBody([functionParam(iParam), functionParam(accParam)], body)),
|
|
192
|
+
returnStatement([call(identifier(recName), [numberLiteral(1), stringLiteral("")])]),
|
|
193
|
+
])
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
type ControlFlowStrategy = "forLoop" | "recursive"
|
|
197
|
+
|
|
198
|
+
function pickControlFlowStrategy(): ControlFlowStrategy {
|
|
199
|
+
return Math.random() < 0.5 ? "forLoop" : "recursive"
|
|
200
|
+
}
|
|
201
|
+
|
|
46
202
|
export function runStringsToExpressions(program: Program, options: StringsToExpressionsOptions): void {
|
|
203
|
+
const controlFlowProbability = options.controlFlowProbability ?? CONTROL_FLOW_PROBABILITY_DEFAULT
|
|
204
|
+
|
|
47
205
|
transformExpressions(program, (expr) => {
|
|
48
206
|
if (expr.type !== "StringLiteral") return
|
|
49
207
|
if (expr.value.length === 0) return
|
|
@@ -51,6 +209,13 @@ export function runStringsToExpressions(program: Program, options: StringsToExpr
|
|
|
51
209
|
const bytes = toUtf8Bytes(expr.value)
|
|
52
210
|
const chunks = splitByteChunks(bytes, options.min, options.max)
|
|
53
211
|
|
|
212
|
+
// 청크가 하나뿐이면 순서를 흩뜨려도 얻는 게 없으니 그냥 원래 방식대로.
|
|
213
|
+
if (chunks.length > 1 && Math.random() < controlFlowProbability) {
|
|
214
|
+
return pickControlFlowStrategy() === "forLoop"
|
|
215
|
+
? buildForLoopStringExpr(chunks)
|
|
216
|
+
: buildRecursiveStringExpr(chunks)
|
|
217
|
+
}
|
|
218
|
+
|
|
54
219
|
return buildConcatChain(chunks)
|
|
55
220
|
})
|
|
56
221
|
}
|
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
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Program, Statement } from "luau-parser"
|
|
2
|
+
import { luauparser } from "luau-parser"
|
|
3
|
+
import { VmCompiler, DEFAULT_BUILTIN_GLOBALS } from "./vmify/compiler"
|
|
4
|
+
import { serializeProto } from "./vmify/serialize"
|
|
5
|
+
import { buildVmRuntimeSource } from "./vmify/runtime"
|
|
6
|
+
import { createOpcodeMap } from "./vmify/opcodes"
|
|
7
|
+
import { generateVmNames } from "./vmify/names"
|
|
8
|
+
import {
|
|
9
|
+
localStatement, identifier, call, table, namedField, returnStatement, vararg,
|
|
10
|
+
} from "./nodeFactory"
|
|
11
|
+
|
|
12
|
+
export interface VmifyOptions {
|
|
13
|
+
/**
|
|
14
|
+
* VM 밖에서 값을 그대로 참조해야 하는 전역 이름 목록(GETGLOBAL/SETGLOBAL 대상).
|
|
15
|
+
* Luau엔 getfenv가 없어서 "임의의 전역 읽기"를 흉내낼 수 없기 때문에,
|
|
16
|
+
* 여기 나열된 이름들만 브리지 테이블에 실제 값으로 미리 채워 넣는다.
|
|
17
|
+
* 나열되지 않은 전역을 참조하면 컴파일은 되지만 런타임에 nil이 나온다 — 실사용 전
|
|
18
|
+
* 반드시 프로그램에서 실제로 쓰는 전역 이름을 전부 이 목록에 채워야 함(TODO: 컴파일러가
|
|
19
|
+
* ScopeAnalysis.globalsByName을 이용해 자동으로 목록을 뽑아주도록 개선 가능).
|
|
20
|
+
*/
|
|
21
|
+
builtinGlobals?: readonly string[]
|
|
22
|
+
/**
|
|
23
|
+
* opcode 번호 셔플 + 전역/필드 이름 무작위화에 쓸 난수 생성기. 지정하지 않으면
|
|
24
|
+
* Math.random. 재현 가능한 빌드가 필요하면(테스트 등) 시드 고정 PRNG를 넘길 것.
|
|
25
|
+
*/
|
|
26
|
+
random?: () => number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* program 전체를 바이트코드로 컴파일하고, program.body를 다음 구조로 치환한다:
|
|
31
|
+
*
|
|
32
|
+
* local <globals> = { print = print, game = game, ... }
|
|
33
|
+
* <런타임 인터프리터 함수 정의 — 식별자/opcode 번호는 매 빌드 무작위>
|
|
34
|
+
* local <protoRoot> = { ...직렬화된 바이트코드... }
|
|
35
|
+
* return <execute>(<protoRoot>, {}, ...)
|
|
36
|
+
*
|
|
37
|
+
* opcode 번호(opcodes.ts:createOpcodeMap)와 전역/필드 이름(names.ts:generateVmNames)을
|
|
38
|
+
* 매 빌드 새로 뽑기 때문에, 컴파일된 청크와 런타임 인터프리터가 산출물마다 구조적으로
|
|
39
|
+
* 달라진다 — "이 VM은 opcode 6이 GETGLOBAL이다" 같은 지식이 다음 빌드에는 안 통함.
|
|
40
|
+
*
|
|
41
|
+
* 주의: 이 패스는 pipeline.ts상 반드시 "가장 먼저"(StripTypes 다음) 실행돼야 한다 —
|
|
42
|
+
* 컴파일이 끝나면 원본 statement/expression 노드 대부분이 바이트코드 숫자로 바뀌어
|
|
43
|
+
* 사라지므로, 이후 패스(RenameVariables, EncryptStrings 등)가 손댈 원본 AST가 없다.
|
|
44
|
+
*/
|
|
45
|
+
export function runVmify(program: Program, options: VmifyOptions): void {
|
|
46
|
+
const globalNames = options.builtinGlobals ?? DEFAULT_BUILTIN_GLOBALS
|
|
47
|
+
const random = options.random ?? Math.random
|
|
48
|
+
|
|
49
|
+
const opcodeMap = createOpcodeMap(random)
|
|
50
|
+
const names = generateVmNames(random)
|
|
51
|
+
|
|
52
|
+
const compiler = new VmCompiler(program, globalNames, opcodeMap)
|
|
53
|
+
const topProto = compiler.compile()
|
|
54
|
+
|
|
55
|
+
const globalsTable = table(
|
|
56
|
+
globalNames.map((name) => namedField(name, identifier(name))),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const runtimeSource = buildVmRuntimeSource(names, opcodeMap)
|
|
60
|
+
const runtimeProgram = luauparser.parse(runtimeSource)
|
|
61
|
+
const runtimeStatements: Statement[] = runtimeProgram.body.statements
|
|
62
|
+
|
|
63
|
+
const protoLiteral = serializeProto(topProto, names)
|
|
64
|
+
|
|
65
|
+
const newBody: Statement[] = [
|
|
66
|
+
localStatement(names.globals, globalsTable),
|
|
67
|
+
...runtimeStatements,
|
|
68
|
+
localStatement(names.protoRoot, protoLiteral),
|
|
69
|
+
returnStatement([call(identifier(names.execute), [identifier(names.protoRoot), table([]), vararg()])]),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
program.body.statements = newBody
|
|
73
|
+
}
|