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,2 @@
1
+ """Lua bytecode frontend."""
2
+
@@ -0,0 +1,587 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import struct
5
+
6
+ from unidecompiler_plugin_lua.luac import (
7
+ LuaChunk,
8
+ LuaConstantListing,
9
+ LuacDecodeError,
10
+ LuacHeader,
11
+ LuaFunctionListing,
12
+ LuaInstructionListing,
13
+ LuaLocalListing,
14
+ LuaUpvalueListing,
15
+ )
16
+
17
+
18
+ LUA_SIGNATURE = b"\x1bLua"
19
+ LUA_54_VERSION = 0x54
20
+ LUAC_DATA = b"\x19\x93\r\n\x1a\n"
21
+ LUAC_INT = 0x5678
22
+ LUAC_NUM = 370.5
23
+
24
+
25
+ class Lua54ChunkError(LuacDecodeError):
26
+ pass
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class _ParsedFunction:
31
+ kind: str
32
+ source: str
33
+ line_start: int
34
+ line_end: int
35
+ param_count: int
36
+ slot_count: int
37
+ instructions: tuple[LuaInstructionListing, ...]
38
+ constants: tuple[LuaConstantListing, ...]
39
+ locals: tuple[LuaLocalListing, ...]
40
+ upvalues: tuple[LuaUpvalueListing, ...]
41
+ child_count: int
42
+ children: tuple["_ParsedFunction", ...]
43
+ inferred_name: str | None = None
44
+ child_function_names: tuple[str, ...] = ()
45
+
46
+
47
+ class _Reader:
48
+ def __init__(self, data: bytes) -> None:
49
+ self.data = data
50
+ self.offset = 0
51
+
52
+ def read(self, size: int) -> bytes:
53
+ end = self.offset + size
54
+ if end > len(self.data):
55
+ raise Lua54ChunkError("truncated chunk")
56
+ result = self.data[self.offset:end]
57
+ self.offset = end
58
+ return result
59
+
60
+ def byte(self) -> int:
61
+ return self.read(1)[0]
62
+
63
+ def varint(self) -> int:
64
+ value = 0
65
+ while True:
66
+ byte = self.byte()
67
+ value = (value << 7) | (byte & 0x7F)
68
+ if byte & 0x80:
69
+ return value
70
+
71
+ def unpack(self, fmt: str):
72
+ fmt = "<" + fmt
73
+ return struct.unpack(fmt, self.read(struct.calcsize(fmt)))[0]
74
+
75
+ def lua_integer(self, size: int) -> int:
76
+ if size == 8:
77
+ return int(self.unpack("q"))
78
+ if size == 4:
79
+ return int(self.unpack("i"))
80
+ raise Lua54ChunkError(f"unsupported lua integer size: {size}")
81
+
82
+ def lua_number(self, size: int) -> float:
83
+ if size == 8:
84
+ return float(self.unpack("d"))
85
+ if size == 4:
86
+ return float(self.unpack("f"))
87
+ raise Lua54ChunkError(f"unsupported lua number size: {size}")
88
+
89
+
90
+ _OPCODES = (
91
+ "MOVE", "LOADI", "LOADF", "LOADK", "LOADKX", "LOADFALSE", "LFALSESKIP", "LOADTRUE",
92
+ "LOADNIL", "GETUPVAL", "SETUPVAL", "GETTABUP", "GETTABLE", "GETI", "GETFIELD",
93
+ "SETTABUP", "SETTABLE", "SETI", "SETFIELD", "NEWTABLE", "SELF", "ADDI", "ADDK",
94
+ "SUBK", "MULK", "MODK", "POWK", "DIVK", "IDIVK", "BANDK", "BORK", "BXORK",
95
+ "SHRI", "SHLI", "ADD", "SUB", "MUL", "MOD", "POW", "DIV", "IDIV", "BAND",
96
+ "BOR", "BXOR", "SHL", "SHR", "MMBIN", "MMBINI", "MMBINK", "UNM", "BNOT", "NOT",
97
+ "LEN", "CONCAT", "CLOSE", "TBC", "JMP", "EQ", "LT", "LE", "EQK", "EQI", "LTI",
98
+ "LEI", "GTI", "GEI", "TEST", "TESTSET", "CALL", "TAILCALL", "RETURN", "RETURN0",
99
+ "RETURN1", "FORLOOP", "FORPREP", "TFORPREP", "TFORCALL", "TFORLOOP", "SETLIST",
100
+ "CLOSURE", "VARARG", "VARARGPREP", "EXTRAARG",
101
+ )
102
+
103
+
104
+ def decode_lua54_chunk(data: bytes, filename: str | None = None) -> LuaChunk:
105
+ reader = _Reader(data)
106
+ header = _read_header(reader)
107
+ reader.byte()
108
+ parsed = _read_function(reader, parent_source=None, kind="main")
109
+ functions = _flatten_and_infer(parsed)
110
+ return LuaChunk(
111
+ header=header,
112
+ raw=data,
113
+ filename=filename,
114
+ functions=tuple(_to_listing(function) for function in functions),
115
+ decoder_id="lua54-binary",
116
+ )
117
+
118
+
119
+ def _read_header(reader: _Reader) -> LuacHeader:
120
+ if reader.read(4) != LUA_SIGNATURE:
121
+ raise Lua54ChunkError("missing Lua chunk signature")
122
+ version = reader.byte()
123
+ if version != LUA_54_VERSION:
124
+ raise Lua54ChunkError(f"unsupported Lua bytecode version 0x{version:02x}")
125
+ fmt = reader.byte()
126
+ if fmt != 0:
127
+ raise Lua54ChunkError(f"unsupported Lua chunk format: {fmt}")
128
+ if reader.read(6) != LUAC_DATA:
129
+ raise Lua54ChunkError("corrupted Lua 5.4 chunk data")
130
+ instruction_size = reader.byte()
131
+ integer_size = reader.byte()
132
+ number_size = reader.byte()
133
+ if reader.lua_integer(integer_size) != LUAC_INT:
134
+ raise Lua54ChunkError("integer format mismatch")
135
+ if reader.lua_number(number_size) != LUAC_NUM:
136
+ raise Lua54ChunkError("number format mismatch")
137
+ return LuacHeader(
138
+ version=version,
139
+ format=fmt,
140
+ little_endian=True,
141
+ int_size=integer_size,
142
+ size_t_size=None,
143
+ instruction_size=instruction_size,
144
+ lua_number_size=number_size,
145
+ integral_numbers=False,
146
+ )
147
+
148
+
149
+ def _read_function(reader: _Reader, parent_source: str | None, kind: str) -> _ParsedFunction:
150
+ source = _read_string(reader) or parent_source or "<chunk>"
151
+ line_start = reader.varint()
152
+ line_end = reader.varint()
153
+ param_count = reader.byte()
154
+ reader.byte()
155
+ slot_count = reader.byte()
156
+ instructions = _read_code(reader)
157
+ constants = _read_constants(reader)
158
+ upvalue_descriptors = _read_upvalues(reader)
159
+ children = _read_children(reader, source)
160
+ line_info, locals_, upvalue_names = _read_debug(reader)
161
+ locals_ = _assign_local_register_slots(locals_, tuple(instructions), param_count)
162
+ upvalues = tuple(
163
+ LuaUpvalueListing(
164
+ index=index,
165
+ name=upvalue_names[index] if index < len(upvalue_names) else None,
166
+ instack=descriptor[0],
167
+ slot=descriptor[1],
168
+ kind=descriptor[2],
169
+ )
170
+ for index, descriptor in enumerate(upvalue_descriptors)
171
+ )
172
+ return _ParsedFunction(
173
+ kind=kind,
174
+ source=source,
175
+ line_start=line_start,
176
+ line_end=line_end,
177
+ param_count=param_count,
178
+ slot_count=slot_count,
179
+ instructions=tuple(
180
+ LuaInstructionListing(
181
+ pc=instruction.pc,
182
+ line=_line_for_pc(line_start, line_info, instruction.pc),
183
+ opcode=instruction.opcode,
184
+ operands=instruction.operands,
185
+ comment=_target_comment(instruction),
186
+ )
187
+ for instruction in instructions
188
+ ),
189
+ constants=constants,
190
+ locals=locals_,
191
+ upvalues=upvalues,
192
+ child_count=len(children),
193
+ children=children,
194
+ )
195
+
196
+
197
+ def _read_code(reader: _Reader) -> tuple[LuaInstructionListing, ...]:
198
+ count = reader.varint()
199
+ instructions: list[LuaInstructionListing] = []
200
+ for pc in range(1, count + 1):
201
+ raw = reader.unpack("I")
202
+ opcode = _OPCODES[raw & 0x7F]
203
+ instructions.append(
204
+ LuaInstructionListing(
205
+ pc=pc,
206
+ line=None,
207
+ opcode=opcode,
208
+ operands=_decode_operands(opcode, raw),
209
+ )
210
+ )
211
+ return tuple(instructions)
212
+
213
+
214
+ def _decode_operands(opcode: str, raw: int) -> tuple[str, ...]:
215
+ a = (raw >> 7) & 0xFF
216
+ k = (raw >> 15) & 0x1
217
+ b = (raw >> 16) & 0xFF
218
+ c = (raw >> 24) & 0xFF
219
+ sc = c - 127
220
+ sb = b - 127
221
+ bx = (raw >> 15) & 0x1FFFF
222
+ sx = bx - ((1 << 16) - 1)
223
+ sj = ((raw >> 7) & 0x1FFFFFF) - ((1 << 24) - 1)
224
+ if opcode in {"LOADI", "LOADF"}:
225
+ return (str(a), str(sx))
226
+ if opcode in {"FORLOOP", "FORPREP", "TFORPREP"}:
227
+ return (str(a), str(bx))
228
+ if opcode == "JMP":
229
+ return (str(sj),)
230
+ if opcode in {"RETURN0"}:
231
+ return ()
232
+ if opcode in {"RETURN1", "LOADFALSE", "LFALSESKIP", "LOADTRUE", "VARARGPREP", "TBC", "CLOSE"}:
233
+ return (str(a),)
234
+ if opcode in {"LOADK", "CLOSURE"}:
235
+ return (str(a), str(bx))
236
+ if opcode in {"ADDI", "SHRI", "SHLI"}:
237
+ return (str(a), str(b), str(sc))
238
+ if opcode in {"EQI", "LTI", "LEI", "GTI", "GEI"}:
239
+ return (str(a), str(sb), str(k))
240
+ if opcode in {"EQ", "LT", "LE", "EQK"}:
241
+ # These comparison opcodes use the iABCk layout. The final operand
242
+ # is the k flag, not the ordinary C field.
243
+ return (str(a), str(b), str(k))
244
+ if opcode == "TEST":
245
+ return (str(a), str(k))
246
+ if opcode == "TESTSET":
247
+ return (str(a), str(b), str(k))
248
+ if opcode == "CONCAT":
249
+ return (str(a), str(b))
250
+ if opcode == "MMBINI":
251
+ return (str(a), str(sb), str(c), str(k))
252
+ if opcode in {"MMBINK", "NEWTABLE"}:
253
+ return (str(a), str(b), str(c), str(k))
254
+ if opcode in {"SETTABLE", "SETI", "SETFIELD"}:
255
+ return (str(a), str(b), f"{c}k" if k else str(c))
256
+ return tuple(str(part) for part in (a, b, c))
257
+
258
+
259
+ def _read_constants(reader: _Reader) -> tuple[LuaConstantListing, ...]:
260
+ count = reader.varint()
261
+ constants: list[LuaConstantListing] = []
262
+ for index in range(count):
263
+ tag = reader.byte()
264
+ if tag == 0:
265
+ constants.append(LuaConstantListing(index=index, kind="N", value=None))
266
+ elif tag == 1:
267
+ constants.append(LuaConstantListing(index=index, kind="b", value=False))
268
+ elif tag == 17:
269
+ constants.append(LuaConstantListing(index=index, kind="b", value=True))
270
+ elif tag == 3:
271
+ constants.append(LuaConstantListing(index=index, kind="I", value=reader.lua_integer(8)))
272
+ elif tag == 19:
273
+ constants.append(LuaConstantListing(index=index, kind="F", value=reader.lua_number(8)))
274
+ elif tag in {4, 20}:
275
+ constants.append(LuaConstantListing(index=index, kind="S", value=_read_string(reader)))
276
+ else:
277
+ raise Lua54ChunkError(f"unsupported Lua 5.4 constant tag: {tag}")
278
+ return tuple(constants)
279
+
280
+
281
+ def _read_upvalues(reader: _Reader) -> tuple[tuple[int, int, int], ...]:
282
+ return tuple((reader.byte(), reader.byte(), reader.byte()) for _ in range(reader.varint()))
283
+
284
+
285
+ def _read_children(reader: _Reader, source: str) -> tuple[_ParsedFunction, ...]:
286
+ return tuple(_read_function(reader, source, "function") for _ in range(reader.varint()))
287
+
288
+
289
+ def _read_debug(
290
+ reader: _Reader,
291
+ ) -> tuple[tuple[int, ...], tuple[LuaLocalListing, ...], tuple[str | None, ...]]:
292
+ line_info = tuple(_signed_byte(reader.byte()) for _ in range(reader.varint()))
293
+ abs_count = reader.varint()
294
+ for _ in range(abs_count):
295
+ reader.varint()
296
+ reader.varint()
297
+ locals_: list[LuaLocalListing] = []
298
+ for slot in range(reader.varint()):
299
+ locals_.append(
300
+ LuaLocalListing(
301
+ slot=slot,
302
+ name=_read_string(reader) or f"local{slot}",
303
+ start_pc=reader.varint() + 1,
304
+ end_pc=reader.varint() + 1,
305
+ )
306
+ )
307
+ upvalue_names = tuple(_read_string(reader) for _ in range(reader.varint()))
308
+ return line_info, tuple(locals_), upvalue_names
309
+
310
+
311
+ def _assign_local_register_slots(
312
+ locals_: tuple[LuaLocalListing, ...],
313
+ instructions: tuple[LuaInstructionListing, ...],
314
+ param_count: int,
315
+ ) -> tuple[LuaLocalListing, ...]:
316
+ assigned: list[LuaLocalListing] = []
317
+ for index, local in enumerate(locals_):
318
+ slot = _infer_local_register_slot(local, locals_[:index], instructions, param_count, index)
319
+ assigned.append(
320
+ LuaLocalListing(
321
+ slot=slot,
322
+ name=local.name,
323
+ start_pc=local.start_pc,
324
+ end_pc=local.end_pc,
325
+ )
326
+ )
327
+ return tuple(assigned)
328
+
329
+
330
+ def _infer_local_register_slot(
331
+ local: LuaLocalListing,
332
+ previous_locals: tuple[LuaLocalListing, ...],
333
+ instructions: tuple[LuaInstructionListing, ...],
334
+ param_count: int,
335
+ ordinal: int,
336
+ ) -> int:
337
+ if local.start_pc <= 1 and ordinal < param_count:
338
+ return ordinal
339
+
340
+ current = _instruction_at_pc(instructions, local.start_pc)
341
+ if current is not None and current.opcode == "FORPREP" and current.operands:
342
+ same_for_state = sum(
343
+ 1
344
+ for previous in previous_locals
345
+ if previous.start_pc == local.start_pc and previous.name == "(for state)"
346
+ )
347
+ if local.name == "(for state)" and same_for_state < 3:
348
+ return int(current.operands[0]) + same_for_state
349
+
350
+ prior = _instruction_at_pc(instructions, local.start_pc - 1)
351
+ if prior is not None and prior.opcode == "FORPREP" and prior.operands and local.name != "(for state)":
352
+ same_visible = sum(
353
+ 1
354
+ for previous in previous_locals
355
+ if previous.start_pc == local.start_pc and previous.name != "(for state)"
356
+ )
357
+ return int(prior.operands[0]) + 3 + same_visible
358
+
359
+ write = _nearest_prior_register_write(local.start_pc, instructions)
360
+ if write is not None:
361
+ base, width = write
362
+ same_start = sum(1 for previous in previous_locals if previous.start_pc == local.start_pc)
363
+ if same_start < width:
364
+ return base + same_start
365
+ return base
366
+
367
+ return ordinal
368
+
369
+
370
+ def _instruction_at_pc(
371
+ instructions: tuple[LuaInstructionListing, ...],
372
+ pc: int,
373
+ ) -> LuaInstructionListing | None:
374
+ for instruction in instructions:
375
+ if instruction.pc == pc:
376
+ return instruction
377
+ return None
378
+
379
+
380
+ def _nearest_prior_register_write(
381
+ start_pc: int,
382
+ instructions: tuple[LuaInstructionListing, ...],
383
+ ) -> tuple[int, int] | None:
384
+ for instruction in reversed(tuple(instruction for instruction in instructions if instruction.pc < start_pc)):
385
+ write = _register_write(instruction)
386
+ if write is not None:
387
+ return write
388
+ return None
389
+
390
+
391
+ def _register_write(instruction: LuaInstructionListing) -> tuple[int, int] | None:
392
+ if not instruction.operands or not instruction.operands[0].lstrip("-").isdigit():
393
+ return None
394
+ base = int(instruction.operands[0])
395
+ if instruction.opcode == "LOADNIL" and len(instruction.operands) >= 2:
396
+ return base, int(instruction.operands[1]) + 1
397
+ if instruction.opcode == "CALL" and len(instruction.operands) >= 3:
398
+ count = int(instruction.operands[2]) - 1
399
+ return base, max(1, count)
400
+ if instruction.opcode == "SELF":
401
+ return base, 2
402
+ if instruction.opcode == "VARARG" and len(instruction.operands) >= 2:
403
+ count = int(instruction.operands[1]) - 1
404
+ return base, max(1, count)
405
+ if instruction.opcode in _REGISTER_DEST_OPS:
406
+ return base, 1
407
+ return None
408
+
409
+
410
+ _REGISTER_DEST_OPS = frozenset(
411
+ {
412
+ "ADDI",
413
+ "ADDK",
414
+ "ADD",
415
+ "BAND",
416
+ "BANDK",
417
+ "BNOT",
418
+ "BOR",
419
+ "BORK",
420
+ "BXOR",
421
+ "BXORK",
422
+ "CLOSURE",
423
+ "CONCAT",
424
+ "DIV",
425
+ "DIVK",
426
+ "GETFIELD",
427
+ "GETI",
428
+ "GETTABLE",
429
+ "GETTABUP",
430
+ "GETUPVAL",
431
+ "IDIV",
432
+ "IDIVK",
433
+ "LEN",
434
+ "LOADFALSE",
435
+ "LOADF",
436
+ "LOADI",
437
+ "LOADK",
438
+ "LOADKX",
439
+ "LOADTRUE",
440
+ "MOD",
441
+ "MODK",
442
+ "MUL",
443
+ "MULK",
444
+ "NEWTABLE",
445
+ "NOT",
446
+ "POW",
447
+ "POWK",
448
+ "SHL",
449
+ "SHLI",
450
+ "SHR",
451
+ "SHRI",
452
+ "SUB",
453
+ "SUBK",
454
+ "UNM",
455
+ }
456
+ )
457
+
458
+
459
+ def _read_string(reader: _Reader) -> str | None:
460
+ size = reader.varint()
461
+ if size == 0:
462
+ return None
463
+ return reader.read(size - 1).decode("utf-8", errors="replace")
464
+
465
+
466
+ def _flatten_and_infer(root: _ParsedFunction) -> tuple[_ParsedFunction, ...]:
467
+ functions: list[_ParsedFunction] = []
468
+
469
+ def visit(function: _ParsedFunction, inferred_name: str | None) -> None:
470
+ named = _replace_name(function, inferred_name)
471
+ parent_index = len(functions)
472
+ functions.append(named)
473
+ child_names = _child_names(named)
474
+ final_child_names: list[str] = []
475
+ for index, child in enumerate(named.children):
476
+ child_name = child_names.get(index) or f"<function_{len(functions)}>"
477
+ final_child_names.append(child_name)
478
+ visit(child, child_name)
479
+ functions[parent_index] = _replace_child_function_names(
480
+ named,
481
+ tuple(final_child_names),
482
+ )
483
+
484
+ visit(root, "<chunk>")
485
+ return tuple(functions)
486
+
487
+
488
+ def _replace_name(function: _ParsedFunction, name: str | None) -> _ParsedFunction:
489
+ return _ParsedFunction(
490
+ kind=function.kind,
491
+ source=function.source,
492
+ line_start=function.line_start,
493
+ line_end=function.line_end,
494
+ param_count=function.param_count,
495
+ slot_count=function.slot_count,
496
+ instructions=function.instructions,
497
+ constants=function.constants,
498
+ locals=function.locals,
499
+ upvalues=function.upvalues,
500
+ child_count=function.child_count,
501
+ children=function.children,
502
+ inferred_name=name,
503
+ child_function_names=function.child_function_names,
504
+ )
505
+
506
+
507
+ def _replace_child_function_names(function: _ParsedFunction, names: tuple[str, ...]) -> _ParsedFunction:
508
+ return _ParsedFunction(
509
+ kind=function.kind,
510
+ source=function.source,
511
+ line_start=function.line_start,
512
+ line_end=function.line_end,
513
+ param_count=function.param_count,
514
+ slot_count=function.slot_count,
515
+ instructions=function.instructions,
516
+ constants=function.constants,
517
+ locals=function.locals,
518
+ upvalues=function.upvalues,
519
+ child_count=function.child_count,
520
+ children=function.children,
521
+ inferred_name=function.inferred_name,
522
+ child_function_names=names,
523
+ )
524
+
525
+
526
+ def _child_names(function: _ParsedFunction) -> dict[int, str]:
527
+ names: dict[int, str] = {}
528
+ for instruction in function.instructions:
529
+ if instruction.opcode != "CLOSURE" or len(instruction.operands) < 2:
530
+ continue
531
+ register = int(instruction.operands[0])
532
+ child_index = int(instruction.operands[1])
533
+ local = _local_for_register(function.locals, register, instruction.pc + 1)
534
+ if local is not None:
535
+ names[child_index] = local.name
536
+ return names
537
+
538
+
539
+ def _to_listing(function: _ParsedFunction) -> LuaFunctionListing:
540
+ return LuaFunctionListing(
541
+ kind=function.kind,
542
+ source=function.source,
543
+ line_start=function.line_start,
544
+ line_end=function.line_end,
545
+ instruction_count=len(function.instructions),
546
+ param_count=function.param_count,
547
+ slot_count=function.slot_count,
548
+ upvalue_count=len(function.upvalues),
549
+ local_count=len(function.locals),
550
+ constant_count=len(function.constants),
551
+ child_function_count=function.child_count,
552
+ instructions=function.instructions,
553
+ constants=function.constants,
554
+ locals=function.locals,
555
+ upvalues=function.upvalues,
556
+ inferred_name=function.inferred_name,
557
+ child_function_names=function.child_function_names,
558
+ )
559
+
560
+
561
+ def _local_for_register(locals_: tuple[LuaLocalListing, ...], register: int, pc: int) -> LuaLocalListing | None:
562
+ for local in locals_:
563
+ if local.slot == register and local.start_pc <= pc < local.end_pc:
564
+ return local
565
+ return None
566
+
567
+
568
+ def _line_for_pc(line_start: int, line_info: tuple[int, ...], pc: int) -> int | None:
569
+ if 1 <= pc <= len(line_info):
570
+ return line_start + sum(line_info[:pc])
571
+ return None
572
+
573
+
574
+ def _target_comment(instruction: LuaInstructionListing) -> str | None:
575
+ if instruction.opcode not in {"JMP", "FORLOOP", "FORPREP", "TFORPREP"} or not instruction.operands:
576
+ return None
577
+ try:
578
+ offset = int(instruction.operands[-1])
579
+ except ValueError:
580
+ return None
581
+ if instruction.opcode == "FORLOOP":
582
+ return f"to {instruction.pc + 1 - offset}"
583
+ return f"to {instruction.pc + 1 + offset}"
584
+
585
+
586
+ def _signed_byte(value: int) -> int:
587
+ return value - 256 if value > 127 else value