declib 4.0.2__tar.gz → 4.1.0__tar.gz
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-4.0.2 → declib-4.1.0}/PKG-INFO +2 -3
- {declib-4.0.2 → declib-4.1.0}/declib/__init__.py +1 -1
- {declib-4.0.2 → declib-4.1.0}/declib/api/decompiler_client.py +66 -0
- {declib-4.0.2 → declib-4.1.0}/declib/api/decompiler_interface.py +175 -0
- declib-4.1.0/declib/api/prototype.py +91 -0
- {declib-4.0.2 → declib-4.1.0}/declib/cli/decompiler_cli.py +912 -8
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/angr/interface.py +12 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/binja/interface.py +108 -3
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/compat/headless.py +21 -2
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/interface.py +206 -36
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ida/compat.py +302 -27
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ida/interface.py +84 -11
- {declib-4.0.2 → declib-4.1.0}/declib/skills/decompiler/SKILL.md +189 -3
- {declib-4.0.2 → declib-4.1.0}/declib.egg-info/PKG-INFO +2 -3
- {declib-4.0.2 → declib-4.1.0}/declib.egg-info/SOURCES.txt +1 -0
- {declib-4.0.2 → declib-4.1.0}/pyproject.toml +2 -3
- {declib-4.0.2 → declib-4.1.0}/tests/test_decompiler_cli.py +556 -0
- {declib-4.0.2 → declib-4.1.0}/tests/test_decompilers.py +108 -0
- {declib-4.0.2 → declib-4.1.0}/LICENSE +0 -0
- {declib-4.0.2 → declib-4.1.0}/README.md +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/__main__.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/api/__init__.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/api/artifact_dict.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/api/artifact_lifter.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/api/decompiler_server.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/api/server_registry.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/api/type_definition_parser.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/api/type_parser.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/api/utils.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/artifacts/__init__.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/artifacts/artifact.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/artifacts/comment.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/artifacts/context.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/artifacts/decompilation.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/artifacts/enum.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/artifacts/formatting.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/artifacts/func.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/artifacts/global_variable.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/artifacts/patch.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/artifacts/segment.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/artifacts/stack_variable.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/artifacts/struct.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/artifacts/typedef.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/cli/__init__.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/configuration.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompiler_stubs/__init__.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompiler_stubs/angr_declib/__init__.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompiler_stubs/binja_declib/__init__.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompiler_stubs/ida_declib.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/__init__.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/angr/__init__.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/angr/artifact_lifter.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/angr/compat.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/binja/__init__.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/binja/artifact_lifter.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/binja/hooks.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/__init__.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/artifact_lifter.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/compat/__init__.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/compat/imports.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/compat/state.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/compat/transaction.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/hooks.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ida/__init__.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ida/artifact_lifter.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ida/hooks.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ida/ida_ui.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/logger.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/plugin_installer.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/skills/__init__.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/ui/__init__.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/ui/qt_objects.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/ui/utils.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib/ui/version.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib.egg-info/dependency_links.txt +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib.egg-info/entry_points.txt +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib.egg-info/requires.txt +0 -0
- {declib-4.0.2 → declib-4.1.0}/declib.egg-info/top_level.txt +0 -0
- {declib-4.0.2 → declib-4.1.0}/setup.cfg +0 -0
- {declib-4.0.2 → declib-4.1.0}/tests/test_artifacts.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/tests/test_cli.py +0 -0
- {declib-4.0.2 → declib-4.1.0}/tests/test_client_server.py +0 -0
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: declib
|
|
3
|
-
Version: 4.0
|
|
3
|
+
Version: 4.1.0
|
|
4
4
|
Summary: Your Only Decompiler API Lib - A generic API to script in and out of decompilers
|
|
5
|
-
License: BSD
|
|
5
|
+
License-Expression: BSD-2-Clause
|
|
6
6
|
Project-URL: Homepage, https://github.com/binsync/declib
|
|
7
|
-
Classifier: License :: OSI Approved :: BSD License
|
|
8
7
|
Classifier: Programming Language :: Python :: 3
|
|
9
8
|
Classifier: Programming Language :: Python :: 3.10
|
|
10
9
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -437,6 +437,8 @@ class DecompilerClient:
|
|
|
437
437
|
raise ValueError(error_msg)
|
|
438
438
|
elif error_type == "AttributeError":
|
|
439
439
|
raise AttributeError(error_msg)
|
|
440
|
+
elif error_type == "NotImplementedError":
|
|
441
|
+
raise NotImplementedError(error_msg)
|
|
440
442
|
else:
|
|
441
443
|
raise RuntimeError(f"{error_type}: {error_msg}")
|
|
442
444
|
|
|
@@ -562,6 +564,70 @@ class DecompilerClient:
|
|
|
562
564
|
def read_memory(self, addr: int, size: int) -> Optional[bytes]:
|
|
563
565
|
"""Read raw bytes from the loaded program."""
|
|
564
566
|
return self._send_request({"type": "method_call", "method_name": "read_memory", "args": [addr, size]})
|
|
567
|
+
|
|
568
|
+
def search_bytes(self, pattern: bytes, max_results: int = 100) -> List[int]:
|
|
569
|
+
"""Return lifted addresses where the byte pattern occurs."""
|
|
570
|
+
return self._send_request({"type": "method_call", "method_name": "search_bytes", "args": [pattern, max_results]})
|
|
571
|
+
|
|
572
|
+
def list_imports(self) -> List:
|
|
573
|
+
"""Return (lifted_addr, name, library) tuples for imported symbols."""
|
|
574
|
+
return self._send_request({"type": "method_call", "method_name": "list_imports"})
|
|
575
|
+
|
|
576
|
+
def backend_eval(self, code: str, mode: str = "eval") -> Dict:
|
|
577
|
+
"""UNSAFE: run arbitrary Python in the backend process. See the interface docstring."""
|
|
578
|
+
return self._send_request({"type": "method_call", "method_name": "backend_eval", "args": [code], "kwargs": {"mode": mode}})
|
|
579
|
+
|
|
580
|
+
def define_function(self, addr: int) -> bool:
|
|
581
|
+
"""Create a function at ``addr`` (lifted)."""
|
|
582
|
+
return self._send_request({"type": "method_call", "method_name": "define_function", "args": [addr]})
|
|
583
|
+
|
|
584
|
+
def define_code(self, addr: int) -> bool:
|
|
585
|
+
"""Turn the bytes at ``addr`` (lifted) into an instruction."""
|
|
586
|
+
return self._send_request({"type": "method_call", "method_name": "define_code", "args": [addr]})
|
|
587
|
+
|
|
588
|
+
def define_data(self, addr: int, type_str: Optional[str] = None, size: Optional[int] = None) -> bool:
|
|
589
|
+
"""Define data at ``addr`` (lifted), optionally with a C type."""
|
|
590
|
+
return self._send_request({"type": "method_call", "method_name": "define_data", "args": [addr, type_str, size]})
|
|
591
|
+
|
|
592
|
+
def undefine(self, addr: int, size: int = 1) -> bool:
|
|
593
|
+
"""Undefine code/data at ``addr`` (lifted)."""
|
|
594
|
+
return self._send_request({"type": "method_call", "method_name": "undefine", "args": [addr, size]})
|
|
595
|
+
|
|
596
|
+
def save(self, path: Optional[str] = None) -> bool:
|
|
597
|
+
"""Persist the backend's analysis to disk. Raises NotImplementedError for in-memory backends."""
|
|
598
|
+
return self._send_request({"type": "method_call", "method_name": "save", "args": [path]})
|
|
599
|
+
|
|
600
|
+
def set_persist_on_close(self, value: bool) -> bool:
|
|
601
|
+
"""Control whether the backend flushes to disk on teardown."""
|
|
602
|
+
return self._send_request({"type": "method_call", "method_name": "set_persist_on_close", "args": [value]})
|
|
603
|
+
|
|
604
|
+
def get_comment(self, addr: int) -> Optional[Comment]:
|
|
605
|
+
"""Return the Comment at ``addr`` (lifted), or None. Direct read, no cache."""
|
|
606
|
+
return self._send_request({"type": "method_call", "method_name": "get_comment", "args": [addr]})
|
|
607
|
+
|
|
608
|
+
def delete_comment(self, addr: int) -> bool:
|
|
609
|
+
"""Delete any comment at ``addr`` (lifted)."""
|
|
610
|
+
return self._send_request({"type": "method_call", "method_name": "delete_comment", "args": [addr]})
|
|
611
|
+
|
|
612
|
+
def get_global_var(self, addr: int) -> Optional[GlobalVariable]:
|
|
613
|
+
"""Return the GlobalVariable at ``addr`` (lifted), or None. Direct read, no cache."""
|
|
614
|
+
return self._send_request({"type": "method_call", "method_name": "get_global_var", "args": [addr]})
|
|
615
|
+
|
|
616
|
+
def get_patch(self, addr: int) -> Optional[Patch]:
|
|
617
|
+
"""Return the Patch at ``addr`` (lifted), or None. Direct read, no cache."""
|
|
618
|
+
return self._send_request({"type": "method_call", "method_name": "get_patch", "args": [addr]})
|
|
619
|
+
|
|
620
|
+
def delete_patch(self, addr: int) -> bool:
|
|
621
|
+
"""Revert the patch at ``addr`` (lifted)."""
|
|
622
|
+
return self._send_request({"type": "method_call", "method_name": "delete_patch", "args": [addr]})
|
|
623
|
+
|
|
624
|
+
def get_function_signature(self, func_addr: int) -> Optional[str]:
|
|
625
|
+
"""Return the C prototype string for the function at ``func_addr`` (lifted)."""
|
|
626
|
+
return self._send_request({"type": "method_call", "method_name": "get_function_signature", "args": [func_addr]})
|
|
627
|
+
|
|
628
|
+
def set_function_signature(self, func_addr: int, prototype: str) -> bool:
|
|
629
|
+
"""Apply a full C prototype (return type + argument types/names)."""
|
|
630
|
+
return self._send_request({"type": "method_call", "method_name": "set_function_signature", "args": [func_addr, prototype]})
|
|
565
631
|
|
|
566
632
|
def get_callgraph(self, only_names=False):
|
|
567
633
|
"""Get the call graph"""
|
|
@@ -528,6 +528,181 @@ class DecompilerInterface:
|
|
|
528
528
|
"""
|
|
529
529
|
raise NotImplementedError
|
|
530
530
|
|
|
531
|
+
def get_comment(self, addr) -> Optional[Comment]:
|
|
532
|
+
"""Return the lifted Comment at ``addr`` (lifted), or None.
|
|
533
|
+
|
|
534
|
+
Reads the backend directly rather than through the light-artifact
|
|
535
|
+
cache, so a comment set moments earlier is immediately visible.
|
|
536
|
+
"""
|
|
537
|
+
lowered = self.art_lifter.lower_addr(addr)
|
|
538
|
+
cmt = self._get_comment(lowered)
|
|
539
|
+
return self.art_lifter.lift(cmt) if cmt is not None else None
|
|
540
|
+
|
|
541
|
+
def delete_comment(self, addr) -> bool:
|
|
542
|
+
"""Delete any comment at ``addr`` (lifted). True if one was removed."""
|
|
543
|
+
lowered = self.art_lifter.lower_addr(addr)
|
|
544
|
+
return self._del_comment(lowered)
|
|
545
|
+
|
|
546
|
+
def get_global_var(self, addr) -> Optional[GlobalVariable]:
|
|
547
|
+
"""Return the lifted GlobalVariable at ``addr`` (lifted), or None.
|
|
548
|
+
|
|
549
|
+
Direct backend read (bypasses the light-artifact cache), so a global
|
|
550
|
+
renamed/retyped moments earlier is immediately visible.
|
|
551
|
+
"""
|
|
552
|
+
lowered = self.art_lifter.lower_addr(addr)
|
|
553
|
+
gvar = self._get_global_var(lowered)
|
|
554
|
+
return self.art_lifter.lift(gvar) if gvar is not None else None
|
|
555
|
+
|
|
556
|
+
def get_patch(self, addr) -> Optional[Patch]:
|
|
557
|
+
"""Return the lifted Patch at ``addr`` (lifted), or None (direct read)."""
|
|
558
|
+
lowered = self.art_lifter.lower_addr(addr)
|
|
559
|
+
patch = self._get_patch(lowered)
|
|
560
|
+
return self.art_lifter.lift(patch) if patch is not None else None
|
|
561
|
+
|
|
562
|
+
def delete_patch(self, addr) -> bool:
|
|
563
|
+
"""Revert the patch at ``addr`` (lifted). True if bytes were reverted."""
|
|
564
|
+
lowered = self.art_lifter.lower_addr(addr)
|
|
565
|
+
return self._del_patch(lowered)
|
|
566
|
+
|
|
567
|
+
def get_function_signature(self, func_addr) -> Optional[str]:
|
|
568
|
+
"""Return the C prototype string for the function at ``func_addr`` (lifted)."""
|
|
569
|
+
from declib.api.prototype import format_prototype
|
|
570
|
+
|
|
571
|
+
func = self.functions[func_addr]
|
|
572
|
+
if func is None or func.header is None:
|
|
573
|
+
return None
|
|
574
|
+
return format_prototype(func.header)
|
|
575
|
+
|
|
576
|
+
def set_function_signature(self, func_addr, prototype: str) -> bool:
|
|
577
|
+
"""Apply a full C prototype (return type + argument types/names).
|
|
578
|
+
|
|
579
|
+
The generic implementation parses the prototype and overwrites the
|
|
580
|
+
function's existing parameters positionally, then re-applies the
|
|
581
|
+
header through the normal set path. It retypes/renames the parameters
|
|
582
|
+
the backend already recognizes; it does not add or remove parameters.
|
|
583
|
+
Backends with an atomic prototype-set API (IDA's ``SetType``) override
|
|
584
|
+
this for fidelity.
|
|
585
|
+
"""
|
|
586
|
+
from declib.api.prototype import parse_prototype
|
|
587
|
+
|
|
588
|
+
ret_type, arg_specs = parse_prototype(prototype)
|
|
589
|
+
func = self.functions[func_addr]
|
|
590
|
+
if func is None or func.header is None:
|
|
591
|
+
return False
|
|
592
|
+
if ret_type:
|
|
593
|
+
func.header.type = ret_type
|
|
594
|
+
for (_off, arg), (atype, aname) in zip(sorted(func.header.args.items()), arg_specs):
|
|
595
|
+
if atype and atype != "...":
|
|
596
|
+
arg.type = atype
|
|
597
|
+
if aname:
|
|
598
|
+
arg.name = aname
|
|
599
|
+
return bool(self.set_artifact(func))
|
|
600
|
+
|
|
601
|
+
# ------------------------------------------------------------------
|
|
602
|
+
# Persistence
|
|
603
|
+
# ------------------------------------------------------------------
|
|
604
|
+
#: Whether the backend should flush its analysis to disk when the
|
|
605
|
+
#: interface is torn down (``shutdown()`` -> ``_deinit_headless_components``).
|
|
606
|
+
#: Backends override the default to match their native behavior (IDA
|
|
607
|
+
#: discards on close, Ghidra saves on close). ``set_persist_on_close``
|
|
608
|
+
#: flips it so ``decompiler stop --save/--discard`` can control the final
|
|
609
|
+
#: flush without an extra round-trip.
|
|
610
|
+
persist_on_close: bool = False
|
|
611
|
+
|
|
612
|
+
def save(self, path: Optional[str] = None) -> bool:
|
|
613
|
+
"""Persist the current analysis to the backend's on-disk database/project.
|
|
614
|
+
|
|
615
|
+
This is the durable-artifact primitive: after ``save()`` returns True,
|
|
616
|
+
renames/retypes/comments made through DecLib survive a stop + reload of
|
|
617
|
+
the same project directory. Backends that hold no persistent database
|
|
618
|
+
(e.g. angr, which is purely in-memory) raise ``NotImplementedError``.
|
|
619
|
+
|
|
620
|
+
@param path: Optional explicit output path. When ``None`` the backend
|
|
621
|
+
writes to whatever database/project it already has open.
|
|
622
|
+
@return: True on success.
|
|
623
|
+
"""
|
|
624
|
+
raise NotImplementedError("This backend does not support saving to disk.")
|
|
625
|
+
|
|
626
|
+
def set_persist_on_close(self, value: bool) -> bool:
|
|
627
|
+
"""Control whether teardown flushes analysis to disk.
|
|
628
|
+
|
|
629
|
+
Used by ``decompiler stop --save/--discard`` to decide the final flush
|
|
630
|
+
for backends (like Ghidra) that persist on close by default.
|
|
631
|
+
"""
|
|
632
|
+
self.persist_on_close = bool(value)
|
|
633
|
+
return True
|
|
634
|
+
|
|
635
|
+
def search_bytes(self, pattern: bytes, max_results: int = 100) -> List[int]:
|
|
636
|
+
"""Return lifted addresses where the raw byte ``pattern`` occurs.
|
|
637
|
+
|
|
638
|
+
Subclasses with a native byte-search API override this. The base
|
|
639
|
+
raises ``NotImplementedError`` so the CLI can report the capability as
|
|
640
|
+
unsupported for backends (e.g. angr) that lack one.
|
|
641
|
+
"""
|
|
642
|
+
raise NotImplementedError("Byte search is not implemented for this backend.")
|
|
643
|
+
|
|
644
|
+
# ------------------------------------------------------------------
|
|
645
|
+
# Code/data repair (define / undefine)
|
|
646
|
+
# ------------------------------------------------------------------
|
|
647
|
+
def define_function(self, addr) -> bool:
|
|
648
|
+
"""Create a function at ``addr`` (lifted). Repairs missed analysis."""
|
|
649
|
+
raise NotImplementedError("define_function is not implemented for this backend.")
|
|
650
|
+
|
|
651
|
+
def define_code(self, addr) -> bool:
|
|
652
|
+
"""Turn the bytes at ``addr`` (lifted) into an instruction."""
|
|
653
|
+
raise NotImplementedError("define_code is not implemented for this backend.")
|
|
654
|
+
|
|
655
|
+
def define_data(self, addr, type_str: Optional[str] = None, size: Optional[int] = None) -> bool:
|
|
656
|
+
"""Define data at ``addr`` (lifted), optionally with a C type."""
|
|
657
|
+
raise NotImplementedError("define_data is not implemented for this backend.")
|
|
658
|
+
|
|
659
|
+
def undefine(self, addr, size: int = 1) -> bool:
|
|
660
|
+
"""Undefine code/data at ``addr`` (lifted). Removes a function if one starts here."""
|
|
661
|
+
raise NotImplementedError("undefine is not implemented for this backend.")
|
|
662
|
+
|
|
663
|
+
def list_imports(self) -> List[Tuple[int, str, str]]:
|
|
664
|
+
"""Return ``(lifted_addr, name, library)`` tuples for imported symbols.
|
|
665
|
+
|
|
666
|
+
Subclasses override with a native import enumeration. The base raises
|
|
667
|
+
``NotImplementedError``.
|
|
668
|
+
"""
|
|
669
|
+
raise NotImplementedError("Import listing is not implemented for this backend.")
|
|
670
|
+
|
|
671
|
+
def backend_eval(self, code: str, mode: str = "eval") -> Dict:
|
|
672
|
+
"""UNSAFE escape hatch: run arbitrary Python inside the backend process.
|
|
673
|
+
|
|
674
|
+
The code executes with this interface bound to ``deci`` and full access
|
|
675
|
+
to the backend's own Python API (``idaapi``, the Ghidra ``flat_api`` via
|
|
676
|
+
``deci.flat_api``, ``deci.project`` for angr, ``deci.bv`` for Binary
|
|
677
|
+
Ninja, ...). This is deliberately backend-specific and not portable — it
|
|
678
|
+
exists for the cases the abstracted API doesn't cover.
|
|
679
|
+
|
|
680
|
+
@param code: Python source. ``mode='eval'`` evaluates a single
|
|
681
|
+
expression and returns its ``repr``; ``mode='exec'`` runs
|
|
682
|
+
statements, capturing stdout and any variable named ``result``.
|
|
683
|
+
@return: ``{"ok", "result", "stdout", "traceback"}``. (The failure key is
|
|
684
|
+
``traceback`` rather than ``error`` on purpose — the client/server
|
|
685
|
+
wire protocol reserves a top-level ``error`` key for RPC failures.)
|
|
686
|
+
"""
|
|
687
|
+
import io
|
|
688
|
+
import contextlib
|
|
689
|
+
import traceback as _traceback
|
|
690
|
+
|
|
691
|
+
env = {"deci": self, "__name__": "__declib_backend_eval__"}
|
|
692
|
+
out = io.StringIO()
|
|
693
|
+
try:
|
|
694
|
+
with contextlib.redirect_stdout(out):
|
|
695
|
+
if mode == "exec":
|
|
696
|
+
exec(code, env) # noqa: S102 - intentional escape hatch
|
|
697
|
+
value = env.get("result", None)
|
|
698
|
+
result = repr(value) if value is not None else ""
|
|
699
|
+
else:
|
|
700
|
+
result = repr(eval(code, env)) # noqa: S307 - intentional escape hatch
|
|
701
|
+
return {"ok": True, "result": result, "stdout": out.getvalue(), "traceback": None}
|
|
702
|
+
except Exception: # noqa: BLE001 - surface any backend error verbatim
|
|
703
|
+
return {"ok": False, "result": None, "stdout": out.getvalue(),
|
|
704
|
+
"traceback": _traceback.format_exc()}
|
|
705
|
+
|
|
531
706
|
def get_callgraph(self, only_names=False) -> nx.DiGraph:
|
|
532
707
|
"""
|
|
533
708
|
Returns the callgraph of the binary. This is a dict of function addresses to a list of function addresses
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Parsing and formatting of C function prototypes.
|
|
2
|
+
|
|
3
|
+
Shared by the CLI (``signature`` command) and the DecompilerInterface's
|
|
4
|
+
``set_function_signature`` so both agree on how a prototype string maps to a
|
|
5
|
+
return type + argument (type, name) pairs.
|
|
6
|
+
"""
|
|
7
|
+
import re
|
|
8
|
+
from typing import List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def split_c_args(body: str) -> List[str]:
|
|
12
|
+
"""Split a C argument list on top-level commas (respecting () <> [] nesting)."""
|
|
13
|
+
parts, depth, cur = [], 0, ""
|
|
14
|
+
for ch in body:
|
|
15
|
+
if ch in "(<[":
|
|
16
|
+
depth += 1
|
|
17
|
+
elif ch in ")>]":
|
|
18
|
+
depth -= 1
|
|
19
|
+
if ch == "," and depth == 0:
|
|
20
|
+
parts.append(cur)
|
|
21
|
+
cur = ""
|
|
22
|
+
else:
|
|
23
|
+
cur += ch
|
|
24
|
+
if cur.strip():
|
|
25
|
+
parts.append(cur)
|
|
26
|
+
return parts
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def norm_c_type(t: str) -> str:
|
|
30
|
+
"""Normalize whitespace and pointer spacing in a C type string."""
|
|
31
|
+
return re.sub(r"\s+", " ", t.replace("*", " * ")).replace(" * ", " *").strip()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_prototype(proto: str) -> Tuple[str, List[Tuple[str, Optional[str]]]]:
|
|
35
|
+
"""Parse ``ret name(argtype argname, ...)`` -> (return_type, [(type, name), ...]).
|
|
36
|
+
|
|
37
|
+
Handles pointer return/argument types, ``void`` argument lists, and varargs
|
|
38
|
+
(``...``). Argument names are optional. Raises ``ValueError`` on malformed
|
|
39
|
+
input.
|
|
40
|
+
"""
|
|
41
|
+
proto = proto.strip().rstrip(";").strip()
|
|
42
|
+
open_i = proto.find("(")
|
|
43
|
+
if open_i == -1:
|
|
44
|
+
raise ValueError("no argument list '(...)' found")
|
|
45
|
+
close_i = proto.rfind(")")
|
|
46
|
+
head = proto[:open_i].strip()
|
|
47
|
+
body = proto[open_i + 1:close_i].strip()
|
|
48
|
+
|
|
49
|
+
m = re.search(r"[A-Za-z_]\w*\s*$", head)
|
|
50
|
+
if not m:
|
|
51
|
+
raise ValueError(f"cannot find a function name in {head!r}")
|
|
52
|
+
ret_type = head[:m.start()].strip()
|
|
53
|
+
if not ret_type:
|
|
54
|
+
raise ValueError("missing return type")
|
|
55
|
+
|
|
56
|
+
args: List[Tuple[str, Optional[str]]] = []
|
|
57
|
+
if body and body != "void":
|
|
58
|
+
for raw in split_c_args(body):
|
|
59
|
+
raw = raw.strip()
|
|
60
|
+
if raw == "...":
|
|
61
|
+
args.append(("...", None))
|
|
62
|
+
continue
|
|
63
|
+
am = re.search(r"[A-Za-z_]\w*\s*$", raw)
|
|
64
|
+
if am and not raw.endswith("*"):
|
|
65
|
+
aname = am.group().strip()
|
|
66
|
+
atype = raw[:am.start()].strip()
|
|
67
|
+
if not atype: # bare type, no name (e.g. "int")
|
|
68
|
+
atype, aname = raw, None
|
|
69
|
+
else:
|
|
70
|
+
atype, aname = raw, None
|
|
71
|
+
args.append((norm_c_type(atype), aname))
|
|
72
|
+
return norm_c_type(ret_type), args
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def format_prototype(header) -> str:
|
|
76
|
+
"""Render a declib FunctionHeader as a C prototype string."""
|
|
77
|
+
ret = header.type or "void"
|
|
78
|
+
name = header.name or "sub"
|
|
79
|
+
parts = []
|
|
80
|
+
for _off, arg in sorted(header.args.items()):
|
|
81
|
+
atype = arg.type or "int"
|
|
82
|
+
aname = arg.name or ""
|
|
83
|
+
if not aname:
|
|
84
|
+
parts.append(atype)
|
|
85
|
+
else:
|
|
86
|
+
# Glue a pointer type to the name: "char *" + "b" -> "char *b".
|
|
87
|
+
sep = "" if atype.endswith("*") else " "
|
|
88
|
+
parts.append(f"{atype}{sep}{aname}")
|
|
89
|
+
arg_str = ", ".join(parts) if parts else "void"
|
|
90
|
+
ret_sep = "" if ret.endswith("*") else " "
|
|
91
|
+
return f"{ret}{ret_sep}{name}({arg_str})"
|