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.
Files changed (82) hide show
  1. {declib-4.0.2 → declib-4.1.0}/PKG-INFO +2 -3
  2. {declib-4.0.2 → declib-4.1.0}/declib/__init__.py +1 -1
  3. {declib-4.0.2 → declib-4.1.0}/declib/api/decompiler_client.py +66 -0
  4. {declib-4.0.2 → declib-4.1.0}/declib/api/decompiler_interface.py +175 -0
  5. declib-4.1.0/declib/api/prototype.py +91 -0
  6. {declib-4.0.2 → declib-4.1.0}/declib/cli/decompiler_cli.py +912 -8
  7. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/angr/interface.py +12 -0
  8. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/binja/interface.py +108 -3
  9. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/compat/headless.py +21 -2
  10. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/interface.py +206 -36
  11. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ida/compat.py +302 -27
  12. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ida/interface.py +84 -11
  13. {declib-4.0.2 → declib-4.1.0}/declib/skills/decompiler/SKILL.md +189 -3
  14. {declib-4.0.2 → declib-4.1.0}/declib.egg-info/PKG-INFO +2 -3
  15. {declib-4.0.2 → declib-4.1.0}/declib.egg-info/SOURCES.txt +1 -0
  16. {declib-4.0.2 → declib-4.1.0}/pyproject.toml +2 -3
  17. {declib-4.0.2 → declib-4.1.0}/tests/test_decompiler_cli.py +556 -0
  18. {declib-4.0.2 → declib-4.1.0}/tests/test_decompilers.py +108 -0
  19. {declib-4.0.2 → declib-4.1.0}/LICENSE +0 -0
  20. {declib-4.0.2 → declib-4.1.0}/README.md +0 -0
  21. {declib-4.0.2 → declib-4.1.0}/declib/__main__.py +0 -0
  22. {declib-4.0.2 → declib-4.1.0}/declib/api/__init__.py +0 -0
  23. {declib-4.0.2 → declib-4.1.0}/declib/api/artifact_dict.py +0 -0
  24. {declib-4.0.2 → declib-4.1.0}/declib/api/artifact_lifter.py +0 -0
  25. {declib-4.0.2 → declib-4.1.0}/declib/api/decompiler_server.py +0 -0
  26. {declib-4.0.2 → declib-4.1.0}/declib/api/server_registry.py +0 -0
  27. {declib-4.0.2 → declib-4.1.0}/declib/api/type_definition_parser.py +0 -0
  28. {declib-4.0.2 → declib-4.1.0}/declib/api/type_parser.py +0 -0
  29. {declib-4.0.2 → declib-4.1.0}/declib/api/utils.py +0 -0
  30. {declib-4.0.2 → declib-4.1.0}/declib/artifacts/__init__.py +0 -0
  31. {declib-4.0.2 → declib-4.1.0}/declib/artifacts/artifact.py +0 -0
  32. {declib-4.0.2 → declib-4.1.0}/declib/artifacts/comment.py +0 -0
  33. {declib-4.0.2 → declib-4.1.0}/declib/artifacts/context.py +0 -0
  34. {declib-4.0.2 → declib-4.1.0}/declib/artifacts/decompilation.py +0 -0
  35. {declib-4.0.2 → declib-4.1.0}/declib/artifacts/enum.py +0 -0
  36. {declib-4.0.2 → declib-4.1.0}/declib/artifacts/formatting.py +0 -0
  37. {declib-4.0.2 → declib-4.1.0}/declib/artifacts/func.py +0 -0
  38. {declib-4.0.2 → declib-4.1.0}/declib/artifacts/global_variable.py +0 -0
  39. {declib-4.0.2 → declib-4.1.0}/declib/artifacts/patch.py +0 -0
  40. {declib-4.0.2 → declib-4.1.0}/declib/artifacts/segment.py +0 -0
  41. {declib-4.0.2 → declib-4.1.0}/declib/artifacts/stack_variable.py +0 -0
  42. {declib-4.0.2 → declib-4.1.0}/declib/artifacts/struct.py +0 -0
  43. {declib-4.0.2 → declib-4.1.0}/declib/artifacts/typedef.py +0 -0
  44. {declib-4.0.2 → declib-4.1.0}/declib/cli/__init__.py +0 -0
  45. {declib-4.0.2 → declib-4.1.0}/declib/configuration.py +0 -0
  46. {declib-4.0.2 → declib-4.1.0}/declib/decompiler_stubs/__init__.py +0 -0
  47. {declib-4.0.2 → declib-4.1.0}/declib/decompiler_stubs/angr_declib/__init__.py +0 -0
  48. {declib-4.0.2 → declib-4.1.0}/declib/decompiler_stubs/binja_declib/__init__.py +0 -0
  49. {declib-4.0.2 → declib-4.1.0}/declib/decompiler_stubs/ida_declib.py +0 -0
  50. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/__init__.py +0 -0
  51. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/angr/__init__.py +0 -0
  52. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/angr/artifact_lifter.py +0 -0
  53. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/angr/compat.py +0 -0
  54. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/binja/__init__.py +0 -0
  55. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/binja/artifact_lifter.py +0 -0
  56. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/binja/hooks.py +0 -0
  57. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/__init__.py +0 -0
  58. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/artifact_lifter.py +0 -0
  59. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/compat/__init__.py +0 -0
  60. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/compat/imports.py +0 -0
  61. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/compat/state.py +0 -0
  62. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/compat/transaction.py +0 -0
  63. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ghidra/hooks.py +0 -0
  64. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ida/__init__.py +0 -0
  65. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ida/artifact_lifter.py +0 -0
  66. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ida/hooks.py +0 -0
  67. {declib-4.0.2 → declib-4.1.0}/declib/decompilers/ida/ida_ui.py +0 -0
  68. {declib-4.0.2 → declib-4.1.0}/declib/logger.py +0 -0
  69. {declib-4.0.2 → declib-4.1.0}/declib/plugin_installer.py +0 -0
  70. {declib-4.0.2 → declib-4.1.0}/declib/skills/__init__.py +0 -0
  71. {declib-4.0.2 → declib-4.1.0}/declib/ui/__init__.py +0 -0
  72. {declib-4.0.2 → declib-4.1.0}/declib/ui/qt_objects.py +0 -0
  73. {declib-4.0.2 → declib-4.1.0}/declib/ui/utils.py +0 -0
  74. {declib-4.0.2 → declib-4.1.0}/declib/ui/version.py +0 -0
  75. {declib-4.0.2 → declib-4.1.0}/declib.egg-info/dependency_links.txt +0 -0
  76. {declib-4.0.2 → declib-4.1.0}/declib.egg-info/entry_points.txt +0 -0
  77. {declib-4.0.2 → declib-4.1.0}/declib.egg-info/requires.txt +0 -0
  78. {declib-4.0.2 → declib-4.1.0}/declib.egg-info/top_level.txt +0 -0
  79. {declib-4.0.2 → declib-4.1.0}/setup.cfg +0 -0
  80. {declib-4.0.2 → declib-4.1.0}/tests/test_artifacts.py +0 -0
  81. {declib-4.0.2 → declib-4.1.0}/tests/test_cli.py +0 -0
  82. {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.2
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 2 Clause
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
@@ -1,4 +1,4 @@
1
- __version__ = "4.0.2"
1
+ __version__ = "4.1.0"
2
2
 
3
3
 
4
4
  import logging
@@ -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})"