unidecompiler-plugin-lua 0.1.1__py3-none-any.whl

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,241 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from typing import Literal
6
+
7
+ from unidecompiler_plugin_lua.luac import LuaFunctionListing, LuaInstructionListing
8
+
9
+
10
+ BranchKind = Literal["conditional-pair", "unconditional"]
11
+
12
+ BRANCH_OPCODES = {
13
+ "EQ",
14
+ "LT",
15
+ "LE",
16
+ "TEST",
17
+ "TESTSET",
18
+ }
19
+ UNCONDITIONAL_JUMP_OPCODES = {"JMP"}
20
+ TERMINATOR_OPCODES = {
21
+ "RETURN",
22
+ "RETURN0",
23
+ "RETURN1",
24
+ "RETURN2",
25
+ "RETURNI",
26
+ "RETURNK",
27
+ }
28
+ UNSUPPORTED_FOR_LINEAR_LIFTING = {
29
+ "FORPREP",
30
+ "FORLOOP",
31
+ "TFORPREP",
32
+ "TFORCALL",
33
+ "TFORLOOP",
34
+ "JMP",
35
+ "TEST",
36
+ "TESTSET",
37
+ "NEWTABLE",
38
+ "SETLIST",
39
+ "LEN",
40
+ }
41
+ TARGET_COMMENT_RE = re.compile(r"\bto\s+(?P<target>\d+)\b")
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class NormalizedLuaBranch:
46
+ pc: int
47
+ opcode: str
48
+ target_pc: int
49
+ kind: BranchKind
50
+ fallthrough_pc: int | None
51
+
52
+ def to_metadata(self) -> dict:
53
+ data = {
54
+ "pc": self.pc,
55
+ "opcode": self.opcode,
56
+ "target_pc": self.target_pc,
57
+ "kind": self.kind,
58
+ }
59
+ if self.fallthrough_pc is not None:
60
+ data["fallthrough_pc"] = self.fallthrough_pc
61
+ return data
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class NormalizedLuaInstruction:
66
+ pc: int
67
+ line: int | None
68
+ opcode: str
69
+ operands: tuple[str, ...]
70
+ next_pc: int | None
71
+ explicit_target_pc: int | None
72
+
73
+ def to_metadata(self) -> dict:
74
+ return {
75
+ "pc": self.pc,
76
+ "line": self.line,
77
+ "opcode": self.opcode,
78
+ "operands": list(self.operands),
79
+ "next_pc": self.next_pc,
80
+ "explicit_target_pc": self.explicit_target_pc,
81
+ }
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class NormalizedLuaFunction:
86
+ name: str
87
+ source: str
88
+ line_start: int | None
89
+ line_end: int | None
90
+ entry_pc: int | None
91
+ instructions: tuple[NormalizedLuaInstruction, ...]
92
+ branches: tuple[NormalizedLuaBranch, ...]
93
+ basic_block_leaders: tuple[int, ...]
94
+ unsupported_opcodes: tuple[str, ...]
95
+
96
+ def to_metadata(self) -> dict:
97
+ return {
98
+ "name": self.name,
99
+ "source": self.source,
100
+ "line_start": self.line_start,
101
+ "line_end": self.line_end,
102
+ "entry_pc": self.entry_pc,
103
+ "instruction_pcs": [instruction.pc for instruction in self.instructions],
104
+ "instructions": [
105
+ instruction.to_metadata() for instruction in self.instructions
106
+ ],
107
+ "branch_targets": [branch.to_metadata() for branch in self.branches],
108
+ "basic_block_leaders": list(self.basic_block_leaders),
109
+ "unsupported_opcodes": list(self.unsupported_opcodes),
110
+ }
111
+
112
+
113
+ def normalize_lua_functions(
114
+ functions: tuple[LuaFunctionListing, ...],
115
+ ) -> tuple[NormalizedLuaFunction, ...]:
116
+ return tuple(normalize_lua_function(function) for function in functions)
117
+
118
+
119
+ def normalize_lua_function(function: LuaFunctionListing) -> NormalizedLuaFunction:
120
+ instructions = _normalize_instructions(function.instructions)
121
+ branches = _normalize_branches(function.instructions)
122
+ leaders = _basic_block_leaders(function.instructions, branches)
123
+ unsupported_opcodes = tuple(
124
+ sorted(
125
+ {
126
+ instruction.opcode
127
+ for instruction in function.instructions
128
+ if instruction.opcode in UNSUPPORTED_FOR_LINEAR_LIFTING
129
+ }
130
+ )
131
+ )
132
+
133
+ return NormalizedLuaFunction(
134
+ name=function.inferred_name or "<function>",
135
+ source=function.source,
136
+ line_start=function.line_start,
137
+ line_end=function.line_end,
138
+ entry_pc=instructions[0].pc if instructions else None,
139
+ instructions=instructions,
140
+ branches=branches,
141
+ basic_block_leaders=leaders,
142
+ unsupported_opcodes=unsupported_opcodes,
143
+ )
144
+
145
+
146
+ def normalized_functions_metadata(
147
+ functions: tuple[LuaFunctionListing, ...],
148
+ ) -> list[dict]:
149
+ return [
150
+ normalized.to_metadata() for normalized in normalize_lua_functions(functions)
151
+ ]
152
+
153
+
154
+ def _normalize_instructions(
155
+ instructions: tuple[LuaInstructionListing, ...],
156
+ ) -> tuple[NormalizedLuaInstruction, ...]:
157
+ return tuple(
158
+ NormalizedLuaInstruction(
159
+ pc=instruction.pc,
160
+ line=instruction.line,
161
+ opcode=instruction.opcode,
162
+ operands=instruction.operands,
163
+ next_pc=_next_pc(instructions, index),
164
+ explicit_target_pc=_target_from_comment(instruction.comment),
165
+ )
166
+ for index, instruction in enumerate(instructions)
167
+ )
168
+
169
+
170
+ def _normalize_branches(
171
+ instructions: tuple[LuaInstructionListing, ...],
172
+ ) -> tuple[NormalizedLuaBranch, ...]:
173
+ branches: list[NormalizedLuaBranch] = []
174
+
175
+ for index, instruction in enumerate(instructions):
176
+ target_pc = _target_from_comment(instruction.comment)
177
+ if target_pc is None:
178
+ continue
179
+
180
+ if instruction.opcode in UNCONDITIONAL_JUMP_OPCODES:
181
+ previous = instructions[index - 1] if index > 0 else None
182
+ kind: BranchKind = (
183
+ "conditional-pair"
184
+ if previous is not None and previous.opcode in BRANCH_OPCODES
185
+ else "unconditional"
186
+ )
187
+ branches.append(
188
+ NormalizedLuaBranch(
189
+ pc=instruction.pc,
190
+ opcode=instruction.opcode,
191
+ target_pc=target_pc,
192
+ kind=kind,
193
+ fallthrough_pc=_next_pc(instructions, index),
194
+ )
195
+ )
196
+
197
+ return tuple(branches)
198
+
199
+
200
+ def _basic_block_leaders(
201
+ instructions: tuple[LuaInstructionListing, ...],
202
+ branches: tuple[NormalizedLuaBranch, ...],
203
+ ) -> tuple[int, ...]:
204
+ if not instructions:
205
+ return ()
206
+
207
+ instruction_pcs = {instruction.pc for instruction in instructions}
208
+ leaders = {instructions[0].pc}
209
+
210
+ for branch in branches:
211
+ if branch.target_pc in instruction_pcs:
212
+ leaders.add(branch.target_pc)
213
+ if branch.fallthrough_pc in instruction_pcs:
214
+ leaders.add(branch.fallthrough_pc)
215
+
216
+ for index, instruction in enumerate(instructions):
217
+ if instruction.opcode in TERMINATOR_OPCODES:
218
+ next_pc = _next_pc(instructions, index)
219
+ if next_pc in instruction_pcs:
220
+ leaders.add(next_pc)
221
+
222
+ return tuple(sorted(leaders))
223
+
224
+
225
+ def _next_pc(
226
+ instructions: tuple[LuaInstructionListing, ...],
227
+ index: int,
228
+ ) -> int | None:
229
+ next_index = index + 1
230
+ if next_index >= len(instructions):
231
+ return None
232
+ return instructions[next_index].pc
233
+
234
+
235
+ def _target_from_comment(comment: str | None) -> int | None:
236
+ if comment is None:
237
+ return None
238
+ match = TARGET_COMMENT_RE.search(comment)
239
+ if match is None:
240
+ return None
241
+ return int(match.group("target"))
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ from unidecompiler.core.ir import ModuleIR
4
+ from unidecompiler.plugins import FrontendModule
5
+ from unidecompiler_plugin_lua.lifter import lift_lua_chunk
6
+ from unidecompiler_plugin_lua.chunk54 import decode_lua54_chunk, Lua54ChunkError
7
+ from unidecompiler_plugin_lua.luac import (
8
+ looks_like_luac,
9
+ PreferredLuaChunkDecoder,
10
+ LuaChunkDecoder,
11
+ )
12
+ from unidecompiler_plugin_lua.normalize import normalized_functions_metadata
13
+ from unidecompiler_plugin_lua.simulation import LuaSimulationAdapter
14
+ from unidecompiler_plugin_lua.support import LUA_VERSION_SUPPORT
15
+
16
+
17
+ class LuaFrontendPlugin:
18
+ id = "lua"
19
+ display_name = "Lua bytecode"
20
+ supported_inputs = (".luac",)
21
+ version_support = LUA_VERSION_SUPPORT
22
+ simulation_adapter = LuaSimulationAdapter
23
+
24
+ def __init__(self, decoder: LuaChunkDecoder | None = None) -> None:
25
+ self.decoder = decoder or PreferredLuaChunkDecoder()
26
+
27
+ def can_load(self, data: bytes, filename: str | None = None) -> bool:
28
+ return looks_like_luac(data) and self.decoder.can_decode(data, filename)
29
+
30
+ def decode(self, data: bytes, filename: str | None = None) -> FrontendModule:
31
+ chunk = self.decoder.decode(data, filename)
32
+ normalized_functions = normalized_functions_metadata(chunk.functions)
33
+ diagnostics = _normalization_diagnostics(normalized_functions)
34
+ return FrontendModule(
35
+ frontend_id=self.id,
36
+ payload=chunk,
37
+ metadata={
38
+ "filename": filename,
39
+ "format": "luac",
40
+ "version": chunk.header.version_label,
41
+ "endianness": (
42
+ None
43
+ if chunk.header.little_endian is None
44
+ else "little"
45
+ if chunk.header.little_endian
46
+ else "big"
47
+ ),
48
+ "debug_info_present": chunk.disassembly is not None,
49
+ "diagnostics": diagnostics,
50
+ "lua": {
51
+ "int_size": chunk.header.int_size,
52
+ "size_t_size": chunk.header.size_t_size,
53
+ "instruction_size": chunk.header.instruction_size,
54
+ "number_size": chunk.header.lua_number_size,
55
+ "integral_numbers": chunk.header.integral_numbers,
56
+ "decoder": chunk.decoder_id or self.decoder.id,
57
+ "decoder_policy": self.decoder.id,
58
+ "has_disassembly": chunk.disassembly is not None,
59
+ "normalized_functions": normalized_functions,
60
+ },
61
+ },
62
+ )
63
+
64
+ def lift(self, module: FrontendModule) -> ModuleIR:
65
+ if module.frontend_id != self.id:
66
+ raise TypeError(
67
+ f"Lua frontend cannot lift module from {module.frontend_id!r}"
68
+ )
69
+
70
+ return lift_lua_chunk(module.payload, module.metadata)
71
+
72
+
73
+ class Lua54BinaryChunkDecoder:
74
+ id = "lua54-binary"
75
+
76
+ def can_decode(self, data: bytes, filename: str | None = None) -> bool:
77
+ return looks_like_luac(data) and len(data) >= 8 and data[4] == 0x54
78
+
79
+ def decode(self, data: bytes, filename: str | None = None):
80
+ try:
81
+ return decode_lua54_chunk(data, filename)
82
+ except Lua54ChunkError as exc:
83
+ from unidecompiler_plugin_lua.luac import LuacDecodeError
84
+
85
+ raise LuacDecodeError(str(exc)) from exc
86
+
87
+
88
+ def _normalization_diagnostics(
89
+ normalized_functions: list[dict],
90
+ ) -> list[dict[str, object]]:
91
+ diagnostics: list[dict[str, object]] = []
92
+ for function in normalized_functions:
93
+ unsupported_opcodes = function["unsupported_opcodes"]
94
+ if not unsupported_opcodes:
95
+ continue
96
+ diagnostics.append(
97
+ {
98
+ "severity": "info",
99
+ "code": "lua.requires-cfg-structuring",
100
+ "function": function["name"],
101
+ "message": (
102
+ "Function contains Lua instructions that require CFG/"
103
+ "structuring before safe pseudocode lifting."
104
+ ),
105
+ "opcodes": unsupported_opcodes,
106
+ }
107
+ )
108
+ return diagnostics
@@ -0,0 +1,78 @@
1
+ """Optional Lua runtime facts for the decoupled generic IR simulator."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class LuaSimulationAdapter:
7
+ """Resolve Lua names and values without executing Lua bytecode."""
8
+
9
+ frontend_id = "lua"
10
+
11
+ def resolve_function(self, query, decoded_module, lifted_module):
12
+ from unidecompiler_simulator import NotHandled, ResolvedFunction
13
+
14
+ if not isinstance(query, str):
15
+ return NotHandled
16
+ matches = [
17
+ function
18
+ for function in self._walk(lifted_module.functions)
19
+ if function.name == query
20
+ ]
21
+ if len(matches) != 1:
22
+ return NotHandled
23
+ return ResolvedFunction(matches[0], identifier=query)
24
+
25
+ def resolve_global(self, name, context):
26
+ from unidecompiler_simulator import IntrinsicCall, NotHandled
27
+
28
+ if name == "vm_forloop_continues":
29
+ return IntrinsicCall("range_continues")
30
+ return NotHandled
31
+
32
+ def list_simulation_targets(self, decoded_module, lifted_module):
33
+ from unidecompiler_simulator import SimulationTargetCandidate
34
+
35
+ functions = tuple(self._walk(lifted_module.functions))
36
+ names = {function.name for function in functions}
37
+ return tuple(
38
+ SimulationTargetCandidate(function.name, function.name)
39
+ for function in functions
40
+ if sum(candidate.name == function.name for candidate in functions) == 1
41
+ and function.name in names
42
+ )
43
+
44
+ def unary_op(self, op, value, context):
45
+ from unidecompiler_simulator import NotHandled, TableValue
46
+
47
+ if op != "#":
48
+ return NotHandled
49
+ if isinstance(value, TableValue):
50
+ return len(value.array_items)
51
+ if isinstance(value, (list, str, tuple)):
52
+ return len(value)
53
+ return NotHandled
54
+
55
+ def get_item(self, obj, key, context):
56
+ from unidecompiler_simulator import NotHandled
57
+
58
+ if not isinstance(key, int):
59
+ return NotHandled
60
+ if isinstance(obj, list):
61
+ return obj[key - 1]
62
+ return NotHandled
63
+
64
+ def set_item(self, obj, key, value, context):
65
+ from unidecompiler_simulator import NotHandled
66
+
67
+ if not isinstance(key, int):
68
+ return NotHandled
69
+ if isinstance(obj, list):
70
+ obj[key - 1] = value
71
+ return None
72
+ return NotHandled
73
+
74
+ @staticmethod
75
+ def _walk(functions):
76
+ for function in functions:
77
+ yield function
78
+ yield from LuaSimulationAdapter._walk(function.nested_functions)
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from unidecompiler.plugins import FrontendVersionSupport
4
+
5
+
6
+ LUA_VERSION_SUPPORT = FrontendVersionSupport(
7
+ family="Lua bytecode",
8
+ versions=("5.4", "5.1 header/resource fallback"),
9
+ parser="internal Lua 5.4 chunk parser; header-only fallback for older chunks",
10
+ status="Lua 5.4 instruction submission; older chunks are detected but not lifted",
11
+ notes=(
12
+ "Adding Lua 5.1 support should add a Lua 5.1 decoder/opcode table in this frontend.",
13
+ "The frontend must still submit thin VM steps only.",
14
+ ),
15
+ )
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: unidecompiler-plugin-lua
3
+ Version: 0.1.1
4
+ Summary: Lua bytecode frontend plugin for unidecompiler
5
+ Author-email: Wker <1670133844@qq.com>
6
+ License-Expression: AGPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/Wker666/unidecompiler
8
+ Project-URL: Repository, https://github.com/Wker666/unidecompiler
9
+ Project-URL: Issues, https://github.com/Wker666/unidecompiler/issues
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: unidecompiler<0.2.0,>=0.1.1
13
+
14
+ # unidecompiler-plugin-lua
15
+
16
+ Frontend plugin for Lua `.luac` bytecode chunks. It decodes Lua bytecode and
17
+ submits neutral thin IR to `unidecompiler` for recovery and rendering.
18
+
19
+ Install with:
20
+
21
+ ```sh
22
+ python -m pip install unidecompiler-plugin-lua
23
+ ```
24
+
25
+ The plugin is discovered automatically by compatible CLI and GUI hosts.
@@ -0,0 +1,13 @@
1
+ unidecompiler_plugin_lua/__init__.py,sha256=ljweni-50I--HtwaMZFPbrGMQ5yW94FKaMUKCjnXT8I,30
2
+ unidecompiler_plugin_lua/chunk54.py,sha256=oWTI-UmkQrfaLqr_qvUM4JjLNSJc-by7jtbyk2RY5jo,19535
3
+ unidecompiler_plugin_lua/lifter.py,sha256=F--6eYjqEUFYNqjM68qDV4s23qR0MD6uOw6Eks-cTes,56041
4
+ unidecompiler_plugin_lua/luac.py,sha256=uvVv6OlIBtqqP-gHYge_paaXtkLj8vylUyptSM1kGCo,18201
5
+ unidecompiler_plugin_lua/normalize.py,sha256=1y0C9nFYXnAhAwbN61-8SW5nqVD0bdZ66OUZ8oLj_EE,6862
6
+ unidecompiler_plugin_lua/plugin.py,sha256=qeaapfHSEeRf9sPEENd_ZZVyOMHkdcq7mo53VfvqEUY,4186
7
+ unidecompiler_plugin_lua/simulation.py,sha256=K5rHem8blewEGFEY_MurKj7SncsisrS73cV65_70gFM,2575
8
+ unidecompiler_plugin_lua/support.py,sha256=I5hM8JubH3pCBGWpwoXVDXI_gO42eoIMfCyQ0qM9f_s,569
9
+ unidecompiler_plugin_lua-0.1.1.dist-info/METADATA,sha256=Mncv_aK4UrFp_Irl2CTDwOmRKglg6Qu-GOO8xJQt10Q,830
10
+ unidecompiler_plugin_lua-0.1.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
11
+ unidecompiler_plugin_lua-0.1.1.dist-info/entry_points.txt,sha256=kA7THgRbo2r_aAVM5z4MCM0DqPZ3xiTB4GQQMMEHwvg,82
12
+ unidecompiler_plugin_lua-0.1.1.dist-info/top_level.txt,sha256=D-0vATcYYrv-RckgazUaZyYJZcCAuth6N7H6hLtuSjw,25
13
+ unidecompiler_plugin_lua-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [unidecompiler.frontends]
2
+ lua = unidecompiler_plugin_lua.plugin:LuaFrontendPlugin
@@ -0,0 +1 @@
1
+ unidecompiler_plugin_lua