Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions declib/api/decompiler_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,14 @@ def save(self, path: Optional[str] = None) -> bool:
def set_persist_on_close(self, value: bool) -> bool:
"""Control whether the backend flushes to disk on teardown."""
return self._send_request({"type": "method_call", "method_name": "set_persist_on_close", "args": [value]})

def get_comment(self, addr: int) -> Optional[Comment]:
"""Return the Comment at ``addr`` (lifted), or None. Direct read, no cache."""
return self._send_request({"type": "method_call", "method_name": "get_comment", "args": [addr]})

def delete_comment(self, addr: int) -> bool:
"""Delete any comment at ``addr`` (lifted)."""
return self._send_request({"type": "method_call", "method_name": "delete_comment", "args": [addr]})

def get_callgraph(self, only_names=False):
"""Get the call graph"""
Expand Down
15 changes: 15 additions & 0 deletions declib/api/decompiler_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,21 @@ def read_memory(self, addr: int, size: int) -> Optional[bytes]:
"""
raise NotImplementedError

def get_comment(self, addr) -> Optional[Comment]:
"""Return the lifted Comment at ``addr`` (lifted), or None.

Reads the backend directly rather than through the light-artifact
cache, so a comment set moments earlier is immediately visible.
"""
lowered = self.art_lifter.lower_addr(addr)
cmt = self._get_comment(lowered)
return self.art_lifter.lift(cmt) if cmt is not None else None

def delete_comment(self, addr) -> bool:
"""Delete any comment at ``addr`` (lifted). True if one was removed."""
lowered = self.art_lifter.lower_addr(addr)
return self._del_comment(lowered)

# ------------------------------------------------------------------
# Persistence
# ------------------------------------------------------------------
Expand Down
135 changes: 135 additions & 0 deletions declib/cli/decompiler_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- disassemble disassemble a function by name or address
- xref_to data + code references to a target
- xref_from things a function calls (callees)
- comment get/set/append/delete/list comments (annotations)
- rename rename a function or local variable
- create-type define a new struct/enum/typedef from a C string
- retype change the type of a function's variable or argument
Expand Down Expand Up @@ -117,6 +118,28 @@ def _resolve_function_addr(client, target: str) -> Optional[int]:
return addr # let the caller raise if it's truly invalid


def _to_lifted_addr(client, addr: int) -> int:
"""Best-effort: normalize an absolute address into the server's lifted form.

The server keys artifacts (comments, patches, ...) by lifted address —
relative to the image base. Users routinely paste absolute addresses, so
when the value is clearly absolute (>= base and not already a known lifted
function start) we subtract the base to land it where they expect.
"""
try:
if addr in set(client.functions.keys()):
return addr
except Exception:
pass
try:
base = client.binary_base_addr
except Exception:
base = 0
if base and addr >= base:
return addr - base
return addr


def _select_server(
server_id: Optional[str],
binary_path: Optional[str],
Expand Down Expand Up @@ -766,6 +789,88 @@ def cmd_rename(args) -> int:
raise SystemExit(f"Unknown rename kind: {kind}")


# ---------------------------------------------------------------------------
# comment (get / set / append / delete / list)
# ---------------------------------------------------------------------------

def cmd_comment(args) -> int:
"""CRUD for comments/annotations, keyed by address.

``set`` replaces, ``append`` concatenates onto whatever is already there,
``get``/``delete`` operate on a single address, and ``list`` enumerates
every comment the backend knows about (optionally regex-filtered).
"""
from declib.artifacts import Comment

action = args.comment_action
with _with_client(args) as client:
if action == "list":
pattern = re.compile(args.filter) if args.filter else None
entries: List[Dict] = []
for addr, cmt in sorted(client.comments.items(), key=lambda kv: kv[0]):
text = getattr(cmt, "comment", None) or ""
if pattern and not pattern.search(text):
continue
entries.append({
"addr": addr,
"decompiled": bool(getattr(cmt, "decompiled", False)),
"comment": text,
})
if args.json:
_emit_list(args, entries)
else:
if not entries:
print("No comments.")
return EXIT_OK
for e in entries:
print(f"{_format_addr_hex(e['addr'])}\t{e['comment']}")
return EXIT_OK

addr_value, _ = _parse_target(args.addr)
if addr_value is None:
raise SystemExit(f"Invalid address {args.addr!r}; expected hex (0x..) or decimal.")
lifted = _to_lifted_addr(client, addr_value)

if action == "get":
cmt = client.get_comment(lifted)
if cmt is None:
raise SystemExit(f"No comment at {_format_addr_hex(lifted)}")
_emit(args, {
"addr": lifted,
"decompiled": bool(getattr(cmt, "decompiled", False)),
"comment": cmt.comment,
}, text_field="comment")
return EXIT_OK

if action == "delete":
ok = bool(client.delete_comment(lifted))
_emit(args, {"addr": lifted, "deleted": ok})
return EXIT_OK if ok else EXIT_USER_ERROR

# set / append
new_text = args.text
if action == "append":
existing = client.get_comment(lifted)
if existing is not None and existing.comment:
new_text = f"{existing.comment}\n{new_text}"

comment = Comment(addr=lifted, comment=new_text, decompiled=bool(args.decompiled))
ok = bool(client.set_artifact(comment))
if not ok:
raise SystemExit(
f"Backend rejected the comment at {_format_addr_hex(lifted)}. "
"Some backends (IDA) only accept comments at addresses inside a "
"function — check the address with `list_functions`/`disassemble`."
)
_emit(args, {
"addr": lifted,
"decompiled": bool(args.decompiled),
"comment": new_text,
"success": ok,
})
return EXIT_OK


# ---------------------------------------------------------------------------
# create-type / retype
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1400,6 +1505,36 @@ def build_parser() -> argparse.ArgumentParser:
_add_output_args(p_ren)
p_ren.set_defaults(func=cmd_rename)

# comment
p_cmt = sub.add_parser(
"comment",
help="Get/set/append/delete/list comments (annotations) by address.",
)
cmt_sub = p_cmt.add_subparsers(dest="comment_action", required=True)

def _add_comment_addr_action(name, help_text, with_text=False):
p = cmt_sub.add_parser(name, help=help_text)
p.add_argument("addr", help="Address (hex 0x.., decimal, lifted or absolute).")
if with_text:
p.add_argument("text", help="Comment text.")
p.add_argument("--decompiled", action="store_true",
help="Attach to the decompiler view (pseudocode) rather than disassembly.")
_add_server_filter_args(p)
_add_output_args(p)
p.set_defaults(func=cmd_comment)
return p

_add_comment_addr_action("set", "Set a comment at an address (replaces existing).", with_text=True)
_add_comment_addr_action("append", "Append text to the comment at an address.", with_text=True)
_add_comment_addr_action("get", "Print the comment at an address.")
_add_comment_addr_action("delete", "Delete any comment at an address.")

p_clist = cmt_sub.add_parser("list", help="List every comment in the binary.")
p_clist.add_argument("--filter", dest="filter", help="Regex to filter comment text.")
_add_server_filter_args(p_clist)
_add_output_args(p_clist)
p_clist.set_defaults(func=cmd_comment)

# create-type
p_ct = sub.add_parser(
"create-type",
Expand Down
21 changes: 20 additions & 1 deletion declib/decompilers/binja/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,19 @@ def _set_comment(self, comment: Comment, **kwargs) -> bool:
# func exists for commenting
bn_func = self.addr_to_bn_func(self.bv, comment.addr)
bn_func.set_comment_at(comment.addr, comment.comment)
return True

def _del_comment(self, addr) -> bool:
"""Clear any comment at ``addr`` (function-local and address-space)."""
removed = False
if self.bv.get_comment_at(addr):
self.bv.set_comment_at(addr, "")
removed = True
for bn_func in self.bv.get_functions_containing(addr) or []:
if bn_func.get_comment_at(addr):
bn_func.set_comment_at(addr, "")
removed = True
return removed

def _get_comment(self, addr) -> Optional[Comment]:
non_func_cmt = self.bv.get_comment_at(addr)
Expand Down Expand Up @@ -686,7 +699,13 @@ def _comments(self) -> Dict[int, Comment]:
if not bn_func.symbol.type in VALID_FUNC_SYM_TYPES:
continue

comments.update(bn_func.comments)
for cmt_addr, cmt_text in bn_func.comments.items():
if not cmt_text:
continue
comments[cmt_addr] = Comment(
addr=cmt_addr, comment=cmt_text,
func_addr=bn_func.start, decompiled=True,
)

# TODO: show non-function based comments
return comments
Expand Down
17 changes: 17 additions & 0 deletions declib/decompilers/ghidra/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,23 @@ def _get_comment(self, addr) -> Optional[Comment]:
comments = self._comments()
return comments.get(addr, None)

@ghidra_transaction
def _del_comment(self, addr) -> bool:
from .compat.imports import CodeUnit, SetCommentCmd

gaddr = self._to_gaddr(addr)
listing = self.currentProgram.getListing()
code_unit = listing.getCodeUnitAt(gaddr)
removed = False
for cmt_type in (
CodeUnit.PLATE_COMMENT, CodeUnit.PRE_COMMENT, CodeUnit.EOL_COMMENT,
CodeUnit.POST_COMMENT, CodeUnit.REPEATABLE_COMMENT,
):
if code_unit is not None and code_unit.getComment(cmt_type):
SetCommentCmd(gaddr, cmt_type, None).applyTo(self.currentProgram)
removed = True
return removed

def _comments(self) -> Dict[int, Comment]:
from .compat.imports import CodeUnit

Expand Down
Loading