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.
Files changed (38) hide show
  1. package/.github/workflows/release.yml +56 -56
  2. package/dist/index.cjs +2186 -95
  3. package/dist/index.d.cts +34 -2
  4. package/dist/index.d.ts +34 -2
  5. package/dist/index.js +2186 -95
  6. package/generated/grammers.luau +13643 -0
  7. package/generated/tests.luau +5567 -0
  8. package/package.json +2 -2
  9. package/scripts/test.js +24 -8
  10. package/{scripts/example.luau → smoketest/grammers.luau} +1174 -1174
  11. package/smoketest/tests.luau +115 -0
  12. package/src/config.ts +2 -1
  13. package/src/passes/ConstantArray.ts +90 -89
  14. package/src/passes/EncryptNumbers.ts +122 -65
  15. package/src/passes/EncryptStrings.ts +162 -82
  16. package/src/passes/GlobalMapping.ts +193 -193
  17. package/src/passes/InsertJunk.ts +184 -184
  18. package/src/passes/Minify.ts +5 -5
  19. package/src/passes/NumbersToExpressions.ts +183 -2
  20. package/src/passes/RenameVariables.ts +2 -9
  21. package/src/passes/StringsToExpressions.ts +195 -6
  22. package/src/passes/StripTypes.ts +191 -191
  23. package/src/passes/Vmify.ts +87 -0
  24. package/src/passes/WrapInFunction.ts +31 -31
  25. package/src/passes/nodeFactory.ts +36 -1
  26. package/src/passes/vmify/chunk.ts +54 -0
  27. package/src/passes/vmify/compiler.ts +1026 -0
  28. package/src/passes/vmify/names.ts +72 -0
  29. package/src/passes/vmify/opcodes.ts +80 -0
  30. package/src/passes/vmify/registers.ts +63 -0
  31. package/src/passes/vmify/runtime.ts +334 -0
  32. package/src/passes/vmify/scope-walk.ts +175 -0
  33. package/src/passes/vmify/serialize.ts +68 -0
  34. package/src/passes/walk.ts +141 -43
  35. package/src/pipeline.ts +5 -6
  36. package/tsconfig.json +20 -20
  37. package/tsup.config.ts +9 -9
  38. package/generated/final.luau +0 -1029
@@ -0,0 +1,87 @@
1
+ import type { Program, Statement, Expression } 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
+ vmStructuralNumbers,
11
+ } from "./nodeFactory"
12
+ import { transformExpressions } from "./walk"
13
+
14
+ /** 파싱된 서브트리(런타임 인터프리터 소스) 안의 모든 NumberLiteral을 vmStructuralNumbers로
15
+ * 표시한다 — 인터프리터 자신의 +1/-1/패딩 같은 구현 디테일 상수는 사용자 데이터가 아닌데,
16
+ * 수십 개 opcode 핸들러에 반복 등장해서 EncryptNumbers/NumbersToExpressions/ConstantArray가
17
+ * 건드리면 곱셈적으로 부풀어 오른다. */
18
+ function markRuntimeNumbersAsStructural(body: { statements: Statement[] }): void {
19
+ transformExpressions({ body } as Program, (expr) => {
20
+ if (expr.type === "NumberLiteral") vmStructuralNumbers.add(expr)
21
+ return undefined
22
+ })
23
+ }
24
+
25
+ export interface VmifyOptions {
26
+ /**
27
+ * VM 밖에서 값을 그대로 참조해야 하는 전역 이름 목록(GETGLOBAL/SETGLOBAL 대상).
28
+ * Luau엔 getfenv가 없어서 "임의의 전역 읽기"를 흉내낼 수 없기 때문에,
29
+ * 여기 나열된 이름들만 브리지 테이블에 실제 값으로 미리 채워 넣는다.
30
+ * 나열되지 않은 전역을 참조하면 컴파일은 되지만 런타임에 nil이 나온다 — 실사용 전
31
+ * 반드시 프로그램에서 실제로 쓰는 전역 이름을 전부 이 목록에 채워야 함(TODO: 컴파일러가
32
+ * ScopeAnalysis.globalsByName을 이용해 자동으로 목록을 뽑아주도록 개선 가능).
33
+ */
34
+ builtinGlobals?: readonly string[]
35
+ /**
36
+ * opcode 번호 셔플 + 전역/필드 이름 무작위화에 쓸 난수 생성기. 지정하지 않으면
37
+ * Math.random. 재현 가능한 빌드가 필요하면(테스트 등) 시드 고정 PRNG를 넘길 것.
38
+ */
39
+ random?: () => number
40
+ }
41
+
42
+ /**
43
+ * program 전체를 바이트코드로 컴파일하고, program.body를 다음 구조로 치환한다:
44
+ *
45
+ * local <globals> = { print = print, game = game, ... }
46
+ * <런타임 인터프리터 함수 정의 — 식별자/opcode 번호는 매 빌드 무작위>
47
+ * local <protoRoot> = { ...직렬화된 바이트코드... }
48
+ * return <execute>(<protoRoot>, {}, ...)
49
+ *
50
+ * opcode 번호(opcodes.ts:createOpcodeMap)와 전역/필드 이름(names.ts:generateVmNames)을
51
+ * 매 빌드 새로 뽑기 때문에, 컴파일된 청크와 런타임 인터프리터가 산출물마다 구조적으로
52
+ * 달라진다 — "이 VM은 opcode 6이 GETGLOBAL이다" 같은 지식이 다음 빌드에는 안 통함.
53
+ *
54
+ * 주의: 이 패스는 pipeline.ts상 반드시 "가장 먼저"(StripTypes 다음) 실행돼야 한다 —
55
+ * 컴파일이 끝나면 원본 statement/expression 노드 대부분이 바이트코드 숫자로 바뀌어
56
+ * 사라지므로, 이후 패스(RenameVariables, EncryptStrings 등)가 손댈 원본 AST가 없다.
57
+ */
58
+ export function runVmify(program: Program, options: VmifyOptions): void {
59
+ const globalNames = options.builtinGlobals ?? DEFAULT_BUILTIN_GLOBALS
60
+ const random = options.random ?? Math.random
61
+
62
+ const opcodeMap = createOpcodeMap(random)
63
+ const names = generateVmNames(random)
64
+
65
+ const compiler = new VmCompiler(program, globalNames, opcodeMap)
66
+ const topProto = compiler.compile()
67
+
68
+ const globalsTable = table(
69
+ globalNames.map((name) => namedField(name, identifier(name))),
70
+ )
71
+
72
+ const runtimeSource = buildVmRuntimeSource(names, opcodeMap)
73
+ const runtimeProgram = luauparser.parse(runtimeSource)
74
+ const runtimeStatements: Statement[] = runtimeProgram.body.statements
75
+ markRuntimeNumbersAsStructural(runtimeProgram.body)
76
+
77
+ const protoLiteral = serializeProto(topProto, names)
78
+
79
+ const newBody: Statement[] = [
80
+ localStatement(names.globals, globalsTable),
81
+ ...runtimeStatements,
82
+ localStatement(names.protoRoot, protoLiteral),
83
+ returnStatement([call(identifier(names.execute), [identifier(names.protoRoot), table([]), vararg()])]),
84
+ ]
85
+
86
+ program.body.statements = newBody
87
+ }
@@ -1,32 +1,32 @@
1
- import type { Program } from "luau-parser"
2
- import {
3
- block, functionBody, functionExpression, paren, call, vararg, returnStatement,
4
- } from "./nodeFactory"
5
-
6
- export interface WrapInFunctionOptions {}
7
-
8
- /**
9
- * 전체 프로그램을:
10
- * return (function(...)
11
- * <원래 코드>
12
- * end)(...)
13
- * 로 감쌈.
14
- *
15
- * - `return`을 쓰는 이유: Script/LocalScript에서는 top-level return이 그냥 청크를
16
- * 조기 종료시킬 뿐 무해하고, ModuleScript라면 IIFE의 결과값이 그대로 require()
17
- * 호출자에게 전달돼야 하므로 필요함. 즉 스크립트 종류를 가리지 않고 안전.
18
- * - `...`을 파라미터로 받아서 다시 그대로 넘겨주는 이유: 원본 청크가 최상위에서
19
- * `...`(스크립트 인자)을 참조하는 경우를 대비. 그냥 지워버리면 그런 코드가
20
- * 깨짐.
21
- *
22
- * 반드시 파이프라인 맨 마지막 근처에서 실행돼야 함 — 이 패스 이후에 실행되는
23
- * 다른 패스가 top-level 스코프를 순회/변형한다면 이미 감싸인 함수 내부까지
24
- * 안 보고 지나칠 수 있음.
25
- */
26
- export function runWrapInFunction(program: Program, _options: WrapInFunctionOptions): void {
27
- const innerBody = program.body
28
- const wrapper = functionExpression(functionBody([], innerBody, true))
29
- const iife = call(paren(wrapper), [vararg()])
30
-
31
- program.body = block([returnStatement([iife])])
1
+ import type { Program } from "luau-parser"
2
+ import {
3
+ block, functionBody, functionExpression, paren, call, vararg, returnStatement,
4
+ } from "./nodeFactory"
5
+
6
+ export interface WrapInFunctionOptions {}
7
+
8
+ /**
9
+ * 전체 프로그램을:
10
+ * return (function(...)
11
+ * <원래 코드>
12
+ * end)(...)
13
+ * 로 감쌈.
14
+ *
15
+ * - `return`을 쓰는 이유: Script/LocalScript에서는 top-level return이 그냥 청크를
16
+ * 조기 종료시킬 뿐 무해하고, ModuleScript라면 IIFE의 결과값이 그대로 require()
17
+ * 호출자에게 전달돼야 하므로 필요함. 즉 스크립트 종류를 가리지 않고 안전.
18
+ * - `...`을 파라미터로 받아서 다시 그대로 넘겨주는 이유: 원본 청크가 최상위에서
19
+ * `...`(스크립트 인자)을 참조하는 경우를 대비. 그냥 지워버리면 그런 코드가
20
+ * 깨짐.
21
+ *
22
+ * 반드시 파이프라인 맨 마지막 근처에서 실행돼야 함 — 이 패스 이후에 실행되는
23
+ * 다른 패스가 top-level 스코프를 순회/변형한다면 이미 감싸인 함수 내부까지
24
+ * 안 보고 지나칠 수 있음.
25
+ */
26
+ export function runWrapInFunction(program: Program, _options: WrapInFunctionOptions): void {
27
+ const innerBody = program.body
28
+ const wrapper = functionExpression(functionBody([], innerBody, true))
29
+ const iife = call(paren(wrapper), [vararg()])
30
+
31
+ program.body = block([returnStatement([iife])])
32
32
  }
@@ -5,7 +5,7 @@ import type {
5
5
  LocalStatement, LocalFunctionStatement, FunctionBody, FunctionParameter,
6
6
  TypedIdentifier, Block, Statement, AssignmentStatement, NumericForStatement,
7
7
  ReturnStatement, DoStatement, IfStatement, IfClause, VarargExpression,
8
- FunctionExpression,
8
+ FunctionExpression, BooleanLiteral, NilLiteral, WhileStatement,
9
9
  } from "luau-parser"
10
10
 
11
11
  /** 합성 노드는 실제 소스 위치가 없으므로 0으로 채움(printer는 구조만 봄). */
@@ -27,6 +27,25 @@ export function numberLiteral(value: number): NumberLiteral {
27
27
  return { type: "NumberLiteral", value, raw: String(value), line: POS, column: POS }
28
28
  }
29
29
 
30
+ /**
31
+ * Vmify가 인스트럭션 op/a/b/c, numParams, maxRegs 같은 "VM 구조 메타데이터"용으로
32
+ * 찍어내는 숫자는 이 표시를 달아 만든다. 이런 값은 원본 소스에 쓰인 상수가 아니라
33
+ * Vmify 자신이 만들어낸, 개수가 코드 크기에 비례해 수천~수만 개까지 불어날 수 있는
34
+ * 내부 값이고, 로직 자체는 이미 Vmify 컴파일로 구조적으로 숨겨져 있다.
35
+ * NumbersToExpressions/EncryptNumbers 같은 뒤쪽 패스가 이 값들까지 하나하나
36
+ * for-loop/재귀 함수/디코더 호출로 부풀리면, 두 패스가 곱셈적으로 상호작용해
37
+ * 출력이 기하급수적으로 커진다 (실측: 4400줄 샘플에서 Vmify+NumbersToExpressions만
38
+ * 켜면 0.9MB가 아니라 7.7MB가 나옴). 그래서 이 표시가 붙은 노드는 뒤쪽 숫자
39
+ * 난독화 패스들이 건드리지 않고 그대로 둔다.
40
+ */
41
+ export const vmStructuralNumbers = new WeakSet<NumberLiteral>()
42
+
43
+ export function vmNumberLiteral(value: number): NumberLiteral {
44
+ const node = numberLiteral(value)
45
+ vmStructuralNumbers.add(node)
46
+ return node
47
+ }
48
+
30
49
  export function binary(
31
50
  operator: BinaryExpression["operator"],
32
51
  left: Expression,
@@ -67,6 +86,18 @@ export function positionalField(value: Expression): TableField {
67
86
  return { type: "TableFieldPositional", value }
68
87
  }
69
88
 
89
+ export function namedField(name: string, value: Expression): TableField {
90
+ return { type: "TableFieldNamed", name: identifier(name), value }
91
+ }
92
+
93
+ export function booleanLiteral(value: boolean): BooleanLiteral {
94
+ return { type: "BooleanLiteral", value, line: POS, column: POS }
95
+ }
96
+
97
+ export function nilLiteral(): NilLiteral {
98
+ return { type: "NilLiteral", line: POS, column: POS }
99
+ }
100
+
70
101
  export function localStatement(name: string, init: Expression): LocalStatement {
71
102
  return {
72
103
  type: "LocalStatement",
@@ -138,6 +169,10 @@ export function ifStatement(clauses: IfClause[], alternate?: Block): IfStatement
138
169
  return { type: "IfStatement", clauses, alternate, line: POS, column: POS }
139
170
  }
140
171
 
172
+ export function whileStatement(condition: Expression, body: Block): WhileStatement {
173
+ return { type: "WhileStatement", condition, body, line: POS, column: POS }
174
+ }
175
+
141
176
  export function vararg(): VarargExpression {
142
177
  return { type: "VarargExpression", line: POS, column: POS }
143
178
  }
@@ -0,0 +1,54 @@
1
+ import type { Instr } from "./opcodes"
2
+
3
+ /** 상수 풀에 들어갈 수 있는 값. */
4
+ export type ConstValue = string | number | boolean | null
5
+
6
+ /**
7
+ * upvalue 하나가 부모 함수 기준으로 어디서 오는지.
8
+ * - "local": 부모 함수의 레지스터(박스)를 그대로 캡처
9
+ * - "upval": 부모 함수 자신의 upvalue를 한 단계 더 전달(중첩 클로저)
10
+ */
11
+ export interface UpvalDesc {
12
+ kind: "local" | "upval"
13
+ index: number
14
+ }
15
+
16
+ /** 함수 하나(최상위 청크 포함)에 대응하는 컴파일 결과. Lua의 Proto와 동일한 역할. */
17
+ export interface Proto {
18
+ id: number
19
+ numParams: number
20
+ hasVarargs: boolean
21
+ maxRegs: number
22
+ code: Instr[]
23
+ consts: ConstValue[]
24
+ /** consts 배열과 별도로 유지하는 value -> index 조회용 인덱스 (interning을 O(1)로). */
25
+ constIndex: Map<ConstValue, number>
26
+ /** 이 함수가 캡처하는 upvalue들이 "부모" 기준 어디서 오는지. */
27
+ upvalDescs: UpvalDesc[]
28
+ /** 중첩 함수 표현식들. CLOSURE 명령의 B는 이 배열의 인덱스. */
29
+ protos: Proto[]
30
+ }
31
+
32
+ export function createProto(id: number): Proto {
33
+ return {
34
+ id,
35
+ numParams: 0,
36
+ hasVarargs: false,
37
+ maxRegs: 0,
38
+ code: [],
39
+ consts: [],
40
+ constIndex: new Map(),
41
+ upvalDescs: [],
42
+ protos: [],
43
+ }
44
+ }
45
+
46
+ /** 상수 풀에 값 추가(중복 제거) 후 인덱스 반환. */
47
+ export function internConst(proto: Proto, value: ConstValue): number {
48
+ const existing = proto.constIndex.get(value)
49
+ if (existing !== undefined) return existing
50
+ proto.consts.push(value)
51
+ const idx = proto.consts.length - 1
52
+ proto.constIndex.set(value, idx)
53
+ return idx
54
+ }