aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/rpc.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-08-07 23:16:30 +0200
committerblasty <blasty@local>2026-08-07 23:16:30 +0200
commit41b3710d5b6f9be24430fd55c46c83f9ae3b8d83 (patch)
treeed3517997a3248310c904c3a57ea053257162ac2 /idatui/rpc.py
parentExport findings as markdown (Ctrl+E), and the journal that makes it true (diff)
downloadida-tui-41b3710d5b6f9be24430fd55c46c83f9ae3b8d83.tar.gz
ida-tui-41b3710d5b6f9be24430fd55c46c83f9ae3b8d83.tar.xz
ida-tui-41b3710d5b6f9be24430fd55c46c83f9ae3b8d83.zip
Ctrl+F: search the whole database, by text or by bytes
`/` only ever searched the lines of the view you were in. This adds the search you actually need on a binary: over the entire database, either through the rendered disassembly or through the image. * **text** matches the line as displayed, whitespace-normalised, so `call cs:` finds `call cs:getenv_ptr` (IDA's column padding is not something anyone types). Smartcase; `regex` available over RPC. * **bytes** is IDA's own `find_bytes`, so the pattern language people already know works unchanged: hex pairs, `?` wildcards for a whole byte or one nibble (`48 8? ?? 24`), quoted literals (`"Hello", 0`). Commas, no separators (`488B05C3`) and ragged spacing all normalise. **Which mode you meant is guessed, and the guess is biased on purpose.** `dead`, `add`, `cafe` and `ff` are valid hex AND ordinary things to search for, so a bare hex-looking word stays TEXT; nobody types `48 8b ?? c3` meaning prose. `hex:`/`text:` prefixes and F2 override it. The subtle case is a *typo* in a byte pattern. `48 zz c3` first fell through to a text search and reported "no match" — indistinguishable from "those bytes are not in this binary", which is the most misleading answer a search can give. Now any query whose tokens are all byte-sized is treated as bytes, and a bad token is refused BY NAME. IDA does the same thing quietly (find_bytes answers a malformed pattern with zero hits and no error), so the validation lives in Program.search, not just in the UI. Enter searches, then Enter opens the highlighted hit; the title says which it will do, because a database-wide scan is far too slow to run on every keystroke like the other palettes. Navigation goes to the item head — a byte match can start mid-instruction — and the status names the exact address. Also: the `find` RPC verb and `drive find`, which is the one an agent wants (`drive find '48 8b ?? c3'`). idatui/search.py holds the classification and is pure, so the whole question of "what did they mean" is tested offline: tests/test_search.py, 35 checks, 0.1s. Pilot scenario db_search covers the UI end to end. Full suite: 890 passed, 0 failed, 51.3s.
Diffstat (limited to 'idatui/rpc.py')
-rw-r--r--idatui/rpc.py26
1 files changed, 25 insertions, 1 deletions
diff --git a/idatui/rpc.py b/idatui/rpc.py
index 98ff90f..1262ebb 100644
--- a/idatui/rpc.py
+++ b/idatui/rpc.py
@@ -39,7 +39,7 @@ _PROGRAM_METHODS = {
"goto", "open", "rename", "comment", "retype", "follow", "xrefs", "symbols",
"structs", "search", "select", "save", "hex", "toggle_view",
"pseudocode", "disassembly", "xrefs_to", "xrefs_from", "resolve",
- "define", "rename_many", "opfmt", "graph", "export",
+ "define", "rename_many", "opfmt", "graph", "export", "find",
}
# Self-documenting method table (returned by the 'methods' verb).
@@ -77,6 +77,9 @@ METHODS = {
"structs": "open the struct editor",
"export": "{path?,types?=true} write the session's comments/names/types as "
"a markdown report -> {path,comments,names,types}",
+ "find": "{query,mode?=auto|text|bytes,limit?=500,regex?,case?} search the "
+ "WHOLE database: disassembly text, or a byte pattern with "
+ "wildcards (48 8b ?? c3) -> {mode,hits:[{addr,head,line,func}]}",
"search": "{term,direction?=1} incremental search in the code view",
"select": "{index?} choose the highlighted/nth item in the open modal",
"save": "persist the .i64 (Ctrl+S)",
@@ -1175,6 +1178,27 @@ class RpcServer:
return await self._press(
["ctrl+t"], lambda: type(app.screen).__name__ == "StructEditor",
timeout, "structs")
+ if method == "find":
+ from . import search as _search
+ q = str(params.get("query", ""))
+ forced = params.get("mode")
+ forced = None if forced in (None, "auto") else str(forced)
+ mode, cleaned = _search.classify(q, forced)
+ if mode == _search.BYTES:
+ problem = _search.pattern_problem(cleaned)
+ if problem:
+ raise ValueError(f"find: {problem}")
+ cleaned = _search.normalise_pattern(cleaned)
+ hits, err, truncated = await asyncio.to_thread(
+ app.program.search, cleaned, mode,
+ limit=int(params.get("limit", 500)),
+ regex=bool(params.get("regex")), case=bool(params.get("case")))
+ if err:
+ raise ValueError(f"find: {err}")
+ return {"mode": mode, "query": cleaned, "truncated": truncated,
+ "hits": [{"addr": hex(h.addr), "head": hex(h.head),
+ "line": h.line, "func": h.func,
+ "seg": h.seg} for h in hits]}
if method == "export":
# Deliberately NOT driven through the prompt: this is the one verb
# whose whole point is the file it leaves behind, and a driver needs