aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-25 00:51:28 +0200
committerblasty <blasty@local>2026-07-25 00:51:28 +0200
commitbe23d13852f4efd062641957b4dcec10622fc0c6 (patch)
treeb2f020fca47fc0afb472d317e91e60ba468a514f /server
parentsplit: follow the decomp across functions as the listing cursor crosses bounds (diff)
downloadida-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 'server')
-rw-r--r--server/patch_server.py69
1 files changed, 69 insertions, 0 deletions
diff --git a/server/patch_server.py b/server/patch_server.py
index c217561..96c4689 100644
--- a/server/patch_server.py
+++ b/server/patch_server.py
@@ -635,6 +635,75 @@ def decomp_map(
eas.append(hex(e))
lines.append({"ea": eas[0] if eas else None, "eas": eas})
return {"addr": hex(func.start_ea), "lines": lines}
+
+
+_idatui_strings_cache = {}
+
+
+def _idatui_build_strings(min_len):
+ """[(ea, text, length, typename)] for every string IDA found, cached by
+ min_len (rebuilding the list is O(n) and the browser pages through it)."""
+ import idautils
+ import ida_nalt
+ hit = _idatui_strings_cache.get(min_len)
+ if hit is not None:
+ return hit
+ tnames = {}
+ for nm, lbl in (("STRTYPE_C", "C"), ("STRTYPE_C_16", "utf16"),
+ ("STRTYPE_C_32", "utf32"), ("STRTYPE_PASCAL", "pascal")):
+ v = getattr(ida_nalt, nm, None)
+ if v is not None:
+ tnames[v & 0xFF] = lbl
+ items = []
+ for s in idautils.Strings():
+ if s is None:
+ continue
+ try:
+ text = str(s)
+ except Exception: # noqa: BLE001 -- undecodable literal
+ continue
+ if len(text) < min_len:
+ continue
+ st = getattr(s, "strtype", 0) & 0xFF
+ items.append((s.ea, text, getattr(s, "length", len(text)),
+ tnames.get(st, "t%d" % st)))
+ _idatui_strings_cache[min_len] = items
+ return items
+
+
+@tool
+@idasync
+def list_strings(
+ offset: Annotated[int, "Start index into the strings list"] = 0,
+ count: Annotated[int, "Max strings to return (page size)"] = 2000,
+ min_len: Annotated[int, "Minimum string length to include"] = 4,
+ refresh: Annotated[bool, "Rebuild the cached strings list"] = False,
+) -> dict:
+ """Every string literal IDA found in the binary (IDA's Shift+F12 window),
+ paginated: {strings:[{addr,text,len,type}], total, next_offset}. Feeds the
+ TUI's strings browser."""
+ try:
+ min_len = max(int(min_len), 1)
+ except (TypeError, ValueError):
+ min_len = 4
+ try:
+ offset = max(int(offset), 0)
+ except (TypeError, ValueError):
+ offset = 0
+ try:
+ count = max(int(count), 1)
+ except (TypeError, ValueError):
+ count = 2000
+ if refresh:
+ _idatui_strings_cache.pop(min_len, None)
+ items = _idatui_build_strings(min_len)
+ page = items[offset:offset + count]
+ return {
+ "strings": [{"addr": hex(ea), "text": text, "len": ln, "type": ty}
+ for (ea, text, ln, ty) in page],
+ "total": len(items),
+ "next_offset": offset + len(page),
+ }
'''
SNIPPET = f"{BEGIN}\n{BODY.strip()}\n{END}\n"