summaryrefslogtreecommitdiffstats
path: root/idatui/app.py
diff options
context:
space:
mode:
authorblasty <peter@haxx.in>2026-07-25 12:15:45 +0200
committerblasty <peter@haxx.in>2026-07-25 12:15:45 +0200
commitf79fbbdd6c089b58f29b6b5dcfd58ebc71e3bf78 (patch)
tree0f35f2206684e209475378ed62664f5ab11ef91a /idatui/app.py
parentui: horizontally centre the splash logo in the loading dialog (diff)
downloadida-tui-f79fbbdd6c089b58f29b6b5dcfd58ebc71e3bf78.tar.gz
ida-tui-f79fbbdd6c089b58f29b6b5dcfd58ebc71e3bf78.tar.xz
ida-tui-f79fbbdd6c089b58f29b6b5dcfd58ebc71e3bf78.zip
retype: 'y' now retypes globals too, not just prototypes and locals
In the decompiler, 'y' on a local variable already worked (func_types -> lvars -> set_lvar_type), but a GLOBAL fell through every case and silently retyped the ENCLOSING FUNCTION'S PROTOTYPE — worse than not working, since the prompt said "prototype" while you thought you were typing a variable. * server/patch_server.py: new data_type tool — {addr,name,type,size,is_func} for a data item, so the prompt can prefill the current type and the caller can tell a global from a function. * domain: Program.data_type() + set_data_type() (set_type with kind="global"). * app: _prepare_retype gains the data case between "function" and the current-function fallback, with a size-based prefill when the global is still untyped; _do_retype routes kind="data" to set_data_type. Classification verified on echo/main: 'v3' -> lvar (prefill 'char *'), 'stdout' -> data (prefill 'FILE *'), 'main' -> func prototype, an unresolvable token -> the enclosing prototype (unchanged fallback). Also fixes a latent crash found while probing this: on_listing/decomp_view_ cursor_moved called self.query_one(ListingView), but App.query_one searches the TOP screen — a cursor-moved message landing while any modal is up (loading overlay, project switch) raised NoMatches out of a message handler and killed the app. Both handlers now go through _try_view(). Pilot `retype` extended to 9 checks covering all three flavours, each asserting the other targets are left alone. Two of the new checks needed settles: applying a retype recompiles asynchronously, so scanning/indexing the pseudocode without waiting reads text that's about to be replaced (this also cut the scenario from 30s to 2.6s of previously-wasted timeout). Full suite 174/2-flake.
Diffstat (limited to 'idatui/app.py')
-rw-r--r--idatui/app.py42
1 files changed, 36 insertions, 6 deletions
diff --git a/idatui/app.py b/idatui/app.py
index 813a774..0012e32 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -4122,7 +4122,10 @@ class IdaTui(App):
lv = next((v for v in ft.lvars if v.name == word), None)
if lv is not None:
kind, prefill = "lvar", lv.type
- # 2) a function under the cursor (self or a referenced one)
+ # 2) a symbol under the cursor: a function (retype its prototype) or a
+ # global/data item (retype the variable). Without the data case a
+ # global fell through to (3) and silently retyped the ENCLOSING
+ # function's prototype instead.
if kind is None and self._looks_like_symbol(word):
try:
tgt = self.program.resolve(word)
@@ -4132,6 +4135,12 @@ class IdaTui(App):
tft = self.program.func_types(tgt)
if tft is not None:
kind, subject, prefill = "func", tgt, tft.prototype
+ else:
+ dt = self.program.data_type(tgt)
+ if dt is not None and not dt.get("is_func"):
+ kind, subject = "data", tgt
+ prefill = dt.get("type") or self._guess_data_type(
+ dt.get("size") or 0)
# 3) fall back to the current function itself
if kind is None and ft is not None:
kind, subject, prefill = "func", self._cur.ea, ft.prototype
@@ -4140,6 +4149,13 @@ class IdaTui(App):
return
self.app.call_from_thread(self._open_retype, view, kind, subject, word or "", prefill)
+ @staticmethod
+ def _guess_data_type(size: int) -> str:
+ """A sensible prefill when a global carries no type yet."""
+ return {1: "unsigned __int8", 2: "unsigned __int16",
+ 4: "unsigned __int32", 8: "unsigned __int64"}.get(
+ size, f"char[{size}]" if size > 0 else "void *")
+
def _open_retype(self, view, kind: str, subject: int, word: str, # type: ignore[no-untyped-def]
prefill: str) -> None:
self._retype_ctx = (view, kind, subject, word)
@@ -4166,6 +4182,8 @@ class IdaTui(App):
assert self.program is not None
if kind == "func":
err = self.program.set_function_type(subject, new)
+ elif kind == "data": # a global / data item referenced in the body
+ err = self.program.set_data_type(subject, new)
else: # lvar of the current function
err = self.program.set_lvar_type(self._cur.ea, word, new)
if err:
@@ -5135,9 +5153,10 @@ class IdaTui(App):
self._sync_split(self._active) # re-link with the region map
def on_decomp_view_cursor_moved(self, msg: DecompView.CursorMoved) -> None:
- if self._nav:
+ dv = self._try_view(DecompView)
+ if self._nav and dv is not None:
self._nav[-1].dec_cursor = msg.index
- self._nav[-1].dec_cursor_x = self.query_one(DecompView).cursor_x
+ self._nav[-1].dec_cursor_x = dv.cursor_x
if self._split:
if self._active == "decomp":
self._sync_split("decomp")
@@ -5147,12 +5166,23 @@ class IdaTui(App):
loc = f" @ {msg.ea:#x}" if msg.ea is not None else ""
self._status(f"{self._cur.name}{loc} [pseudocode line {msg.index}]")
+ def _try_view(self, cls): # type: ignore[no-untyped-def]
+ """The code view, or None. App.query_one searches the TOP screen, so a
+ cursor-moved message that lands while any modal is up (the loading
+ overlay, a project switch) would otherwise raise NoMatches and kill the
+ app from a message handler."""
+ try:
+ return self.query_one(cls)
+ except Exception: # noqa: BLE001 -- NoMatches: a modal owns the screen
+ return None
+
def on_listing_view_cursor_moved(self, msg: ListingView.CursorMoved) -> None:
- if self._nav:
+ lst = self._try_view(ListingView)
+ if self._nav and lst is not None:
self._nav[-1].cursor = msg.index
- self._nav[-1].cursor_x = self.query_one(ListingView).cursor_x
+ self._nav[-1].cursor_x = lst.cursor_x
if msg.index >= 0:
- self._nav[-1].scroll_y = round(self.query_one(ListingView).scroll_offset.y)
+ self._nav[-1].scroll_y = round(lst.scroll_offset.y)
if self._split:
if self._active == "listing":
self._sync_split("listing")