diff options
| -rw-r--r-- | idatui/app.py | 8 | ||||
| -rw-r--r-- | idatui/domain.py | 8 | ||||
| -rw-r--r-- | server/patch_server.py | 39 | ||||
| -rw-r--r-- | tests/test_scenarios.py | 25 |
4 files changed, 77 insertions, 3 deletions
diff --git a/idatui/app.py b/idatui/app.py index c924fa5..fd8c8c0 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -54,6 +54,8 @@ _S_OPBYTES = Style(color="grey50") # raw opcode bytes column _S_DATA = Style(color="#c8a15a") # data items in the flat listing (db/dw/strings) _S_UNK = Style(color="grey54", italic=True) # undefined bytes in the flat listing _S_MEMBER = Style(color="#9a8a6a") # struct field rows (expanded, indented) +_S_SEP = Style(color="grey42") # function boundary separators / banners +_S_FUNCHDR = Style(color="#d7a021", bold=True) # 'name proc near'/'endp' headers # Tokens that look like identifiers but aren't renamable symbols (so 'n' on them # in the listing names the address instead of trying to rename the token). @@ -963,6 +965,8 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru h = self._head(idx) if h is None: return None + if h.kind in ("sep", "funchdr"): + return h.text indent = " " if h.kind == "member" else "" return (f"{h.ea:08X} " + self._op_field(h) + indent + self._name_prefix(h) + h.text) @@ -1092,6 +1096,10 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru h = model.get(idx) if h is None: strip = Strip([Segment(f" {idx:>8} …", _S_DIM)]) + elif h.kind == "sep": + strip = Strip([Segment(h.text, _S_SEP)]) + elif h.kind == "funchdr": + strip = Strip([Segment(h.text, _S_FUNCHDR)]) else: segs: list[Segment] = [Segment(f"{h.ea:08X} ", _S_ADDR)] op = self._op_field(h) diff --git a/idatui/domain.py b/idatui/domain.py index e392936..7452ffd 100644 --- a/idatui/domain.py +++ b/idatui/domain.py @@ -595,7 +595,8 @@ class ListingModel: if self._done or self._next is None: return 0 frm = self._next - payload = self._prog.client.call("heads", addr=hex(frm), count=self.PAGE) + payload = self._prog.client.call( + "heads", addr=hex(frm), count=self.PAGE, annotate=True) rows = payload.get("heads", []) if isinstance(payload, dict) else [] cur = payload.get("cursor", {}) if isinstance(payload, dict) else {} page = [] @@ -608,7 +609,10 @@ class ListingModel: with self._lock: base = len(self._heads) for i, h in enumerate(page): - self._by_ea.setdefault(h.ea, base + i) + # Banner rows (function headers/separators) are display-only; + # don't index them so navigation lands on real code/data. + if h.kind not in ("sep", "funchdr"): + self._by_ea.setdefault(h.ea, base + i) self._heads.append(h) nxt = cur.get("next") if nxt is None: diff --git a/server/patch_server.py b/server/patch_server.py index ff5c493..99cd584 100644 --- a/server/patch_server.py +++ b/server/patch_server.py @@ -338,6 +338,32 @@ def _idatui_struct_member_rows(ea): return rows +def _idatui_func_header_rows(ea): + """IDA-style subroutine banner rows shown just before a function's entry.""" + import ida_funcs + + name = ida_funcs.get_func_name(ea) or "sub_%X" % ea + bar = "=" * 15 + " S U B R O U T I N E " + "=" * 15 + return [ + {"ea": hex(ea), "kind": "sep", "size": 0, "text": ""}, + {"ea": hex(ea), "kind": "sep", "size": 0, "text": "; " + bar}, + {"ea": hex(ea), "kind": "funchdr", "size": 0, + "text": name + " proc near", "name": name}, + ] + + +def _idatui_func_footer_rows(ea, func): + """End-of-function marker shown just after a function's last item.""" + import ida_funcs + + name = ida_funcs.get_func_name(func.start_ea) or "sub_%X" % func.start_ea + return [ + {"ea": hex(ea), "kind": "funchdr", "size": 0, + "text": name + " endp", "name": name}, + {"ea": hex(ea), "kind": "sep", "size": 0, "text": "; " + "-" * 60}, + ] + + @tool @idasync def heads( @@ -346,6 +372,7 @@ def heads( offset: Annotated[int, "Skip first N heads from addr (default 0)"] = 0, end: Annotated[str, "Optional exclusive end address; default = segment end"] = "", back: Annotated[bool, "Walk backwards: return the count heads ENDING just before addr, in forward order"] = False, + annotate: Annotated[bool, "Emit IDA-style function boundary banner rows (kind sep/funchdr)"] = False, ) -> dict: """Walk item heads from ``addr`` as a flat listing: every head is rendered (code OR data OR undefined) via generate_disasm_line and stepped with @@ -415,10 +442,20 @@ def heads( def _rows_for(e): if _is_unknown(e): return [_idatui_unknown_row(e, _run_end(e) - e)] + func = idaapi.get_func(e) if annotate else None + at_start = func is not None and func.start_ea == e + out = [] + if at_start: + out.extend(_idatui_func_header_rows(e)) row = _idatui_head_row(e) - out = [row] + if at_start: + row = dict(row) + row["name"] = None # the name is shown on the proc header line + out.append(row) if row.get("kind") == "data": out.extend(_idatui_struct_member_rows(e)) # expand struct fields + if func is not None and ida_bytes.get_item_end(e) >= func.end_ea: + out.extend(_idatui_func_footer_rows(e, func)) return out ea = ida_bytes.get_item_head(start) diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 8a5df2e..73fc50b 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -1619,6 +1619,31 @@ async def s_continuous_view(c: Ctx): app._active == "listing", f"active={app._active}") +@scenario("func_banners") +async def s_func_banners(c: Ctx): + """The unified listing shows IDA-style function boundary banners: a + SUBROUTINE separator + 'name proc near' header and a 'name endp' footer, + and those banner rows are display-only (navigation lands on real code).""" + app = c.app + fn = await c.open_biggest("listing") + c.lst.model.load_all() + heads = [c.lst.model.get(i) for i in range(len(c.lst.model))] + ci = c.lst.model.index_of_ea(fn.addr) + c.check("navigation to a function lands on its code head, not a banner", + ci >= 0 and c.lst.model.get(ci).kind == "code", + f"kind={c.lst.model.get(ci).kind if ci >= 0 else None}") + c.check("a SUBROUTINE separator banner is present", + any(h.kind == "sep" and "S U B R O U T I N E" in h.text for h in heads)) + c.check("a 'name proc near' header is present", + any(h.kind == "funchdr" and h.text.endswith("proc near") for h in heads)) + c.check("a 'name endp' footer is present", + any(h.kind == "funchdr" and h.text.endswith("endp") for h in heads)) + # the proc header for the origin function carries its name + hdr = next((h for h in heads if h.kind == "funchdr" + and h.text == f"{fn.name} proc near"), None) + c.check("the proc header names the function", hdr is not None, f"fn={fn.name}") + + # --------------------------------------------------------------------------- # # Runner # --------------------------------------------------------------------------- # |
