diff options
| author | blasty <blasty@local> | 2026-07-25 00:51:28 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-25 00:51:28 +0200 |
| commit | be23d13852f4efd062641957b4dcec10622fc0c6 (patch) | |
| tree | b2f020fca47fc0afb472d317e91e60ba468a514f /tests | |
| parent | split: follow the decomp across functions as the listing cursor crosses bounds (diff) | |
| download | ida-tui-be23d13852f4efd062641957b4dcec10622fc0c6.tar.gz ida-tui-be23d13852f4efd062641957b4dcec10622fc0c6.tar.xz ida-tui-be23d13852f4efd062641957b4dcec10622fc0c6.zip | |
strings: browse every string in the binary and jump to it (IDA's Shift+F12)
ida-pro-mcp exposes no full strings list (only a filtered/capped "interesting"
survey), so this is a new injected tool plus a filterable browser.
* server/patch_server.py: list_strings(offset,count,min_len,refresh) — every
literal from idautils.Strings() as {addr,text,len,type}, paginated, with a
module-level cache keyed by min_len (rebuilding is O(n) and the browser pages
the whole list).
* domain: StrLit dataclass + Program.strings() — pages the full list once and
caches it.
* app: StringsPalette modal (mirrors SymbolPalette) — case-insensitive substring
filter with the match highlighted, addr/len/text columns, ↑↓/Enter/Esc.
Bodies are sanitized to one printable line (\n/\r/\t escaped, non-printables
dropped, long strings clipped) so control chars can't break the layout;
display strings are pre-rendered+pre-lowered once since filtering runs per
keystroke. Enter jumps to the literal in the unified listing via _goto_ea.
Bound to '"' and Shift+F12, plus a "Strings…" command-palette entry.
Verified on echo: 150 strings listed with addr/len/text, filtering 'usage'
narrows to 2 (case-insensitive), Enter lands the listing cursor on the literal.
Pilot `strings` scenario 6/6; full suite 165/2-flake.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_scenarios.py | 45 |
1 files changed, 44 insertions, 1 deletions
diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index fad54ec..2476b21 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -25,7 +25,8 @@ import traceback sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from idatui.app import ( # noqa: E402 ConfirmScreen, DecompView, DisasmView, FunctionsPanel, HexView, IdaTui, - ListingView, StructEditor, SymbolPalette, XrefsScreen, + ListingView, StringsPalette, StructEditor, SymbolPalette, XrefsScreen, + _str_display, ) from textual.widgets import ( # noqa: E402 DataTable, Footer, Input, OptionList, Static, TextArea, @@ -339,6 +340,48 @@ async def s_command_palette(c: Ctx): await c.wait(lambda: app._active != "hex", 5) +@scenario("strings") +async def s_strings(c: Ctx): + app = c.app + await c.open_biggest("listing") + items = app.program.strings() + c.check("program.strings() lists the binary's literals", len(items) > 3, + f"n={len(items)}") + if not items: + return + c.check("strings carry addr/text/length", + all(s.addr > 0 and s.text and s.length > 0 for s in items[:5]), + f"first={items[0]}") + await c.press("quotation_mark") + opened = await c.wait(lambda: isinstance(app.screen, StringsPalette), 25) + c.check('\'"\' opens the strings browser', opened, + f"screen={type(app.screen).__name__}") + if not opened: + return + pal = app.screen + c.check("the browser lists strings", len(pal._results) > 0, + f"results={len(pal._results)}") + # filter on a fragment of a real (unescaped) literal + target = next((s for s in items + if len(s.text) >= 6 and _str_display(s.text) == s.text), None) + if target is not None: + frag = target.text[:6] + pal.query_one(Input).value = frag + await c.pause(0.2) + ok = (pal._results + and all(frag.lower() in s.text.lower() for s in pal._results)) + c.check("filtering narrows to matching strings", bool(ok), + f"frag={frag!r} n={len(pal._results)}") + want = pal._results[0].addr + await c.press("enter") + await c.wait(lambda: not isinstance(app.screen, StringsPalette), 10) + landed = await c.wait(lambda: c.lst._cursor_ea() == want, 20) + c.check("Enter jumps to the string in the unified listing", landed, + f"cursor={c.lst._cursor_ea()} want={want:#x}") + else: + await c.press("escape") + + @scenario("split_view") async def s_split_view(c: Ctx): app, lst, dec = c.app, c.lst, c.dec |
