aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-23 02:12:24 +0200
committerblasty <blasty@local>2026-07-23 02:12:24 +0200
commit938235f7484b791a3828da3905c7c4949b8dba30 (patch)
treec13920c817bc77a147e4272e49281cec16ab9ec3
parentTODO: M4 progress — string auto-detect + streaming done; struct-expand/unif... (diff)
downloadida-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.py6
-rw-r--r--server/patch_server.py45
-rw-r--r--tests/test_scenarios.py48
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
# --------------------------------------------------------------------------- #