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.
@@ -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,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
+ python-pyc = unidecompiler_plugin_python_pyc.plugin:PythonPycFrontendPlugin
@@ -0,0 +1 @@
1
+ unidecompiler_plugin_python_pyc