diff options
| author | blasty <blasty@local> | 2026-07-25 14:48:21 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-25 14:48:21 +0200 |
| commit | 603771a57d9e4ff8c4f5e64e37944e54530ca16e (patch) | |
| tree | ed319dad43a2ad720885e3b323e5eeda9edcde52 | |
| parent | palette: cut the per-keystroke cost of project-wide symbol search (diff) | |
| download | ida-tui-603771a57d9e4ff8c4f5e64e37944e54530ca16e.tar.gz ida-tui-603771a57d9e4ff8c4f5e64e37944e54530ca16e.tar.xz ida-tui-603771a57d9e4ff8c4f5e64e37944e54530ca16e.zip | |
fix: project-wide search crashed on a name shared by two binaries
TypeError: '<' not supported between instances of 'Hit' and 'Hit'.
The rank tuple built for project scope ended with the Hit itself, so when two
binaries contain the SAME symbol name the first three fields (match position,
length, text) tied and sort() fell through to comparing Hit dataclasses, which
aren't orderable. Shared names — main, textdomain, the whole libc surface — are
the norm in a project, so this fired almost immediately.
Sort on an explicit key that stops at the orderable fields and breaks ties on
(binary, addr), which also makes the ordering deterministic instead of
input-order dependent.
Regression test in test_project_ui.py, where 'main' exists in both echo and cat:
widen to project scope and assert the shared name is found in both binaries — it
crashed before the fix. 20/20 there, suite 191/0.
| -rw-r--r-- | idatui/app.py | 5 | ||||
| -rw-r--r-- | tests/test_project_ui.py | 23 |
2 files changed, 27 insertions, 1 deletions
diff --git a/idatui/app.py b/idatui/app.py index f589cea..c3578da 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -2120,7 +2120,10 @@ class SymbolPalette(ModalScreen): for h in hits: at = h.text.lower().find(q) scored.append((at if at >= 0 else 1 << 30, len(h.text), h.text, h)) - scored.sort() + # Sort on an explicit key: the same symbol name in two binaries ties + # on (position, length, text), and a bare sort() would then fall + # through to comparing Hit objects, which aren't orderable. + scored.sort(key=lambda t: (t[0], t[1], t[2], t[3].binary, t[3].addr)) rows = [(h.binary, h.addr, h.text, tuple(range(at, at + len(q))) if at < (1 << 30) else ()) for at, _, _, h in scored[:self.LIMIT]] diff --git a/tests/test_project_ui.py b/tests/test_project_ui.py index 025107d..8a0fd2b 100644 --- a/tests/test_project_ui.py +++ b/tests/test_project_ui.py @@ -138,6 +138,29 @@ async def run(bins): again and app._cur.ea == where, f"cur={app._cur.ea if app._cur else None} want={where}") + # -- project-wide symbol search ------------------------------- # + # 'main' exists in BOTH binaries: identical name, so the rank tuple + # ties and a bare sort() would fall through to comparing Hit objects + # ('<' not supported between instances of 'Hit'). + from idatui.app import SymbolPalette + await settle(lambda: app._index is not None + and len(app._index.counts()) == 2, 60) + check("both binaries got indexed", + len(app._index.counts()) == 2, f"{app._index.counts()}") + await pilot.press("ctrl+n") + if await settle(lambda: isinstance(app.screen, SymbolPalette), 20): + pal = app.screen + pal.query_one(Input).value = "main" + await pilot.pause(0.3) + await pilot.press("f2") # widen to the whole project + await pilot.pause(0.4) + names = [(b, n) for b, _, n in pal._results] + check("project scope finds a name shared by both binaries", + len({b for b, n in names if n == "main"}) == 2, + f"{names[:6]}") + await pilot.press("escape") + await pilot.pause(0.2) + # -- the promise: nothing was written next to the sources ---------- # left = sorted(os.listdir(src)) check("the source tree stays pristine (no .i64/scratch beside it)", |
