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.
Files changed (71) hide show
  1. declib/__init__.py +9 -0
  2. declib/__main__.py +190 -0
  3. declib/api/__init__.py +13 -0
  4. declib/api/artifact_dict.py +153 -0
  5. declib/api/artifact_lifter.py +161 -0
  6. declib/api/decompiler_client.py +1219 -0
  7. declib/api/decompiler_interface.py +1261 -0
  8. declib/api/decompiler_server.py +782 -0
  9. declib/api/server_registry.py +171 -0
  10. declib/api/type_definition_parser.py +201 -0
  11. declib/api/type_parser.py +409 -0
  12. declib/api/utils.py +31 -0
  13. declib/artifacts/__init__.py +93 -0
  14. declib/artifacts/artifact.py +311 -0
  15. declib/artifacts/comment.py +49 -0
  16. declib/artifacts/context.py +61 -0
  17. declib/artifacts/decompilation.py +35 -0
  18. declib/artifacts/enum.py +53 -0
  19. declib/artifacts/formatting.py +27 -0
  20. declib/artifacts/func.py +433 -0
  21. declib/artifacts/global_variable.py +31 -0
  22. declib/artifacts/patch.py +49 -0
  23. declib/artifacts/segment.py +37 -0
  24. declib/artifacts/stack_variable.py +50 -0
  25. declib/artifacts/struct.py +184 -0
  26. declib/artifacts/typedef.py +59 -0
  27. declib/cli/__init__.py +3 -0
  28. declib/cli/decompiler_cli.py +1487 -0
  29. declib/configuration.py +184 -0
  30. declib/decompiler_stubs/__init__.py +0 -0
  31. declib/decompiler_stubs/angr_declib/__init__.py +4 -0
  32. declib/decompiler_stubs/binja_declib/__init__.py +4 -0
  33. declib/decompiler_stubs/ida_declib.py +8 -0
  34. declib/decompilers/__init__.py +8 -0
  35. declib/decompilers/angr/__init__.py +11 -0
  36. declib/decompilers/angr/artifact_lifter.py +46 -0
  37. declib/decompilers/angr/compat.py +262 -0
  38. declib/decompilers/angr/interface.py +949 -0
  39. declib/decompilers/binja/__init__.py +0 -0
  40. declib/decompilers/binja/artifact_lifter.py +32 -0
  41. declib/decompilers/binja/hooks.py +201 -0
  42. declib/decompilers/binja/interface.py +795 -0
  43. declib/decompilers/ghidra/__init__.py +0 -0
  44. declib/decompilers/ghidra/artifact_lifter.py +60 -0
  45. declib/decompilers/ghidra/compat/__init__.py +0 -0
  46. declib/decompilers/ghidra/compat/headless.py +156 -0
  47. declib/decompilers/ghidra/compat/imports.py +78 -0
  48. declib/decompilers/ghidra/compat/state.py +54 -0
  49. declib/decompilers/ghidra/compat/transaction.py +30 -0
  50. declib/decompilers/ghidra/hooks.py +242 -0
  51. declib/decompilers/ghidra/interface.py +1433 -0
  52. declib/decompilers/ida/__init__.py +0 -0
  53. declib/decompilers/ida/artifact_lifter.py +51 -0
  54. declib/decompilers/ida/compat.py +2054 -0
  55. declib/decompilers/ida/hooks.py +700 -0
  56. declib/decompilers/ida/ida_ui.py +80 -0
  57. declib/decompilers/ida/interface.py +659 -0
  58. declib/logger.py +101 -0
  59. declib/plugin_installer.py +259 -0
  60. declib/skills/__init__.py +24 -0
  61. declib/skills/decompiler/SKILL.md +316 -0
  62. declib/ui/__init__.py +33 -0
  63. declib/ui/qt_objects.py +146 -0
  64. declib/ui/utils.py +115 -0
  65. declib/ui/version.py +14 -0
  66. declib-3.8.0.dist-info/METADATA +138 -0
  67. declib-3.8.0.dist-info/RECORD +71 -0
  68. declib-3.8.0.dist-info/WHEEL +5 -0
  69. declib-3.8.0.dist-info/entry_points.txt +3 -0
  70. declib-3.8.0.dist-info/licenses/LICENSE +24 -0
  71. 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)