unidecompiler-plugin-python-pyc 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_python_pyc/__init__.py +2 -0
- unidecompiler_plugin_python_pyc/lifter.py +1085 -0
- unidecompiler_plugin_python_pyc/plugin.py +47 -0
- unidecompiler_plugin_python_pyc/pyc.py +136 -0
- unidecompiler_plugin_python_pyc/simulation.py +54 -0
- unidecompiler_plugin_python_pyc/support.py +15 -0
- unidecompiler_plugin_python_pyc-0.1.1.dist-info/METADATA +26 -0
- unidecompiler_plugin_python_pyc-0.1.1.dist-info/RECORD +11 -0
- unidecompiler_plugin_python_pyc-0.1.1.dist-info/WHEEL +5 -0
- unidecompiler_plugin_python_pyc-0.1.1.dist-info/entry_points.txt +2 -0
- unidecompiler_plugin_python_pyc-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unidecompiler.core.ir import ModuleIR
|
|
4
|
+
from unidecompiler.plugins import FrontendModule
|
|
5
|
+
from unidecompiler_plugin_python_pyc.lifter import lift_pyc_module
|
|
6
|
+
from unidecompiler_plugin_python_pyc.pyc import decode_pyc, looks_like_pyc
|
|
7
|
+
from unidecompiler_plugin_python_pyc.simulation import PythonPycSimulationAdapter
|
|
8
|
+
from unidecompiler_plugin_python_pyc.support import PYTHON_PYC_VERSION_SUPPORT
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PythonPycFrontendPlugin:
|
|
12
|
+
id = "python-pyc"
|
|
13
|
+
display_name = "Python bytecode"
|
|
14
|
+
supported_inputs = (".pyc",)
|
|
15
|
+
version_support = PYTHON_PYC_VERSION_SUPPORT
|
|
16
|
+
simulation_adapter = PythonPycSimulationAdapter
|
|
17
|
+
|
|
18
|
+
def can_load(self, data: bytes, filename: str | None = None) -> bool:
|
|
19
|
+
return looks_like_pyc(data)
|
|
20
|
+
|
|
21
|
+
def decode(self, data: bytes, filename: str | None = None) -> FrontendModule:
|
|
22
|
+
module = decode_pyc(data, filename)
|
|
23
|
+
return FrontendModule(
|
|
24
|
+
frontend_id=self.id,
|
|
25
|
+
payload=module,
|
|
26
|
+
metadata={
|
|
27
|
+
"filename": filename,
|
|
28
|
+
"format": "pyc",
|
|
29
|
+
"version": module.magic.hex(),
|
|
30
|
+
"endianness": "little",
|
|
31
|
+
"debug_info_present": True,
|
|
32
|
+
"diagnostics": [],
|
|
33
|
+
"python": {
|
|
34
|
+
"flags": module.flags,
|
|
35
|
+
"decoder": "stdlib-marshal-dis",
|
|
36
|
+
"root_name": module.code.name,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def lift(self, module: FrontendModule) -> ModuleIR:
|
|
42
|
+
if module.frontend_id != self.id:
|
|
43
|
+
raise TypeError(
|
|
44
|
+
f"Python pyc frontend cannot lift module from {module.frontend_id!r}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return lift_pyc_module(module.payload, module.metadata)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dis
|
|
4
|
+
import importlib.util
|
|
5
|
+
import marshal
|
|
6
|
+
import types
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from unidecompiler.plugins import FrontendDecodeError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
PYC_MIN_HEADER_SIZE = 16
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PycDecodeError(FrontendDecodeError):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class PycInstruction:
|
|
21
|
+
offset: int
|
|
22
|
+
opname: str
|
|
23
|
+
arg: int | None
|
|
24
|
+
argval: object
|
|
25
|
+
argrepr: str
|
|
26
|
+
starts_line: int | None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class PycExceptionRegion:
|
|
31
|
+
"""Decoded CPython exception-table entry expressed as bytecode offsets."""
|
|
32
|
+
|
|
33
|
+
start: int
|
|
34
|
+
end: int
|
|
35
|
+
target: int
|
|
36
|
+
depth: int
|
|
37
|
+
lasti: bool
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class PycCodeObject:
|
|
42
|
+
name: str
|
|
43
|
+
argcount: int
|
|
44
|
+
kwonlyargcount: int
|
|
45
|
+
flags: int
|
|
46
|
+
varnames: tuple[str, ...]
|
|
47
|
+
cellvars: tuple[str, ...]
|
|
48
|
+
freevars: tuple[str, ...]
|
|
49
|
+
names: tuple[str, ...]
|
|
50
|
+
consts: tuple[object, ...]
|
|
51
|
+
instructions: tuple[PycInstruction, ...]
|
|
52
|
+
exception_regions: tuple[PycExceptionRegion, ...]
|
|
53
|
+
children: tuple["PycCodeObject", ...]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class PycModule:
|
|
58
|
+
magic: bytes
|
|
59
|
+
flags: int
|
|
60
|
+
code: PycCodeObject
|
|
61
|
+
filename: str | None = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def looks_like_pyc(data: bytes) -> bool:
|
|
65
|
+
return len(data) > PYC_MIN_HEADER_SIZE and data[:4] == importlib.util.MAGIC_NUMBER and data[4:8] in {
|
|
66
|
+
b"\x00\x00\x00\x00",
|
|
67
|
+
b"\x01\x00\x00\x00",
|
|
68
|
+
b"\x03\x00\x00\x00",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def decode_pyc(data: bytes, filename: str | None = None) -> PycModule:
|
|
73
|
+
if len(data) <= PYC_MIN_HEADER_SIZE:
|
|
74
|
+
raise PycDecodeError("truncated pyc file")
|
|
75
|
+
|
|
76
|
+
flags = int.from_bytes(data[4:8], "little")
|
|
77
|
+
try:
|
|
78
|
+
code = marshal.loads(data[PYC_MIN_HEADER_SIZE:])
|
|
79
|
+
except Exception as error: # marshal gives several low-level exceptions.
|
|
80
|
+
raise PycDecodeError(f"failed to unmarshal pyc code object: {error}") from error
|
|
81
|
+
|
|
82
|
+
if not isinstance(code, types.CodeType):
|
|
83
|
+
raise PycDecodeError("pyc payload is not a code object")
|
|
84
|
+
|
|
85
|
+
return PycModule(
|
|
86
|
+
magic=data[:4],
|
|
87
|
+
flags=flags,
|
|
88
|
+
code=_decode_code_object(code),
|
|
89
|
+
filename=filename,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _decode_code_object(code: types.CodeType) -> PycCodeObject:
|
|
94
|
+
children = tuple(
|
|
95
|
+
_decode_code_object(const)
|
|
96
|
+
for const in code.co_consts
|
|
97
|
+
if isinstance(const, types.CodeType)
|
|
98
|
+
)
|
|
99
|
+
return PycCodeObject(
|
|
100
|
+
name=code.co_name,
|
|
101
|
+
argcount=code.co_argcount,
|
|
102
|
+
kwonlyargcount=code.co_kwonlyargcount,
|
|
103
|
+
flags=code.co_flags,
|
|
104
|
+
varnames=tuple(code.co_varnames),
|
|
105
|
+
cellvars=tuple(code.co_cellvars),
|
|
106
|
+
freevars=tuple(code.co_freevars),
|
|
107
|
+
names=tuple(code.co_names),
|
|
108
|
+
consts=tuple(
|
|
109
|
+
f"<code {const.co_name}>"
|
|
110
|
+
if isinstance(const, types.CodeType)
|
|
111
|
+
else const
|
|
112
|
+
for const in code.co_consts
|
|
113
|
+
),
|
|
114
|
+
instructions=tuple(
|
|
115
|
+
PycInstruction(
|
|
116
|
+
offset=instruction.offset,
|
|
117
|
+
opname=instruction.opname,
|
|
118
|
+
arg=instruction.arg,
|
|
119
|
+
argval=instruction.argval,
|
|
120
|
+
argrepr=instruction.argrepr,
|
|
121
|
+
starts_line=instruction.starts_line,
|
|
122
|
+
)
|
|
123
|
+
for instruction in dis.get_instructions(code)
|
|
124
|
+
),
|
|
125
|
+
exception_regions=tuple(
|
|
126
|
+
PycExceptionRegion(
|
|
127
|
+
start=entry.start,
|
|
128
|
+
end=entry.end,
|
|
129
|
+
target=entry.target,
|
|
130
|
+
depth=entry.depth,
|
|
131
|
+
lasti=entry.lasti,
|
|
132
|
+
)
|
|
133
|
+
for entry in dis.Bytecode(code).exception_entries
|
|
134
|
+
),
|
|
135
|
+
children=children,
|
|
136
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Optional Python function lookup for the decoupled generic IR simulator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PythonPycSimulationAdapter:
|
|
7
|
+
frontend_id = "python-pyc"
|
|
8
|
+
|
|
9
|
+
def resolve_function(self, query, decoded_module, lifted_module):
|
|
10
|
+
from unidecompiler_simulator import NotHandled, ResolvedFunction
|
|
11
|
+
|
|
12
|
+
if not isinstance(query, str):
|
|
13
|
+
return NotHandled
|
|
14
|
+
context = self._function_context(lifted_module.functions)
|
|
15
|
+
matches = context.get(query, ())
|
|
16
|
+
if len(matches) != 1:
|
|
17
|
+
return NotHandled
|
|
18
|
+
return ResolvedFunction(matches[0], context=context, identifier=query)
|
|
19
|
+
|
|
20
|
+
def resolve_global(self, name, context):
|
|
21
|
+
from unidecompiler_simulator import IntrinsicCall, NotHandled, ResolvedFunction
|
|
22
|
+
|
|
23
|
+
if name in {"len", "range", "iter_has_next", "iter_next"}:
|
|
24
|
+
return IntrinsicCall(name)
|
|
25
|
+
|
|
26
|
+
if not isinstance(context, dict):
|
|
27
|
+
return NotHandled
|
|
28
|
+
matches = context.get(name, ())
|
|
29
|
+
if len(matches) != 1:
|
|
30
|
+
return NotHandled
|
|
31
|
+
return ResolvedFunction(matches[0], context=context, identifier=name)
|
|
32
|
+
|
|
33
|
+
def list_simulation_targets(self, decoded_module, lifted_module):
|
|
34
|
+
from unidecompiler_simulator import SimulationTargetCandidate
|
|
35
|
+
|
|
36
|
+
context = self._function_context(lifted_module.functions)
|
|
37
|
+
return tuple(
|
|
38
|
+
SimulationTargetCandidate(name, name)
|
|
39
|
+
for name, matches in context.items()
|
|
40
|
+
if len(matches) == 1
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def _function_context(cls, functions):
|
|
45
|
+
context = {}
|
|
46
|
+
for function in cls._walk(functions):
|
|
47
|
+
context.setdefault(function.name, []).append(function)
|
|
48
|
+
return {name: tuple(matches) for name, matches in context.items()}
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def _walk(functions):
|
|
52
|
+
for function in functions:
|
|
53
|
+
yield function
|
|
54
|
+
yield from PythonPycSimulationAdapter._walk(function.nested_functions)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from unidecompiler.plugins import FrontendVersionSupport
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
PYTHON_PYC_VERSION_SUPPORT = FrontendVersionSupport(
|
|
10
|
+
family="CPython bytecode",
|
|
11
|
+
versions=(f"{sys.version_info.major}.{sys.version_info.minor}",),
|
|
12
|
+
parser="stdlib marshal/dis",
|
|
13
|
+
status="current interpreter magic decode + thin opcode submission",
|
|
14
|
+
notes=(f"magic={importlib.util.MAGIC_NUMBER.hex()}",),
|
|
15
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: unidecompiler-plugin-python-pyc
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python 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-python-pyc
|
|
15
|
+
|
|
16
|
+
Frontend plugin for Python `.pyc` bytecode files. It decodes Python bytecode
|
|
17
|
+
and submits neutral thin IR to `unidecompiler`, where recovery and pseudocode
|
|
18
|
+
rendering occur.
|
|
19
|
+
|
|
20
|
+
Install with:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
python -m pip install unidecompiler-plugin-python-pyc
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The plugin is discovered automatically by compatible CLI and GUI hosts.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
unidecompiler_plugin_python_pyc/__init__.py,sha256=hVoovcpsYUC1mNX2ZOw_gITFH8JIUelqdWQgjjzgNW4,29
|
|
2
|
+
unidecompiler_plugin_python_pyc/lifter.py,sha256=v4aPxsvUEbrfL4o3ZxF_VLhNVO3AaXF0fpAxbbQhwzY,38731
|
|
3
|
+
unidecompiler_plugin_python_pyc/plugin.py,sha256=xYGl2QaPOIQ8B3VQQSYC3SFLOeaA-VtN_uuD-d09yBk,1759
|
|
4
|
+
unidecompiler_plugin_python_pyc/pyc.py,sha256=0q43DH5fBOokSHi1FtgU1jsi8eRlTY7Xj-uae5_UkkU,3597
|
|
5
|
+
unidecompiler_plugin_python_pyc/simulation.py,sha256=_ZsytvY2-GpCHKcRKfL25ty2yLdEtPnT0tqkT4TVDWo,1965
|
|
6
|
+
unidecompiler_plugin_python_pyc/support.py,sha256=U44X6nRbjIKJKZoCtdCzmJ7ryShbGpUxGKytUrUAYRo,449
|
|
7
|
+
unidecompiler_plugin_python_pyc-0.1.1.dist-info/METADATA,sha256=qzsLA5mhEF42F-ElMP3LcngyAY7Ya9ypWOS8qS17XdI,878
|
|
8
|
+
unidecompiler_plugin_python_pyc-0.1.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
9
|
+
unidecompiler_plugin_python_pyc-0.1.1.dist-info/entry_points.txt,sha256=iV_iOT4G7hxzxMFz9NSZNpSNrn6XrGXkgepQU2fYvww,102
|
|
10
|
+
unidecompiler_plugin_python_pyc-0.1.1.dist-info/top_level.txt,sha256=WUNxCWlylGTlts-LVvZMQycGC4zmeBfASTYVoZWQAW8,32
|
|
11
|
+
unidecompiler_plugin_python_pyc-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
unidecompiler_plugin_python_pyc
|