aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--TODO9
-rw-r--r--idatui/app.py86
-rw-r--r--idatui/domain.py11
-rw-r--r--tests/test_scenarios.py41
4 files changed, 144 insertions, 3 deletions
diff --git a/TODO b/TODO
index 92ecfa1..935fc24 100644
--- a/TODO
+++ b/TODO
@@ -25,9 +25,12 @@ hard:
-> M2 done: ListingView — virtualized flat listing (code+data+undefined,
kind-styled), wired into nav/follow/xrefs/hex/edit-verbs. region views
now use it (not the DisasmModel stopgap).
- -> next: M3 data typing in .data (`d`/make_data with a type prompt, struct-
- typed data, string/dup coalescing for big undefined runs); M4 optionally
- unify the function disasm view as a filtered listing.
+ -> M3 done: `d` = typed make_data prompt in the listing (define data in
+ .data with any C type: int, char[16], my_struct, ...); undefined byte
+ runs coalesce into `db N dup(?)` rows so big .bss doesn't explode.
+ -> next (optional): M4 unify the function disasm view as a filtered listing;
+ struct-typed data expansion / string auto-detection; back-paging in the
+ ListingModel (currently loads a segment forward via load_all).
crazy:
[ ] multi/split view ala ghidra?
diff --git a/idatui/app.py b/idatui/app.py
index ffef9b4..7bca4e0 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -145,6 +145,14 @@ class EditItemRequested(Message):
self.kind = kind
+class MakeDataRequested(Message):
+ """The listing view asks to define typed data (IDA 'd') at the current head."""
+
+ def __init__(self, view) -> None: # type: ignore[no-untyped-def]
+ super().__init__()
+ self.view = view
+
+
class NavMixin:
"""Follow / xrefs actions shared by the code views (bindings live on each
view since Textual only merges BINDINGS from DOMNode subclasses)."""
@@ -891,6 +899,7 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru
Binding("home", "goto_top", "Top", show=False),
Binding("G,end", "goto_bottom", "Bottom", show=False),
Binding("c", "define_code", "Code", show=False),
+ Binding("d", "make_data", "Data", show=False),
Binding("p", "define_func", "Func", show=False),
Binding("u", "undefine", "Undef", show=False),
*SearchMixin.SEARCH_BINDINGS,
@@ -1059,6 +1068,12 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru
def action_undefine(self) -> None:
self.post_message(EditItemRequested(self, "undef"))
+ def action_make_data(self) -> None:
+ self.post_message(MakeDataRequested(self))
+
+ def cur_head(self) -> Head | None:
+ return self._head(self.cursor)
+
def action_cursor_down(self) -> None:
self._move(1)
@@ -1962,6 +1977,10 @@ class IdaTui(App):
height: 1; border: none; padding: 0 1;
background: $accent-darken-2; color: $text;
}
+ #makedata {
+ height: 1; border: none; padding: 0 1;
+ background: $secondary-darken-2; color: $text;
+ }
#goto {
height: 1; border: none; padding: 0 1;
background: $primary-darken-3; color: $text;
@@ -2037,6 +2056,7 @@ class IdaTui(App):
self._rename_ctx: tuple[object | None, str] = (None, "")
self._comment_ctx: tuple[object | None, int, str] = (None, 0, "")
self._retype_ctx: tuple[object | None, str, int, str] = (None, "", 0, "")
+ self._makedata_ctx: tuple[object | None, int] = (None, 0)
self._xref_focus_name: str | None = None
self._dirty = False
@@ -2072,6 +2092,10 @@ class IdaTui(App):
ti.display = False
ti.can_focus = False
yield ti
+ mdi = Input(id="makedata")
+ mdi.display = False
+ mdi.can_focus = False
+ yield mdi
gi = Input(id="goto")
gi.display = False
gi.can_focus = False
@@ -2823,6 +2847,57 @@ class IdaTui(App):
what = "prototype" if kind == "func" else f"'{word}'"
self._status(f"retyped {what} (Ctrl+S to save)")
+ # -- typed data definition (make_data, IDA 'd') ----------------------- #
+ @staticmethod
+ def _default_data_type(head) -> str: # type: ignore[no-untyped-def]
+ """A sensible prefill C type for defining data over ``head``."""
+ sz = getattr(head, "size", 0) or 0
+ return {1: "unsigned __int8", 2: "unsigned __int16",
+ 4: "unsigned __int32", 8: "unsigned __int64"}.get(
+ sz, f"char[{sz}]" if sz > 0 else "unsigned __int8")
+
+ def on_make_data_requested(self, msg: MakeDataRequested) -> None:
+ view = msg.view
+ ea = view._cursor_ea() if isinstance(view, ListingView) else None
+ if ea is None:
+ self._status("no address on this line to define data")
+ return
+ self._makedata_ctx = (view, ea)
+ self.query_one("#status", Static).display = False
+ inp = self.query_one("#makedata", Input)
+ inp.placeholder = (f"data type @ {ea:#x} (e.g. int, char[16], my_struct)"
+ " — Enter=apply Esc=cancel")
+ inp.can_focus = True
+ inp.display = True
+ head = view.cur_head() if isinstance(view, ListingView) else None
+ inp.value = self._default_data_type(head) if head is not None else "int"
+ inp.focus()
+
+ def _end_makedata(self) -> None:
+ inp = self.query_one("#makedata", Input)
+ inp.display = False
+ inp.can_focus = False
+ self.query_one("#status", Static).display = True
+ view = self._makedata_ctx[0]
+ if view is not None:
+ view.focus()
+
+ @work(thread=True, exclusive=True, group="makedata")
+ def _do_make_data(self, ea: int, type_decl: str) -> None: # worker context
+ assert self.program is not None
+ try:
+ self.program.make_data(ea, type_decl)
+ except Exception as e: # noqa: BLE001
+ self.app.call_from_thread(self._status, f"make data: {e}")
+ return
+ self.program.bump_items()
+ name = self.program.region_label(ea)
+ lm = self.program.listing(ea)
+ idx = max(lm.ensure_ea(ea), 0) if lm is not None else 0
+ self.app.call_from_thread(self._open_at, ea, name, idx, False, -1, 0, True)
+ self.app.call_from_thread(
+ self._edit_item_done, f"data ({type_decl})", ea)
+
@work(thread=True, exclusive=True, group="rename")
def _do_rename(self, view, old: str, new: str) -> None: # type: ignore[no-untyped-def]
assert self.program is not None
@@ -3094,6 +3169,11 @@ class IdaTui(App):
event.prevent_default()
self._end_retype()
return
+ if self.query_one("#makedata", Input).display:
+ event.stop()
+ event.prevent_default()
+ self._end_makedata()
+ return
if self.query_one("#goto", Input).display:
event.stop()
event.prevent_default()
@@ -3142,6 +3222,12 @@ class IdaTui(App):
if view is not None and value:
self._do_retype(kind, subject, word, value)
return
+ if inp.id == "makedata":
+ view, ea = self._makedata_ctx
+ self._end_makedata()
+ if view is not None and value:
+ self._do_make_data(ea, value)
+ return
if inp.id == "goto":
self._end_goto()
(self.query_one(HexView) if self._active == "hex"
diff --git a/idatui/domain.py b/idatui/domain.py
index fd673ca..4f33211 100644
--- a/idatui/domain.py
+++ b/idatui/domain.py
@@ -1071,6 +1071,17 @@ class Program:
if res.get("error"):
raise IDAToolError(f"undefine @ {ea:#x}: {res['error']}")
+ def make_data(self, ea: int, type_decl: str, name: str | None = None) -> None:
+ """Create a typed data item at ``ea`` (IDA's 'd', but typed). ``type_decl``
+ is a C type, e.g. 'int', 'unsigned __int32', 'char[5]', 'my_struct'."""
+ item: dict = {"addr": hex(ea), "type": type_decl}
+ if name:
+ item["name"] = name
+ res = self._first_result(self.client.call("make_data", items=[item]))
+ if res.get("ok") is False or res.get("error"):
+ raise IDAToolError(
+ f"make data @ {ea:#x}: {res.get('error') or 'rejected'}")
+
def region_label(self, ea: int) -> str:
"""Display name for a non-function address (segment-qualified)."""
try:
diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py
index 2b9a9b8..f947888 100644
--- a/tests/test_scenarios.py
+++ b/tests/test_scenarios.py
@@ -1369,6 +1369,47 @@ async def s_listing_view(c: Ctx):
c.check("returning from hex lands back on the listing (not a func view)",
app._active == "listing" and c.lst.display, f"active={app._active}")
+ # 'd' defines typed data over an undefined run. Synthesize the run
+ # deterministically: undefine a data head, then re-type it via the prompt.
+ dhead = next((c.lst.model.get(i) for i in range(min(c.lst.total, 200))
+ if c.lst.model.get(i) and c.lst.model.get(i).kind == "data"
+ and (c.lst.model.get(i).size or 0) >= 4), None)
+ if dhead is None:
+ c.check("found a data head to re-type", False)
+ return
+ dea = dhead.ea
+ try:
+ c.prog.undefine(dea, size=4)
+ c.prog.bump_items()
+ # reopen the listing so the model reflects the new undefined run
+ await c.goto_ui(hex(dea))
+ await c.wait(lambda: app._active == "listing" and c.lst.total > 0
+ and c.lst.model.index_of_ea(dea) >= 0, 25)
+ ui = c.lst.model.index_of_ea(dea)
+ c.check("undefining a data head yields an unknown run in the listing",
+ ui >= 0 and c.lst.model.get(ui).kind == "unknown",
+ f"kind={c.lst.model.get(ui).kind if ui>=0 else None}")
+ c.lst.focus()
+ c.lst.cursor = ui
+ await c.pause(0.05)
+ await c.press("d")
+ await c.pause(0.1)
+ mdi = app.query_one("#makedata", Input)
+ c.check("'d' opens the make-data prompt prefilled with a type",
+ mdi.display and bool(mdi.value), f"display={mdi.display} val={mdi.value!r}")
+ mdi.value = "char[4]"
+ await c.press("enter")
+ await c.wait(lambda: app._active == "listing"
+ and c.lst.model.index_of_ea(dea) >= 0
+ and c.lst.model.get(c.lst.model.index_of_ea(dea)) is not None
+ and c.lst.model.get(c.lst.model.index_of_ea(dea)).kind == "data", 25)
+ di = c.lst.model.index_of_ea(dea)
+ c.check("'d' turns the undefined run into a typed data item",
+ di >= 0 and c.lst.model.get(di).kind == "data",
+ f"kind={c.lst.model.get(di).kind if di>=0 else None}")
+ finally:
+ c.prog.bump_items()
+
# --------------------------------------------------------------------------- #
# Runner