declib 3.8.0__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.
- declib/__init__.py +9 -0
- declib/__main__.py +190 -0
- declib/api/__init__.py +13 -0
- declib/api/artifact_dict.py +153 -0
- declib/api/artifact_lifter.py +161 -0
- declib/api/decompiler_client.py +1219 -0
- declib/api/decompiler_interface.py +1261 -0
- declib/api/decompiler_server.py +782 -0
- declib/api/server_registry.py +171 -0
- declib/api/type_definition_parser.py +201 -0
- declib/api/type_parser.py +409 -0
- declib/api/utils.py +31 -0
- declib/artifacts/__init__.py +93 -0
- declib/artifacts/artifact.py +311 -0
- declib/artifacts/comment.py +49 -0
- declib/artifacts/context.py +61 -0
- declib/artifacts/decompilation.py +35 -0
- declib/artifacts/enum.py +53 -0
- declib/artifacts/formatting.py +27 -0
- declib/artifacts/func.py +433 -0
- declib/artifacts/global_variable.py +31 -0
- declib/artifacts/patch.py +49 -0
- declib/artifacts/segment.py +37 -0
- declib/artifacts/stack_variable.py +50 -0
- declib/artifacts/struct.py +184 -0
- declib/artifacts/typedef.py +59 -0
- declib/cli/__init__.py +3 -0
- declib/cli/decompiler_cli.py +1487 -0
- declib/configuration.py +184 -0
- declib/decompiler_stubs/__init__.py +0 -0
- declib/decompiler_stubs/angr_declib/__init__.py +4 -0
- declib/decompiler_stubs/binja_declib/__init__.py +4 -0
- declib/decompiler_stubs/ida_declib.py +8 -0
- declib/decompilers/__init__.py +8 -0
- declib/decompilers/angr/__init__.py +11 -0
- declib/decompilers/angr/artifact_lifter.py +46 -0
- declib/decompilers/angr/compat.py +262 -0
- declib/decompilers/angr/interface.py +949 -0
- declib/decompilers/binja/__init__.py +0 -0
- declib/decompilers/binja/artifact_lifter.py +32 -0
- declib/decompilers/binja/hooks.py +201 -0
- declib/decompilers/binja/interface.py +795 -0
- declib/decompilers/ghidra/__init__.py +0 -0
- declib/decompilers/ghidra/artifact_lifter.py +60 -0
- declib/decompilers/ghidra/compat/__init__.py +0 -0
- declib/decompilers/ghidra/compat/headless.py +156 -0
- declib/decompilers/ghidra/compat/imports.py +78 -0
- declib/decompilers/ghidra/compat/state.py +54 -0
- declib/decompilers/ghidra/compat/transaction.py +30 -0
- declib/decompilers/ghidra/hooks.py +242 -0
- declib/decompilers/ghidra/interface.py +1433 -0
- declib/decompilers/ida/__init__.py +0 -0
- declib/decompilers/ida/artifact_lifter.py +51 -0
- declib/decompilers/ida/compat.py +2054 -0
- declib/decompilers/ida/hooks.py +700 -0
- declib/decompilers/ida/ida_ui.py +80 -0
- declib/decompilers/ida/interface.py +659 -0
- declib/logger.py +101 -0
- declib/plugin_installer.py +259 -0
- declib/skills/__init__.py +24 -0
- declib/skills/decompiler/SKILL.md +316 -0
- declib/ui/__init__.py +33 -0
- declib/ui/qt_objects.py +146 -0
- declib/ui/utils.py +115 -0
- declib/ui/version.py +14 -0
- declib-3.8.0.dist-info/METADATA +138 -0
- declib-3.8.0.dist-info/RECORD +71 -0
- declib-3.8.0.dist-info/WHEEL +5 -0
- declib-3.8.0.dist-info/entry_points.txt +3 -0
- declib-3.8.0.dist-info/licenses/LICENSE +24 -0
- declib-3.8.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Server registry for declib DecompilerServer instances.
|
|
3
|
+
|
|
4
|
+
Each running server writes a small JSON descriptor into a shared registry
|
|
5
|
+
directory so that the `decompiler` CLI (and DecompilerClient.discover) can
|
|
6
|
+
find, filter, and connect to the right server instance. Stale records
|
|
7
|
+
(servers whose process has exited or whose socket has vanished) are pruned
|
|
8
|
+
on read.
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import tempfile
|
|
14
|
+
import time
|
|
15
|
+
import uuid
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
import psutil
|
|
20
|
+
from platformdirs import user_state_dir
|
|
21
|
+
|
|
22
|
+
_l = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _registry_dir() -> Path:
|
|
26
|
+
"""Return the registry directory, creating it if missing."""
|
|
27
|
+
env_override = os.environ.get("DECLIB_SERVER_REGISTRY")
|
|
28
|
+
if env_override:
|
|
29
|
+
path = Path(env_override)
|
|
30
|
+
else:
|
|
31
|
+
path = Path(user_state_dir("declib")) / "servers"
|
|
32
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
return path
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def new_server_id() -> str:
|
|
37
|
+
"""Generate a short unique ID for a new server."""
|
|
38
|
+
return uuid.uuid4().hex[:10]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def default_socket_path(server_id: str) -> str:
|
|
42
|
+
"""Compute a default socket path for a server with the given ID."""
|
|
43
|
+
temp_dir = Path(tempfile.gettempdir()) / f"declib_server_{server_id}"
|
|
44
|
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
return str(temp_dir / "decompiler.sock")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def registry_path(server_id: str) -> Path:
|
|
49
|
+
return _registry_dir() / f"{server_id}.json"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def register_server(info: Dict) -> Path:
|
|
53
|
+
"""Write a server descriptor into the registry. Required keys: id, socket_path."""
|
|
54
|
+
server_id = info["id"]
|
|
55
|
+
path = registry_path(server_id)
|
|
56
|
+
payload = dict(info)
|
|
57
|
+
payload.setdefault("started_at", time.time())
|
|
58
|
+
payload.setdefault("pid", os.getpid())
|
|
59
|
+
tmp_path = path.with_suffix(".json.tmp")
|
|
60
|
+
with open(tmp_path, "w") as f:
|
|
61
|
+
json.dump(payload, f, indent=2, default=str)
|
|
62
|
+
os.replace(tmp_path, path)
|
|
63
|
+
return path
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def unregister_server(server_id: str) -> bool:
|
|
67
|
+
path = registry_path(server_id)
|
|
68
|
+
try:
|
|
69
|
+
path.unlink()
|
|
70
|
+
return True
|
|
71
|
+
except FileNotFoundError:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _is_record_live(record: Dict) -> bool:
|
|
76
|
+
pid = record.get("pid")
|
|
77
|
+
socket_path = record.get("socket_path")
|
|
78
|
+
if not socket_path or not os.path.exists(socket_path):
|
|
79
|
+
return False
|
|
80
|
+
if pid is not None:
|
|
81
|
+
try:
|
|
82
|
+
if not psutil.pid_exists(int(pid)):
|
|
83
|
+
return False
|
|
84
|
+
except Exception:
|
|
85
|
+
return False
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def list_servers(prune_stale: bool = True) -> List[Dict]:
|
|
90
|
+
"""Return all server records, optionally dropping and removing stale entries."""
|
|
91
|
+
records: List[Dict] = []
|
|
92
|
+
try:
|
|
93
|
+
entries = sorted(_registry_dir().glob("*.json"))
|
|
94
|
+
except FileNotFoundError:
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
for entry in entries:
|
|
98
|
+
try:
|
|
99
|
+
with open(entry, "r") as f:
|
|
100
|
+
record = json.load(f)
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
_l.debug("Failed to read server registry file %s: %s", entry, exc)
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
if prune_stale and not _is_record_live(record):
|
|
106
|
+
try:
|
|
107
|
+
entry.unlink()
|
|
108
|
+
except FileNotFoundError:
|
|
109
|
+
pass
|
|
110
|
+
except Exception as exc:
|
|
111
|
+
_l.debug("Failed to remove stale registry entry %s: %s", entry, exc)
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
records.append(record)
|
|
115
|
+
return records
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def find_server(
|
|
119
|
+
server_id: Optional[str] = None,
|
|
120
|
+
binary_path: Optional[str] = None,
|
|
121
|
+
binary_hash: Optional[str] = None,
|
|
122
|
+
backend: Optional[str] = None,
|
|
123
|
+
) -> Optional[Dict]:
|
|
124
|
+
"""Return the first server record matching all provided filters, else None."""
|
|
125
|
+
binary_path_resolved = str(Path(binary_path).expanduser().resolve()) if binary_path else None
|
|
126
|
+
for record in list_servers():
|
|
127
|
+
if server_id and record.get("id") != server_id:
|
|
128
|
+
continue
|
|
129
|
+
if binary_path_resolved:
|
|
130
|
+
record_path = record.get("binary_path")
|
|
131
|
+
if not record_path:
|
|
132
|
+
continue
|
|
133
|
+
try:
|
|
134
|
+
if str(Path(record_path).expanduser().resolve()) != binary_path_resolved:
|
|
135
|
+
continue
|
|
136
|
+
except Exception:
|
|
137
|
+
if record_path != binary_path_resolved:
|
|
138
|
+
continue
|
|
139
|
+
if binary_hash and record.get("binary_hash") != binary_hash:
|
|
140
|
+
continue
|
|
141
|
+
if backend and record.get("backend") != backend:
|
|
142
|
+
continue
|
|
143
|
+
return record
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def find_servers(
|
|
148
|
+
binary_path: Optional[str] = None,
|
|
149
|
+
binary_hash: Optional[str] = None,
|
|
150
|
+
backend: Optional[str] = None,
|
|
151
|
+
) -> List[Dict]:
|
|
152
|
+
"""Return all server records matching the provided filters."""
|
|
153
|
+
matches: List[Dict] = []
|
|
154
|
+
binary_path_resolved = str(Path(binary_path).expanduser().resolve()) if binary_path else None
|
|
155
|
+
for record in list_servers():
|
|
156
|
+
if binary_path_resolved:
|
|
157
|
+
record_path = record.get("binary_path")
|
|
158
|
+
if not record_path:
|
|
159
|
+
continue
|
|
160
|
+
try:
|
|
161
|
+
if str(Path(record_path).expanduser().resolve()) != binary_path_resolved:
|
|
162
|
+
continue
|
|
163
|
+
except Exception:
|
|
164
|
+
if record_path != binary_path_resolved:
|
|
165
|
+
continue
|
|
166
|
+
if binary_hash and record.get("binary_hash") != binary_hash:
|
|
167
|
+
continue
|
|
168
|
+
if backend and record.get("backend") != backend:
|
|
169
|
+
continue
|
|
170
|
+
matches.append(record)
|
|
171
|
+
return matches
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parse a single C type *definition* string into the matching declib artifact.
|
|
3
|
+
|
|
4
|
+
Unlike ``CTypeParser`` (declib/api/type_parser.py), which is deliberately scoped to
|
|
5
|
+
type *expressions* ("int *", "struct Foo *"), this module handles full type
|
|
6
|
+
*definitions* with bodies:
|
|
7
|
+
|
|
8
|
+
- ``struct Name { <members> };`` -> :class:`declib.artifacts.Struct`
|
|
9
|
+
- ``enum Name { A, B=5, C };`` -> :class:`declib.artifacts.Enum`
|
|
10
|
+
- ``typedef <type> Name;`` -> :class:`declib.artifacts.Typedef`
|
|
11
|
+
|
|
12
|
+
It is intentionally decompiler-free and unit-testable: the heavy lifting is done by
|
|
13
|
+
``pycparser`` (already a declib dependency) for the AST and member type-string
|
|
14
|
+
rendering, and by ``CTypeParser`` for member sizing. The resulting artifact is then
|
|
15
|
+
applied to a decompiler via the normal ``deci.structs[name] = struct`` /
|
|
16
|
+
``deci.set_artifact(...)`` path, which is portable across every backend.
|
|
17
|
+
"""
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
from typing import Optional, Union
|
|
21
|
+
|
|
22
|
+
import pycparser
|
|
23
|
+
from pycparser import c_ast, c_generator
|
|
24
|
+
from pycparser.c_parser import ParseError
|
|
25
|
+
|
|
26
|
+
from declib.artifacts import Struct, StructMember, Enum, Typedef
|
|
27
|
+
from declib.api.type_parser import CTypeParser
|
|
28
|
+
|
|
29
|
+
_l = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# Reuse single instances; both are stateless across parses.
|
|
32
|
+
_GENERATOR = c_generator.CGenerator()
|
|
33
|
+
_PARSER = pycparser.CParser()
|
|
34
|
+
_DEFAULT_TYPE_PARSER = CTypeParser()
|
|
35
|
+
|
|
36
|
+
# Member natural alignment is its own size in System V, capped at the platform
|
|
37
|
+
# word width (pointers/long are 8 in CTypeParser's defaults).
|
|
38
|
+
_MAX_ALIGN = 8
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TypeDefinitionParseError(ValueError):
|
|
42
|
+
"""Raised when a C type-definition string cannot be turned into a declib artifact."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_type_definition(
|
|
46
|
+
text: str,
|
|
47
|
+
type_parser: Optional[CTypeParser] = None,
|
|
48
|
+
) -> Union[Struct, Enum, Typedef]:
|
|
49
|
+
"""
|
|
50
|
+
Parse a single C type *definition* into the matching declib artifact.
|
|
51
|
+
|
|
52
|
+
Supports exactly one top-level definition: a named ``struct``, ``enum``, or
|
|
53
|
+
``typedef``. Raises :class:`TypeDefinitionParseError` on anything unparseable,
|
|
54
|
+
anonymous, multi-definition, or otherwise unsupported.
|
|
55
|
+
|
|
56
|
+
>>> parse_type_definition("struct Point { int x; int y; }")
|
|
57
|
+
<Struct: Point membs=2 (0x8)>
|
|
58
|
+
"""
|
|
59
|
+
tp = type_parser or _DEFAULT_TYPE_PARSER
|
|
60
|
+
ast = _parse_ast(_normalize(text))
|
|
61
|
+
top = ast.ext[0]
|
|
62
|
+
|
|
63
|
+
if isinstance(top, c_ast.Typedef):
|
|
64
|
+
return _typedef_from_ast(top)
|
|
65
|
+
|
|
66
|
+
# struct/enum arrive wrapped in a Decl whose .type is the Struct/Enum node
|
|
67
|
+
if isinstance(top, c_ast.Decl):
|
|
68
|
+
inner = top.type
|
|
69
|
+
if isinstance(inner, c_ast.Struct):
|
|
70
|
+
return _struct_from_ast(inner, tp)
|
|
71
|
+
if isinstance(inner, c_ast.Enum):
|
|
72
|
+
return _enum_from_ast(inner, tp)
|
|
73
|
+
|
|
74
|
+
raise TypeDefinitionParseError(
|
|
75
|
+
f"Unsupported top-level definition: {type(top).__name__}. "
|
|
76
|
+
"Expected a named struct, enum, or typedef."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _normalize(text: str) -> str:
|
|
81
|
+
if not text or not text.strip():
|
|
82
|
+
raise TypeDefinitionParseError("Empty type definition.")
|
|
83
|
+
# strip C comments (same approach as CTypeParser.parse_type_with_name)
|
|
84
|
+
text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL)
|
|
85
|
+
text = re.sub(r"//.*?$", "", text, flags=re.MULTILINE)
|
|
86
|
+
text = text.strip()
|
|
87
|
+
if not text.endswith(";"):
|
|
88
|
+
text += ";"
|
|
89
|
+
return text
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _parse_ast(text: str) -> c_ast.FileAST:
|
|
93
|
+
try:
|
|
94
|
+
ast = _PARSER.parse(text)
|
|
95
|
+
except ParseError as exc:
|
|
96
|
+
raise TypeDefinitionParseError(f"could not parse C definition: {exc}")
|
|
97
|
+
if not ast.ext:
|
|
98
|
+
raise TypeDefinitionParseError("no type definition found.")
|
|
99
|
+
if len(ast.ext) != 1:
|
|
100
|
+
raise TypeDefinitionParseError(
|
|
101
|
+
"expected exactly one type definition, got "
|
|
102
|
+
f"{len(ast.ext)}. Define one type at a time."
|
|
103
|
+
)
|
|
104
|
+
return ast
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _render_type(node) -> str:
|
|
108
|
+
"""Render a member/typedef type node back to a C type string, e.g. "char *"."""
|
|
109
|
+
rendered = _GENERATOR.visit(node).strip()
|
|
110
|
+
if "\n" in rendered or "{" in rendered:
|
|
111
|
+
raise TypeDefinitionParseError(
|
|
112
|
+
"inline/nested type definitions are unsupported here; define the "
|
|
113
|
+
"inner type separately and reference it by name."
|
|
114
|
+
)
|
|
115
|
+
return rendered
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _member_size(tp: CTypeParser, type_str: str) -> int:
|
|
119
|
+
ct = tp.parse_type(type_str)
|
|
120
|
+
if ct is None or not ct.size:
|
|
121
|
+
# Unknown, user-defined non-pointer type (e.g. "struct Bar" before Bar
|
|
122
|
+
# exists): we cannot reliably size it, so reject rather than emit a
|
|
123
|
+
# 0-size member that would corrupt every subsequent offset.
|
|
124
|
+
raise TypeDefinitionParseError(
|
|
125
|
+
f"could not determine the size of member type {type_str!r}. "
|
|
126
|
+
"Define referenced types first, or use a pointer/primitive."
|
|
127
|
+
)
|
|
128
|
+
return ct.size
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _struct_from_ast(struct_node: c_ast.Struct, tp: CTypeParser) -> Struct:
|
|
132
|
+
if not struct_node.name:
|
|
133
|
+
raise TypeDefinitionParseError(
|
|
134
|
+
"anonymous structs are not supported; give the struct a name."
|
|
135
|
+
)
|
|
136
|
+
if not struct_node.decls:
|
|
137
|
+
raise TypeDefinitionParseError(
|
|
138
|
+
f"struct {struct_node.name!r} has no members to define."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
members = {}
|
|
142
|
+
offset = 0
|
|
143
|
+
max_align = 1
|
|
144
|
+
for decl in struct_node.decls:
|
|
145
|
+
if decl.name is None:
|
|
146
|
+
raise TypeDefinitionParseError(
|
|
147
|
+
f"unnamed member in struct {struct_node.name!r} is unsupported."
|
|
148
|
+
)
|
|
149
|
+
type_str = _render_type(decl.type)
|
|
150
|
+
size = _member_size(tp, type_str)
|
|
151
|
+
align = min(size, _MAX_ALIGN) if size else 1
|
|
152
|
+
# round the running offset up to this member's natural alignment
|
|
153
|
+
if align > 1 and offset % align:
|
|
154
|
+
offset += align - (offset % align)
|
|
155
|
+
members[offset] = StructMember(
|
|
156
|
+
name=decl.name, offset=offset, type_=type_str, size=size,
|
|
157
|
+
)
|
|
158
|
+
offset += size
|
|
159
|
+
max_align = max(max_align, align)
|
|
160
|
+
|
|
161
|
+
total = offset
|
|
162
|
+
if max_align > 1 and total % max_align:
|
|
163
|
+
total += max_align - (total % max_align)
|
|
164
|
+
|
|
165
|
+
return Struct(name=struct_node.name, size=total, members=members)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _enum_from_ast(enum_node: c_ast.Enum, tp: CTypeParser) -> Enum:
|
|
169
|
+
if not enum_node.name:
|
|
170
|
+
raise TypeDefinitionParseError(
|
|
171
|
+
"anonymous enums are not supported; give the enum a name."
|
|
172
|
+
)
|
|
173
|
+
if not enum_node.values or not enum_node.values.enumerators:
|
|
174
|
+
raise TypeDefinitionParseError(
|
|
175
|
+
f"enum {enum_node.name!r} has no members to define."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
members = {}
|
|
179
|
+
next_val = 0
|
|
180
|
+
for en in enum_node.values.enumerators:
|
|
181
|
+
if en.value is None:
|
|
182
|
+
val = next_val
|
|
183
|
+
else:
|
|
184
|
+
try:
|
|
185
|
+
val = tp._parse_const(en.value)
|
|
186
|
+
except Exception:
|
|
187
|
+
raise TypeDefinitionParseError(
|
|
188
|
+
f"could not evaluate enum value for {en.name!r}."
|
|
189
|
+
)
|
|
190
|
+
members[en.name] = val
|
|
191
|
+
next_val = val + 1
|
|
192
|
+
|
|
193
|
+
return Enum(name=enum_node.name, members=members)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _typedef_from_ast(typedef_node: c_ast.Typedef) -> Typedef:
|
|
197
|
+
name = typedef_node.name
|
|
198
|
+
if not name:
|
|
199
|
+
raise TypeDefinitionParseError("typedef is missing a name.")
|
|
200
|
+
type_str = _render_type(typedef_node.type)
|
|
201
|
+
return Typedef(name=name, type_=type_str)
|