diff options
| author | blasty <blasty@local> | 2026-07-09 13:54:40 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-09 13:54:40 +0200 |
| commit | 8d9644e8c6158af45ce95467c4a0611e8e10fa22 (patch) | |
| tree | c22c94a1f130308fcc2514a83b01c6f93f6e8f73 /idatui/domain.py | |
| parent | function-name filter: incremental + highlight + clear (diff) | |
| download | ida-tui-8d9644e8c6158af45ce95467c4a0611e8e10fa22.tar.gz ida-tui-8d9644e8c6158af45ce95467c4a0611e8e10fa22.tar.xz ida-tui-8d9644e8c6158af45ce95467c4a0611e8e10fa22.zip | |
xrefs + follow-under-cursor (disasm & pseudocode)
- Enter follows the reference under the cursor: disasm uses xref_query(from) to
find the code target; pseudocode matches a decompiler ref name on the line.
Navigates to the containing function at the exact line (address-history push).
- x opens an xrefs popup (XrefsScreen modal): xrefs_to the subject under cursor
(call target if any, else the current item); Enter jumps to a referencing
site, Esc closes.
- domain: Program.function_of (mid-addr -> containing func via lookup_funcs),
xrefs_from/xrefs_to (xref_query), Xref model, DisasmModel ea->index map for
landing on an exact address.
- fix: goto to a mid-function address now lands on the right line (was opening
it as a bogus function start).
- pilot suite 32/32; domain 17/17.
Diffstat (limited to 'idatui/domain.py')
| -rw-r--r-- | idatui/domain.py | 81 |
1 files changed, 81 insertions, 0 deletions
diff --git a/idatui/domain.py b/idatui/domain.py index 9ed2432..6578932 100644 --- a/idatui/domain.py +++ b/idatui/domain.py @@ -21,6 +21,7 @@ Textual worker threads; the internal prefetch pool is separate and small. from __future__ import annotations +import bisect import json import re import threading @@ -83,6 +84,15 @@ class Ref: @dataclass +class Xref: + frm: int # the referencing address + to: int | None # the referenced address + type: str # "code" | "data" | ... + fn_name: str | None # function containing `frm` + fn_addr: int | None + + +@dataclass class Decompilation: ea: int code: str | None @@ -210,6 +220,7 @@ class DisasmModel: self.name = name self._blocks: dict[int, list[Line]] = {} self._total: int | None = None + self._ea_list: list[int] | None = None self._lock = threading.Lock() self._inflight: set[int] = set() @@ -306,10 +317,35 @@ class DisasmModel: with self._lock: return len(self._blocks) + def ensure_ea_index(self) -> list[int]: + """Build (once) a sorted list of every line's ea, for ea->index lookup. + Fetches the whole function; cached. Only needed for mid-function jumps.""" + if self._ea_list is not None: + return self._ea_list + total = self.total() + eas: list[int] = [] + off = 0 + while off < total: + lines = self.lines(off, self.BLOCK, prefetch=False) + if not lines: + break + eas.extend(ln.ea for ln in lines) + off += len(lines) + with self._lock: + self._ea_list = eas + return eas + + def index_of_ea(self, ea: int) -> int: + """Instruction index of the line at/containing ``ea`` (0 if before start).""" + eas = self.ensure_ea_index() + i = bisect.bisect_right(eas, ea) - 1 + return i if 0 <= i < len(eas) else 0 + def invalidate(self) -> None: with self._lock: self._blocks.clear() self._total = None + self._ea_list = None # --------------------------------------------------------------------------- # @@ -394,6 +430,29 @@ class Program: except Exception: # noqa: BLE001 -- fall back to the truncated preview return None + # -- cross-references & containing function --------------------------- # + def function_of(self, ea: int) -> Func | None: + """Return the function containing ``ea`` (resolves mid-function addrs).""" + payload = self.client.call("lookup_funcs", queries=[hex(ea)]) + res = payload.get("result", []) if isinstance(payload, dict) else [] + fn = res[0].get("fn") if res and isinstance(res[0], dict) else None + return Func.from_raw(fn) if fn else None + + def xrefs_from(self, ea: int) -> list[Xref]: + payload = self.client.call( + "xref_query", + queries=[{"addr": hex(ea), "direction": "from", "include_fn": True}], + ) + return _parse_xrefs(payload) + + def xrefs_to(self, ea: int, limit: int = 2000) -> list[Xref]: + payload = self.client.call( + "xref_query", + queries=[{"addr": hex(ea), "direction": "to", "include_fn": True, + "dedup": True, "count": limit}], + ) + return _parse_xrefs(payload) + # -- address resolution ------------------------------------------------ # def resolve(self, target: int | str) -> int: """Resolve an int/hex-string/symbol name to an address (ea).""" @@ -431,6 +490,28 @@ class Program: self._indices.clear() +def _parse_xrefs(payload) -> list[Xref]: + res = payload.get("result", []) if isinstance(payload, dict) else [] + if not res: + return [] + data = res[0].get("data", []) or [] + out: list[Xref] = [] + for d in data: + if not isinstance(d, dict): + continue + fn = d.get("fn") or {} + frm = d.get("from", d.get("addr")) + to = d.get("to") + out.append(Xref( + frm=_as_int(frm) if frm is not None else 0, + to=_as_int(to) if to is not None else None, + type=d.get("type", "?"), + fn_name=fn.get("name"), + fn_addr=_as_int(fn["addr"]) if fn.get("addr") else None, + )) + return out + + def _parse_decompilation(ea: int, payload) -> Decompilation: if not isinstance(payload, dict): return Decompilation(ea, None, True, "unexpected payload", False, None) |
