diff options
| -rw-r--r-- | docs/RPC.md | 2 | ||||
| -rw-r--r-- | idatui/app.py | 3 | ||||
| -rw-r--r-- | idatui/rpc.py | 21 | ||||
| -rw-r--r-- | tests/rpc_smoke.py | 14 |
4 files changed, 35 insertions, 5 deletions
diff --git a/docs/RPC.md b/docs/RPC.md index b8ee87f..a4d077f 100644 --- a/docs/RPC.md +++ b/docs/RPC.md @@ -62,7 +62,7 @@ stalls. `target` is a name, `0xADDR`, or omitted (= current function). | `pseudocode` | `target?` | `{ea,name,code,failed,error,truncated}` — the **whole** decompiled body. | | `disassembly` | `target?`, `max?=2000` | `{ea,name,total,lines:[{ea,text}]}`. | | `xrefs_to` | `target`, `limit?=200` | `[{frm,to,type,fn_addr,fn_name}]` — who references it. | -| `xrefs_from` | `target`, `limit?=200` | `[{frm,to,type,fn_addr,fn_name}]` — what it references. | +| `xrefs_from` | `target`, `limit?=200` | what it references. For a **function** (name/entry ea): whole-body callees + string/data refs from the decompiler, `[{to,name,string,is_func,type}]`. For an explicit **0xADDR**: address-scoped `[{frm,to,type,fn_addr,fn_name}]`. | | `resolve` | `name` | `{ea}` (or `{ea:null}`). | ### Semantic verbs diff --git a/idatui/app.py b/idatui/app.py index ebf928c..086785c 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -36,7 +36,7 @@ from textual.screen import ModalScreen from textual.scroll_view import ScrollView from textual.strip import Strip from textual.widgets import ( - DataTable, Footer, Header, Input, OptionList, Static, TextArea, + DataTable, Footer, Input, OptionList, Static, TextArea, ) from textual.widgets.option_list import Option @@ -1763,7 +1763,6 @@ class IdaTui(App): # -- layout ------------------------------------------------------------ # def compose(self) -> ComposeResult: - yield Header(show_clock=True) with Horizontal(id="panes"): fp = FunctionsPanel(id="left") fp.display = False # overlay-first: reveal the docked pane with Ctrl+B diff --git a/idatui/rpc.py b/idatui/rpc.py index 73e10d2..426b337 100644 --- a/idatui/rpc.py +++ b/idatui/rpc.py @@ -52,7 +52,8 @@ METHODS = { "pseudocode": "{target?} -> full decompiled body", "disassembly": "{target?,max?=2000} -> {total,lines:[{ea,text}]}", "xrefs_to": "{target,limit?=200} -> [{frm,to,type,fn_addr,fn_name}]", - "xrefs_from": "{target,limit?=200} -> same shape", + "xrefs_from": "{target,limit?=200} -> callees/refs; function-scoped for a " + "function (decomp refs), address-scoped for a 0xADDR", "resolve": "{name} -> {ea}", "keys": "{keys:[str],settle?,timeout?} raw key injection (supports 'wait:<ms>')", "text": "{text,delay_ms?,settle?} type a literal string into the focused input", @@ -319,8 +320,24 @@ def xrefs_to(app, target, limit: int = 200) -> list[dict[str, Any]]: def xrefs_from(app, target, limit: int = 200) -> list[dict[str, Any]]: + """References *out of* the target. For a function (a bare name / its entry ea) + this is whole-body — the callees, string and data refs from the decompiler + (the server's xrefs_from is address-scoped, so it only sees the entry + instruction). For an explicit mid-function 0xADDR it stays address-scoped.""" ea = _target_ea(app, target) - return _xref_dicts(app.program.xrefs_from(ea), limit) if ea is not None else [] + if ea is None: + return [] + fn = app.program.function_of(ea) + if fn is not None and fn.addr == ea: # function target -> decomp outgoing refs + out = [] + for r in app.program.decompile(ea).refs[:limit]: + tf = app.program.function_of(r.addr) + is_func = bool(tf and tf.addr == r.addr) + out.append({"to": r.addr, "name": r.name or (tf.name if tf else None), + "string": r.string, "is_func": is_func, + "type": "code" if is_func else "data"}) + return out + return _xref_dicts(app.program.xrefs_from(ea), limit) def resolve(app, name) -> dict[str, Any]: diff --git a/tests/rpc_smoke.py b/tests/rpc_smoke.py index 5d24978..fe2b79b 100644 --- a/tests/rpc_smoke.py +++ b/tests/rpc_smoke.py @@ -186,6 +186,20 @@ async def run(db): callee is not None and all("frm" in x for x in callee[1]), "no referenced function found" if callee is None else "") + # xrefs_from a function is whole-body (decomp refs), not just the entry: + # find a function that actually calls something. + caller = next((f["name"] for f in funcs if f["name"] == "main"), None) + xf = [] + for name in ([caller] if caller else []) + [f["name"] for f in funcs]: + xf = (await c.call("xrefs_from", target=name)).get("result", []) + if any(x.get("is_func") for x in xf): + caller = name + break + check("xrefs_from a function lists whole-body callees", + isinstance(xf, list) and len(xf) > 1 + and any(x.get("is_func") for x in xf) and all("to" in x for x in xf), + f"caller={caller} n={len(xf)}") + # --- modal select: open xrefs on the callee, pick the first site --- # if callee is not None: f, xr = callee |
