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.
- unidecompiler_plugin_lua/__init__.py +2 -0
- unidecompiler_plugin_lua/chunk54.py +587 -0
- unidecompiler_plugin_lua/lifter.py +1631 -0
- unidecompiler_plugin_lua/luac.py +558 -0
- unidecompiler_plugin_lua/normalize.py +241 -0
- unidecompiler_plugin_lua/plugin.py +108 -0
- unidecompiler_plugin_lua/simulation.py +78 -0
- unidecompiler_plugin_lua/support.py +15 -0
- unidecompiler_plugin_lua-0.1.1.dist-info/METADATA +25 -0
- unidecompiler_plugin_lua-0.1.1.dist-info/RECORD +13 -0
- unidecompiler_plugin_lua-0.1.1.dist-info/WHEEL +5 -0
- unidecompiler_plugin_lua-0.1.1.dist-info/entry_points.txt +2 -0
- unidecompiler_plugin_lua-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
from dataclasses import dataclass, replace
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
from typing import Protocol
|
|
10
|
+
|
|
11
|
+
from unidecompiler.plugins import FrontendDecodeError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
LUA_SIGNATURE = b"\x1bLua"
|
|
15
|
+
LUA_51_VERSION = 0x51
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LuacDecodeError(FrontendDecodeError):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class LuacHeader:
|
|
24
|
+
version: int
|
|
25
|
+
format: int
|
|
26
|
+
little_endian: bool | None = None
|
|
27
|
+
int_size: int | None = None
|
|
28
|
+
size_t_size: int | None = None
|
|
29
|
+
instruction_size: int | None = None
|
|
30
|
+
lua_number_size: int | None = None
|
|
31
|
+
integral_numbers: bool | None = None
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def version_label(self) -> str:
|
|
35
|
+
major = self.version >> 4
|
|
36
|
+
minor = self.version & 0x0F
|
|
37
|
+
return f"{major}.{minor}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class LuaChunk:
|
|
42
|
+
header: LuacHeader
|
|
43
|
+
raw: bytes
|
|
44
|
+
filename: str | None = None
|
|
45
|
+
disassembly: str | None = None
|
|
46
|
+
functions: tuple["LuaFunctionListing", ...] = ()
|
|
47
|
+
decoder_id: str | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class LuaInstructionListing:
|
|
52
|
+
pc: int
|
|
53
|
+
line: int | None
|
|
54
|
+
opcode: str
|
|
55
|
+
operands: tuple[str, ...]
|
|
56
|
+
comment: str | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class LuaLocalListing:
|
|
61
|
+
slot: int
|
|
62
|
+
name: str
|
|
63
|
+
start_pc: int
|
|
64
|
+
end_pc: int
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class LuaUpvalueListing:
|
|
69
|
+
index: int
|
|
70
|
+
name: str | None = None
|
|
71
|
+
instack: int | None = None
|
|
72
|
+
slot: int | None = None
|
|
73
|
+
kind: int | None = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class LuaConstantListing:
|
|
78
|
+
index: int
|
|
79
|
+
kind: str
|
|
80
|
+
value: object
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class LuaFunctionListing:
|
|
85
|
+
kind: str
|
|
86
|
+
source: str
|
|
87
|
+
line_start: int | None
|
|
88
|
+
line_end: int | None
|
|
89
|
+
instruction_count: int
|
|
90
|
+
param_count: int
|
|
91
|
+
slot_count: int
|
|
92
|
+
upvalue_count: int
|
|
93
|
+
local_count: int
|
|
94
|
+
constant_count: int
|
|
95
|
+
child_function_count: int
|
|
96
|
+
instructions: tuple[LuaInstructionListing, ...]
|
|
97
|
+
constants: tuple[LuaConstantListing, ...]
|
|
98
|
+
locals: tuple[LuaLocalListing, ...]
|
|
99
|
+
upvalues: tuple[LuaUpvalueListing, ...] = ()
|
|
100
|
+
inferred_name: str | None = None
|
|
101
|
+
child_function_names: tuple[str, ...] = ()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class LuaChunkDecoder(Protocol):
|
|
105
|
+
"""Adapter seam for Lua bytecode decoding.
|
|
106
|
+
|
|
107
|
+
Implementations may use third-party libraries, official Lua tools, or a
|
|
108
|
+
small internal fallback. The Lua frontend plugin depends on this seam rather
|
|
109
|
+
than on one concrete parser.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
id: str
|
|
113
|
+
|
|
114
|
+
def can_decode(self, data: bytes, filename: str | None = None) -> bool:
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
def decode(self, data: bytes, filename: str | None = None) -> LuaChunk:
|
|
118
|
+
...
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def looks_like_luac(data: bytes) -> bool:
|
|
122
|
+
return data.startswith(LUA_SIGNATURE)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def decode_lua51_header(data: bytes) -> LuacHeader:
|
|
126
|
+
"""Decode a Lua 5.1 chunk header.
|
|
127
|
+
|
|
128
|
+
V1 intentionally pins Lua 5.1. Later frontends can add version-specific
|
|
129
|
+
decoders without changing the frontend plugin seam.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
if len(data) < 12:
|
|
133
|
+
raise LuacDecodeError("truncated Lua chunk header")
|
|
134
|
+
if not looks_like_luac(data):
|
|
135
|
+
raise LuacDecodeError("missing Lua chunk signature")
|
|
136
|
+
|
|
137
|
+
version = data[4]
|
|
138
|
+
if version != LUA_51_VERSION:
|
|
139
|
+
raise LuacDecodeError(
|
|
140
|
+
f"unsupported Lua bytecode version 0x{version:02x}; V1 supports 0x51"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
fmt = data[5]
|
|
144
|
+
if fmt != 0:
|
|
145
|
+
raise LuacDecodeError(f"unsupported Lua chunk format: {fmt}")
|
|
146
|
+
|
|
147
|
+
endianness = data[6]
|
|
148
|
+
if endianness not in (0, 1):
|
|
149
|
+
raise LuacDecodeError(f"invalid Lua 5.1 endianness flag: {endianness}")
|
|
150
|
+
|
|
151
|
+
return LuacHeader(
|
|
152
|
+
version=version,
|
|
153
|
+
format=fmt,
|
|
154
|
+
little_endian=endianness == 1,
|
|
155
|
+
int_size=data[7],
|
|
156
|
+
size_t_size=data[8],
|
|
157
|
+
instruction_size=data[9],
|
|
158
|
+
lua_number_size=data[10],
|
|
159
|
+
integral_numbers=data[11] == 1,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def decode_lua_header(data: bytes) -> LuacHeader:
|
|
164
|
+
if len(data) < 6:
|
|
165
|
+
raise LuacDecodeError("truncated Lua chunk header")
|
|
166
|
+
if not looks_like_luac(data):
|
|
167
|
+
raise LuacDecodeError("missing Lua chunk signature")
|
|
168
|
+
|
|
169
|
+
if data[4] == LUA_51_VERSION:
|
|
170
|
+
return decode_lua51_header(data)
|
|
171
|
+
|
|
172
|
+
return LuacHeader(version=data[4], format=data[5])
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def decode_lua51_chunk(data: bytes, filename: str | None = None) -> LuaChunk:
|
|
176
|
+
return LuaChunk(header=decode_lua51_header(data), raw=data, filename=filename)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
FUNCTION_HEADER_RE = re.compile(
|
|
180
|
+
r"^(main|function) <(?P<source>.*):(?P<start>\d+),(?P<end>\d+)> "
|
|
181
|
+
r"\((?P<instructions>\d+) instructions at .*\)$"
|
|
182
|
+
)
|
|
183
|
+
FUNCTION_STATS_RE = re.compile(
|
|
184
|
+
r"^(?P<params>\d+)\+? params?, (?P<slots>\d+) slots, "
|
|
185
|
+
r"(?P<upvalues>\d+) upvalues?, (?P<locals>\d+) locals?, "
|
|
186
|
+
r"(?P<constants>\d+) constants?, (?P<functions>\d+) functions?$"
|
|
187
|
+
)
|
|
188
|
+
INSTRUCTION_RE = re.compile(
|
|
189
|
+
r"^\s*(?P<pc>\d+)\s+\[(?P<line>-|\d+)\]\s+"
|
|
190
|
+
r"(?P<opcode>[A-Z0-9_]+)\s*(?P<rest>.*)$"
|
|
191
|
+
)
|
|
192
|
+
LOCAL_RE = re.compile(
|
|
193
|
+
r"^\s*(?P<slot>\d+)\s+(?P<name>\S+)\s+"
|
|
194
|
+
r"(?P<start>\d+)\s+(?P<end>\d+)\s*$"
|
|
195
|
+
)
|
|
196
|
+
CONSTANT_RE = re.compile(
|
|
197
|
+
r"^\s*(?P<index>\d+)\s+(?P<kind>\S+)\s*(?P<value>.*)$"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def parse_luac_listing(disassembly: str) -> tuple[LuaFunctionListing, ...]:
|
|
202
|
+
"""Parse the stable parts of ``luac -l -l`` output.
|
|
203
|
+
|
|
204
|
+
The official listing format is not a formal interchange format, so this
|
|
205
|
+
parser intentionally extracts only the pieces the first lifter needs:
|
|
206
|
+
function headers, instructions, and local-variable tables.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
lines = disassembly.splitlines()
|
|
210
|
+
functions: list[LuaFunctionListing] = []
|
|
211
|
+
index = 0
|
|
212
|
+
|
|
213
|
+
while index < len(lines):
|
|
214
|
+
header_match = FUNCTION_HEADER_RE.match(lines[index].strip())
|
|
215
|
+
if header_match is None:
|
|
216
|
+
index += 1
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
kind = lines[index].strip().split(" ", 1)[0]
|
|
220
|
+
index += 1
|
|
221
|
+
if index >= len(lines):
|
|
222
|
+
break
|
|
223
|
+
|
|
224
|
+
stats_match = FUNCTION_STATS_RE.match(lines[index].strip())
|
|
225
|
+
if stats_match is None:
|
|
226
|
+
index += 1
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
index += 1
|
|
230
|
+
instructions: list[LuaInstructionListing] = []
|
|
231
|
+
constants: list[LuaConstantListing] = []
|
|
232
|
+
locals_: list[LuaLocalListing] = []
|
|
233
|
+
upvalues: list[LuaUpvalueListing] = []
|
|
234
|
+
|
|
235
|
+
while index < len(lines):
|
|
236
|
+
line = lines[index]
|
|
237
|
+
if FUNCTION_HEADER_RE.match(line.strip()):
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
instruction_match = INSTRUCTION_RE.match(line)
|
|
241
|
+
if instruction_match is not None:
|
|
242
|
+
rest = instruction_match.group("rest").strip()
|
|
243
|
+
operands_text, _, comment_text = rest.partition(";")
|
|
244
|
+
instructions.append(
|
|
245
|
+
LuaInstructionListing(
|
|
246
|
+
pc=int(instruction_match.group("pc")),
|
|
247
|
+
line=(
|
|
248
|
+
None
|
|
249
|
+
if instruction_match.group("line") == "-"
|
|
250
|
+
else int(instruction_match.group("line"))
|
|
251
|
+
),
|
|
252
|
+
opcode=instruction_match.group("opcode"),
|
|
253
|
+
operands=tuple(operands_text.split()),
|
|
254
|
+
comment=comment_text.strip() or None,
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
index += 1
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
if line.strip().startswith("locals "):
|
|
261
|
+
index += 1
|
|
262
|
+
while index < len(lines):
|
|
263
|
+
local_match = LOCAL_RE.match(lines[index])
|
|
264
|
+
if local_match is None:
|
|
265
|
+
break
|
|
266
|
+
locals_.append(
|
|
267
|
+
LuaLocalListing(
|
|
268
|
+
slot=int(local_match.group("slot")),
|
|
269
|
+
name=local_match.group("name"),
|
|
270
|
+
start_pc=int(local_match.group("start")),
|
|
271
|
+
end_pc=int(local_match.group("end")),
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
index += 1
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
if line.strip().startswith("constants "):
|
|
278
|
+
index += 1
|
|
279
|
+
while index < len(lines):
|
|
280
|
+
constant_match = CONSTANT_RE.match(lines[index])
|
|
281
|
+
if constant_match is None:
|
|
282
|
+
break
|
|
283
|
+
constants.append(
|
|
284
|
+
LuaConstantListing(
|
|
285
|
+
index=int(constant_match.group("index")),
|
|
286
|
+
kind=constant_match.group("kind"),
|
|
287
|
+
value=_parse_lua_constant(
|
|
288
|
+
constant_match.group("kind"),
|
|
289
|
+
constant_match.group("value").strip(),
|
|
290
|
+
),
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
index += 1
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
if line.strip().startswith("upvalues "):
|
|
297
|
+
index += 1
|
|
298
|
+
upvalue_index = 0
|
|
299
|
+
while index < len(lines):
|
|
300
|
+
parts = lines[index].split()
|
|
301
|
+
if len(parts) < 4 or not parts[0].isdigit():
|
|
302
|
+
break
|
|
303
|
+
upvalues.append(
|
|
304
|
+
LuaUpvalueListing(
|
|
305
|
+
index=int(parts[0]),
|
|
306
|
+
name=parts[1],
|
|
307
|
+
instack=_parse_int(parts[2]),
|
|
308
|
+
slot=_parse_int(parts[3]),
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
upvalue_index += 1
|
|
312
|
+
index += 1
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
index += 1
|
|
316
|
+
|
|
317
|
+
functions.append(
|
|
318
|
+
LuaFunctionListing(
|
|
319
|
+
kind=kind,
|
|
320
|
+
source=header_match.group("source"),
|
|
321
|
+
line_start=int(header_match.group("start")),
|
|
322
|
+
line_end=int(header_match.group("end")),
|
|
323
|
+
instruction_count=int(header_match.group("instructions")),
|
|
324
|
+
param_count=int(stats_match.group("params")),
|
|
325
|
+
slot_count=int(stats_match.group("slots")),
|
|
326
|
+
upvalue_count=int(stats_match.group("upvalues")),
|
|
327
|
+
local_count=int(stats_match.group("locals")),
|
|
328
|
+
constant_count=int(stats_match.group("constants")),
|
|
329
|
+
child_function_count=int(stats_match.group("functions")),
|
|
330
|
+
instructions=tuple(instructions),
|
|
331
|
+
constants=tuple(constants),
|
|
332
|
+
locals=tuple(locals_),
|
|
333
|
+
upvalues=tuple(upvalues),
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return _infer_function_names(tuple(functions))
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _infer_function_names(
|
|
341
|
+
functions: tuple[LuaFunctionListing, ...],
|
|
342
|
+
) -> tuple[LuaFunctionListing, ...]:
|
|
343
|
+
if not functions:
|
|
344
|
+
return functions
|
|
345
|
+
|
|
346
|
+
names: list[str | None] = [None for _ in functions]
|
|
347
|
+
child_function_names: list[list[str]] = [[] for _ in functions]
|
|
348
|
+
names[0] = "<chunk>"
|
|
349
|
+
child_index = 1
|
|
350
|
+
|
|
351
|
+
for parent_index, parent in enumerate(functions):
|
|
352
|
+
for instruction in parent.instructions:
|
|
353
|
+
if instruction.opcode != "CLOSURE" or len(instruction.operands) < 1:
|
|
354
|
+
continue
|
|
355
|
+
if child_index >= len(functions):
|
|
356
|
+
break
|
|
357
|
+
register = _parse_int(instruction.operands[0])
|
|
358
|
+
if register is None:
|
|
359
|
+
child_index += 1
|
|
360
|
+
continue
|
|
361
|
+
local_name = _local_name_for_register(
|
|
362
|
+
parent.locals,
|
|
363
|
+
register=register,
|
|
364
|
+
pc=instruction.pc + 1,
|
|
365
|
+
)
|
|
366
|
+
if local_name is not None:
|
|
367
|
+
names[child_index] = local_name
|
|
368
|
+
child_function_names[parent_index].append(names[child_index] or f"<function_{child_index}>")
|
|
369
|
+
child_index += 1
|
|
370
|
+
|
|
371
|
+
return tuple(
|
|
372
|
+
LuaFunctionListing(
|
|
373
|
+
kind=function.kind,
|
|
374
|
+
source=function.source,
|
|
375
|
+
line_start=function.line_start,
|
|
376
|
+
line_end=function.line_end,
|
|
377
|
+
instruction_count=function.instruction_count,
|
|
378
|
+
param_count=function.param_count,
|
|
379
|
+
slot_count=function.slot_count,
|
|
380
|
+
upvalue_count=function.upvalue_count,
|
|
381
|
+
local_count=function.local_count,
|
|
382
|
+
constant_count=function.constant_count,
|
|
383
|
+
child_function_count=function.child_function_count,
|
|
384
|
+
instructions=function.instructions,
|
|
385
|
+
constants=function.constants,
|
|
386
|
+
locals=function.locals,
|
|
387
|
+
upvalues=function.upvalues,
|
|
388
|
+
inferred_name=names[index] or f"<function_{index}>",
|
|
389
|
+
child_function_names=tuple(child_function_names[index]),
|
|
390
|
+
)
|
|
391
|
+
for index, function in enumerate(functions)
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _local_name_for_register(
|
|
396
|
+
locals_: tuple[LuaLocalListing, ...],
|
|
397
|
+
register: int,
|
|
398
|
+
pc: int,
|
|
399
|
+
) -> str | None:
|
|
400
|
+
for local in locals_:
|
|
401
|
+
if local.slot == register and local.start_pc <= pc <= local.end_pc:
|
|
402
|
+
return local.name
|
|
403
|
+
return None
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _parse_int(value: str) -> int | None:
|
|
407
|
+
try:
|
|
408
|
+
return int(value)
|
|
409
|
+
except ValueError:
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _parse_lua_constant(kind: str, value: str) -> object:
|
|
414
|
+
if kind == "I":
|
|
415
|
+
return int(value)
|
|
416
|
+
if kind == "F":
|
|
417
|
+
return float(value)
|
|
418
|
+
if kind == "S":
|
|
419
|
+
if len(value) >= 2 and value[0] == '"' and value[-1] == '"':
|
|
420
|
+
return value[1:-1]
|
|
421
|
+
return value
|
|
422
|
+
if kind == "b":
|
|
423
|
+
return value.lower() == "true"
|
|
424
|
+
if kind == "N":
|
|
425
|
+
return None
|
|
426
|
+
return value
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class HeaderOnlyLuaChunkDecoder:
|
|
430
|
+
id = "lua-header-only"
|
|
431
|
+
|
|
432
|
+
def can_decode(self, data: bytes, filename: str | None = None) -> bool:
|
|
433
|
+
return looks_like_luac(data)
|
|
434
|
+
|
|
435
|
+
def decode(self, data: bytes, filename: str | None = None) -> LuaChunk:
|
|
436
|
+
return LuaChunk(
|
|
437
|
+
header=decode_lua_header(data),
|
|
438
|
+
raw=data,
|
|
439
|
+
filename=filename,
|
|
440
|
+
decoder_id=self.id,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
class LuacToolChunkDecoder:
|
|
445
|
+
"""Decode/disassemble chunks through the official ``luac`` executable."""
|
|
446
|
+
|
|
447
|
+
id = "luac-tool"
|
|
448
|
+
|
|
449
|
+
def __init__(self, luac_path: str = "luac") -> None:
|
|
450
|
+
self.luac_path = luac_path
|
|
451
|
+
|
|
452
|
+
def can_decode(self, data: bytes, filename: str | None = None) -> bool:
|
|
453
|
+
return looks_like_luac(data) and shutil.which(self.luac_path) is not None
|
|
454
|
+
|
|
455
|
+
def decode(self, data: bytes, filename: str | None = None) -> LuaChunk:
|
|
456
|
+
header = decode_lua_header(data)
|
|
457
|
+
luac = shutil.which(self.luac_path)
|
|
458
|
+
if luac is None:
|
|
459
|
+
raise LuacDecodeError(f"luac executable not found: {self.luac_path}")
|
|
460
|
+
|
|
461
|
+
with tempfile.TemporaryDirectory(prefix="unidecompiler-luac-") as temp_dir:
|
|
462
|
+
chunk_path = Path(temp_dir) / "input.luac"
|
|
463
|
+
chunk_path.write_bytes(data)
|
|
464
|
+
result = subprocess.run(
|
|
465
|
+
[luac, "-l", "-l", chunk_path.name],
|
|
466
|
+
check=False,
|
|
467
|
+
capture_output=True,
|
|
468
|
+
text=True,
|
|
469
|
+
cwd=temp_dir,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
if result.returncode != 0:
|
|
473
|
+
detail = result.stderr.strip() or result.stdout.strip()
|
|
474
|
+
raise LuacDecodeError(f"luac failed to decode chunk: {detail}")
|
|
475
|
+
|
|
476
|
+
return LuaChunk(
|
|
477
|
+
header=header,
|
|
478
|
+
raw=data,
|
|
479
|
+
filename=filename,
|
|
480
|
+
disassembly=result.stdout,
|
|
481
|
+
functions=parse_luac_listing(result.stdout),
|
|
482
|
+
decoder_id=self.id,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class LuaBytecodeLibraryChunkDecoder:
|
|
487
|
+
"""Adapter seam for importable Lua bytecode/chunk libraries.
|
|
488
|
+
|
|
489
|
+
This intentionally does not implement a binary chunk parser itself. The
|
|
490
|
+
project policy is that bytecode container parsing belongs to a maintained
|
|
491
|
+
frontend dependency/adapter, not to the generic pipeline. At the time this
|
|
492
|
+
adapter was added, no reliable Python package exposing Lua 5.4 chunk
|
|
493
|
+
functions/instructions/constants/locals was available in the environment,
|
|
494
|
+
so the adapter reports unavailable instead of pretending that source-level
|
|
495
|
+
parsers can decode luac bytecode.
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
id = "lua-bytecode-library-unavailable"
|
|
499
|
+
|
|
500
|
+
def can_decode(self, data: bytes, filename: str | None = None) -> bool:
|
|
501
|
+
return looks_like_luac(data) and self._available_backend_id() is not None
|
|
502
|
+
|
|
503
|
+
def decode(self, data: bytes, filename: str | None = None) -> LuaChunk:
|
|
504
|
+
backend_id = self._available_backend_id()
|
|
505
|
+
if backend_id is None:
|
|
506
|
+
raise LuacDecodeError(
|
|
507
|
+
"no supported importable Lua 5.4 bytecode parser is available"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
raise LuacDecodeError(
|
|
511
|
+
f"Lua bytecode library backend {backend_id!r} is not wired to the "
|
|
512
|
+
"LuaFunctionListing adapter yet"
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
def _available_backend_id(self) -> str | None:
|
|
516
|
+
return None
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
class PreferredLuaChunkDecoder:
|
|
520
|
+
"""Try importable library decoders before safe internal fallbacks.
|
|
521
|
+
|
|
522
|
+
The default path must not shell out to ``luac``. ``LuacToolChunkDecoder`` is
|
|
523
|
+
intentionally excluded here and may only be used by explicitly injecting it
|
|
524
|
+
into ``LuaFrontendPlugin`` or this preferred decoder.
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
id = "lua-library-preferred"
|
|
528
|
+
|
|
529
|
+
def __init__(self, decoders: tuple[LuaChunkDecoder, ...] | None = None) -> None:
|
|
530
|
+
if decoders is None:
|
|
531
|
+
from unidecompiler_plugin_lua.plugin import Lua54BinaryChunkDecoder
|
|
532
|
+
|
|
533
|
+
decoders = (
|
|
534
|
+
Lua54BinaryChunkDecoder(),
|
|
535
|
+
LuaBytecodeLibraryChunkDecoder(),
|
|
536
|
+
HeaderOnlyLuaChunkDecoder(),
|
|
537
|
+
)
|
|
538
|
+
self.decoders = decoders
|
|
539
|
+
|
|
540
|
+
def can_decode(self, data: bytes, filename: str | None = None) -> bool:
|
|
541
|
+
return any(decoder.can_decode(data, filename) for decoder in self.decoders)
|
|
542
|
+
|
|
543
|
+
def decode(self, data: bytes, filename: str | None = None) -> LuaChunk:
|
|
544
|
+
errors: list[str] = []
|
|
545
|
+
for decoder in self.decoders:
|
|
546
|
+
if not decoder.can_decode(data, filename):
|
|
547
|
+
continue
|
|
548
|
+
try:
|
|
549
|
+
chunk = decoder.decode(data, filename)
|
|
550
|
+
if chunk.decoder_id is None:
|
|
551
|
+
return replace(chunk, decoder_id=decoder.id)
|
|
552
|
+
return chunk
|
|
553
|
+
except LuacDecodeError as error:
|
|
554
|
+
errors.append(f"{decoder.id}: {error}")
|
|
555
|
+
|
|
556
|
+
if errors:
|
|
557
|
+
raise LuacDecodeError("; ".join(errors))
|
|
558
|
+
raise LuacDecodeError("no Lua decoder can decode this input")
|