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.
@@ -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, 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, names: VmNames): Expression {
30
+ const fields: TableField[] = proto.code.map((instr) =>
31
+ positionalField(
32
+ table([
33
+ namedField(names.op, numberLiteral(instr.op as number)),
34
+ namedField(names.a, numberLiteral(instr.a)),
35
+ namedField(names.b, numberLiteral(instr.b)),
36
+ namedField(names.c, numberLiteral(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, numberLiteral(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, numberLiteral(proto.numParams)),
58
+ namedField(names.hasVarargs, booleanLiteral(proto.hasVarargs)),
59
+ namedField(names.maxRegs, numberLiteral(proto.maxRegs)),
60
+ namedField(names.code, serializeInstrs(proto, names)),
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/pipeline.ts CHANGED
@@ -10,10 +10,12 @@ 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
20
  "StringsToExpressions",
19
21
  "NumbersToExpressions",
@@ -22,7 +24,6 @@ export const PASS_ORDER: (keyof ObfuscateConfig)[] = [
22
24
  "EncryptStrings",
23
25
  "EncryptNumbers",
24
26
  "InsertJunk",
25
- "Vmify",
26
27
  "WrapInFunction",
27
28
  "Minify",
28
29
  ]
@@ -37,6 +38,7 @@ 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 {
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
  });