diff options
| author | blasty <blasty@local> | 2026-07-09 23:28:37 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-09 23:28:37 +0200 |
| commit | 0c738bbcf00cf329847d9409f9639a84adeeced0 (patch) | |
| tree | 94f2068e45f3809eeb34d3a20675b88a7794c697 | |
| parent | default to the pseudocode view instead of disassembly (diff) | |
| download | ida-tui-0c738bbcf00cf329847d9409f9639a84adeeced0.tar.gz ida-tui-0c738bbcf00cf329847d9409f9639a84adeeced0.tar.xz ida-tui-0c738bbcf00cf329847d9409f9639a84adeeced0.zip | |
decompiler: fall back to disassembly when decompilation fails
Opening a function that Hex-Rays can't decompile (import/PLT stubs, mis-analyzed
monsters) now shows the disassembly instead of a '/* decompilation failed */'
panel. A separate _pref (the preferred view, set by Tab) is kept, so only the
failing function falls back — the next decompilable function still opens as
pseudocode. _apply_decomp ignores a stale result if you've navigated away.
Pilot checks: failing func -> disasm (pref kept), next func -> pseudocode.
full suite 77/77.
| -rw-r--r-- | idatui/app.py | 28 | ||||
| -rw-r--r-- | tests/test_tui.py | 35 |
2 files changed, 51 insertions, 12 deletions
diff --git a/idatui/app.py b/idatui/app.py index 41e7e08..c87c27d 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -1232,7 +1232,8 @@ class IdaTui(App): self._filter_timer = None self._sort_col = 0 # 0=addr, 1=name, 2=size self._sort_reverse = False - self._active = "decomp" # default code view (or "disasm") + self._pref = "decomp" # preferred code view (changed by Tab) + self._active = "decomp" # currently shown view (falls back on decomp fail) self._cur: NavEntry | None = None self._search_ctx: tuple[object | None, int] = (None, 1) self._rename_ctx: tuple[object | None, str] = (None, "") @@ -1463,6 +1464,7 @@ class IdaTui(App): if self._cur is None: return self._active = "decomp" if self._active == "disasm" else "disasm" + self._pref = self._active # an explicit toggle sets the preference self._show_active() def action_filter(self) -> None: @@ -2130,6 +2132,9 @@ class IdaTui(App): if self.program is None: return self._cur = entry + # Each open honours the preferred view; a decomp failure last time fell + # back to disasm without changing the preference, so retry decomp here. + self._active = self._pref model = self.program.disasm(entry.ea, entry.name) sy = entry.scroll_y if entry.scroll_y >= 0 else None self.query_one(DisasmView).load( @@ -2176,6 +2181,17 @@ class IdaTui(App): def _apply_decomp(self, ea: int, name: str, dec) -> None: # type: ignore[no-untyped-def] view = self.query_one(DecompView) view.loading = False + if dec.failed: + # No pseudocode for this function: fall back to the disassembly view + # rather than showing an error panel. Keep the pseudocode preference + # so the next (decompilable) function still opens as pseudocode. + if self._cur is None or self._cur.ea != ea: + return # navigated away; stale result + self._active = "disasm" + self._show_active() + self._status( + f"{name} — no pseudocode (decompile failed); showing disassembly") + return if self._active == "decomp": view.focus() # loading cover had blurred it; restore focus # Restore the saved pseudocode position when returning to this function. @@ -2185,13 +2201,9 @@ class IdaTui(App): cx = cur.dec_cursor_x if same else 0 sy = cur.dec_scroll_y if (same and cur.dec_scroll_y >= 0) else -1 sx = cur.dec_scroll_x if same else 0 - if dec.failed: - view.show(ea, f"/* decompilation failed at {ea:#x}: {dec.error} */\n") - self._status(f"{name} — decompile failed; Tab for disasm") - else: - note = " (truncated)" if dec.truncated else "" - view.show(ea, dec.code or "", cursor=c, cursor_x=cx, scroll_y=sy, scroll_x=sx) - self._status(f"{name} @ {ea:#x} [pseudocode {len(dec.code or '')} chars]{note}") + note = " (truncated)" if dec.truncated else "" + view.show(ea, dec.code or "", cursor=c, cursor_x=cx, scroll_y=sy, scroll_x=sx) + self._status(f"{name} @ {ea:#x} [pseudocode {len(dec.code or '')} chars]{note}") def on_decomp_view_cursor_moved(self, msg: DecompView.CursorMoved) -> None: if self._nav: diff --git a/tests/test_tui.py b/tests/test_tui.py index 044aabd..47a0e3e 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -117,6 +117,32 @@ async def run(db): await wait_until(pilot, lambda: not isinstance(app.screen, SymbolPalette), 10) check("Esc closes the palette", not isinstance(app.screen, SymbolPalette)) + # Decompile-failure fallback: a function that can't be decompiled opens + # in disassembly (not an error panel), and the pseudocode preference is + # kept so the next decompilable function still opens as pseudocode. + # (Import/PLT stubs at high addresses fail fast; scan from the top.) + d_view = app.query_one(DisasmView) + c_view = app.query_one(DecompView) + failing = next((f for f in reversed(app.program.functions().all_loaded()) + if app.program.decompile(f.addr).failed), None) + if failing is not None and app._pref == "decomp": + app._goto(hex(failing.addr)) + await wait_until(pilot, lambda: app._cur and app._cur.ea == failing.addr, 20) + await wait_until(pilot, lambda: app._active == "disasm", 20) + check("decompile failure falls back to disassembly (preference kept)", + app._active == "disasm" and d_view.display and not c_view.display + and app._pref == "decomp", + f"active={app._active} pref={app._pref} " + f"disp(dis={d_view.display},dec={c_view.display})") + app._goto("main") + await wait_until(pilot, lambda: app._cur and app._cur.name == "main", 20) + await wait_until(pilot, lambda: app._active == "decomp" and c_view.display, 25) + check("preference preserved: next function opens as pseudocode", + app._active == "decomp" and c_view.display, f"active={app._active}") + else: + check("found a decompile-failing function", failing is not None, + f"pref={app._pref}") + # Open the biggest function we can find (scan a sample of rows for size). # Simpler: select the row whose Size column is largest among first N. biggest_i, biggest_sz = 0, -1 @@ -430,10 +456,10 @@ async def run(db): if be < e <= xref.frm: be, dexp = e, i if dexp >= 0: + app._pref = "decomp" # navigation honours the preferred view app._open_function(xfn.addr, xfn.name) # start elsewhere - await wait_until(pilot, lambda: app._cur.ea == xfn.addr, 20) - app._active = "decomp" - app._show_active() + await wait_until(pilot, lambda: app._cur.ea == xfn.addr + and app._active == "decomp", 20) await wait_until(pilot, lambda: dec.loaded_ea == xfn.addr, 25) # xfn is the referenced function; its name is the token that # appears at the call site the xref points to. @@ -472,7 +498,8 @@ async def run(db): check("xref-select moves the cursor within the same function", dec.cursor == exp2 and exp2 != start_ln, f"dec.cursor={dec.cursor} want={exp2} start={start_ln}") - app._active = "disasm" # restore for the following blocks + app._pref = "disasm" # restore for the following blocks + app._active = "disasm" app._show_active() await pilot.pause(0.05) |
