diff options
| author | blasty <blasty@local> | 2026-07-23 02:12:24 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-23 02:12:24 +0200 |
| commit | 938235f7484b791a3828da3905c7c4949b8dba30 (patch) | |
| tree | c13920c817bc77a147e4272e49281cec16ab9ec3 | |
| parent | TODO: M4 progress — string auto-detect + streaming done; struct-expand/unif... (diff) | |
| download | ida-tui-938235f7484b791a3828da3905c7c4949b8dba30.tar.gz ida-tui-938235f7484b791a3828da3905c7c4949b8dba30.tar.xz ida-tui-938235f7484b791a3828da3905c7c4949b8dba30.zip | |
listing: expand struct-typed globals into member rows (M4 item 3)
A struct-typed data item rendered as one dense 'MyS <...>' line. Now the heads
walker emits indented member rows after the summary (from get_udt_details):
+off name type, each a head of kind member at ea+offset. ListingView renders
them indented/dimmed; _line_plain matches for cursor/search.
Verified: a struct global expands to +0 a int / +4 b char[4] / +8 c __int16,
member rows carry field addresses. New listing_struct_expand scenario; listing
suite 22/0 (in-process).
| -rw-r--r-- | idatui/app.py | 6 | ||||
| -rw-r--r-- | server/patch_server.py | 45 | ||||
| -rw-r--r-- | tests/test_scenarios.py | 48 |
3 files changed, 94 insertions, 5 deletions
diff --git a/idatui/app.py b/idatui/app.py index 27dc33e..234b720 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -53,6 +53,7 @@ _S_MNEM = Style(color="cyan") _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_CURSOR = Style(bgcolor="grey30") _S_DIM = Style(color="grey42", italic=True) _S_MATCH = Style(bgcolor="#7a5c00") # all search matches @@ -941,7 +942,8 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru h = self._head(idx) if h is None: return None - return f"{h.ea:08X} " + self._name_prefix(h) + h.text + indent = " " if h.kind == "member" else "" + return f"{h.ea:08X} " + indent + self._name_prefix(h) + h.text # -- public API -------------------------------------------------------- # def load(self, model: ListingModel, name: str, cursor: int = 0, @@ -1057,6 +1059,8 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru segs.append(Segment(" " + rest, _S_INSN)) elif h.kind == "data": segs.append(Segment(h.text, _S_DATA)) + elif h.kind == "member": + segs.append(Segment(" " + h.text, _S_MEMBER)) else: segs.append(Segment(h.text, _S_UNK)) strip = Strip(segs) diff --git a/server/patch_server.py b/server/patch_server.py index e283cab..ff5c493 100644 --- a/server/patch_server.py +++ b/server/patch_server.py @@ -305,6 +305,39 @@ def _idatui_unknown_row(ea, size): return row +def _idatui_struct_member_rows(ea): + """Indented member rows for a struct-typed data item at ``ea`` (expansion), + or [] if it isn't a struct. Top-level fields only.""" + import ida_nalt + import ida_typeinf + import idaapi + + tif = ida_typeinf.tinfo_t() + if not (ida_nalt.get_tinfo(tif, ea) and tif.is_udt()): + return [] + udt = ida_typeinf.udt_type_data_t() + if not tif.get_udt_details(udt): + return [] + rows = [] + for m in udt: + off = m.begin() // 8 + try: + mtype = m.type._print() or "" + except Exception: + mtype = "" + try: + sz = int(m.type.get_size()) + if sz == idaapi.BADSIZE: + sz = 0 + except Exception: + sz = 0 + name = m.name or "" + text = f"+{off:X} {name}" + (f" {mtype}" if mtype else "") + rows.append({"ea": hex(ea + off), "kind": "member", "size": sz, + "text": text}) + return rows + + @tool @idasync def heads( @@ -379,10 +412,14 @@ def heads( nxt = ida_bytes.get_item_end(e) return nxt if nxt > e else e + 1 - def _row(e): + def _rows_for(e): if _is_unknown(e): - return _idatui_unknown_row(e, _run_end(e) - e) - return _idatui_head_row(e) + return [_idatui_unknown_row(e, _run_end(e) - e)] + row = _idatui_head_row(e) + out = [row] + if row.get("kind") == "data": + out.extend(_idatui_struct_member_rows(e)) # expand struct fields + return out ea = ida_bytes.get_item_head(start) for _ in range(offset): @@ -394,7 +431,7 @@ def heads( if len(rows) >= count: more = True break - rows.append(_row(ea)) + rows.extend(_rows_for(ea)) # a struct head expands into member rows ea = _advance(ea) cursor = {"next": hex(ea)} if more else {"done": True} return {"addr": str(addr), "heads": rows, "cursor": cursor} diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 62d86dd..d3576c0 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -1556,6 +1556,54 @@ async def s_listing_make_string(c: Ctx): c.prog.bump_items() +@scenario("listing_struct_expand") +async def s_listing_struct_expand(c: Ctx): + """A struct-typed global expands into indented member rows in the listing.""" + app = c.app + # a writable data address: reuse a data segment's first data head + A = None + for start, end, name in c.prog.sections(): + if start >= end: + continue + lm = c.prog.listing(start) + if lm is None: + continue + lm.ensure(60) + h = next((h for h in lm.window(0, 60) if h.kind == "data"), None) + if h is not None: + A = h.ea + break + if A is None: + c.check("found a data address for the struct test", False) + return + try: + c.prog.client.call( + "declare_type", + decls=["struct TuiExpandS { int a; char b[4]; short c; };"]) + c.prog.make_data(A, "TuiExpandS") + c.prog.bump_items() + await c.goto_ui(hex(A)) + await c.wait(lambda: app._active == "listing" and app._cur is not None + and app._cur.ea == A and c.lst.total > 0, 25) + # the summary head, then member rows for a/b/c + si = c.lst.model.index_of_ea(A) + members = [c.lst.model.get(si + 1 + k) for k in range(3)] + names = [m.text for m in members if m is not None] + c.check("struct global expands into member rows", + all(m is not None and m.kind == "member" for m in members) + and any("a" in t for t in names) and any("b" in t for t in names), + f"members={names}") + c.check("member rows carry field addresses", + members[1] is not None and members[1].ea == A + 4, + f"ea={members[1].ea if members[1] else None:#x} want={A+4:#x}") + finally: + try: + c.prog.undefine(A, size=16) + except Exception: # noqa: BLE001 + pass + c.prog.bump_items() + + # --------------------------------------------------------------------------- # # Runner # --------------------------------------------------------------------------- # |
