aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/rpc.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-08-07 22:55:27 +0200
committerblasty <blasty@local>2026-08-07 22:55:27 +0200
commit2eb2a0a8cff586fffecfcb068c53b65e8f6f9839 (patch)
tree82ccb8e8fa9608c41862a13d04827d7c76ac99ee /idatui/rpc.py
parentSPEED.md: the 85ms keypress, and what settle() still cannot see (diff)
downloadida-tui-2eb2a0a8cff586fffecfcb068c53b65e8f6f9839.tar.gz
ida-tui-2eb2a0a8cff586fffecfcb068c53b65e8f6f9839.tar.xz
ida-tui-2eb2a0a8cff586fffecfcb068c53b65e8f6f9839.zip
Export findings as markdown (Ctrl+E), and the journal that makes it true
The output of an RE session is what you worked out, and it was locked in a .i64 that only IDA can read. Ctrl+E (or `drive export`, or the `export` RPC verb) writes it out: your comments grouped by function with the line each annotates, the names and prototypes you set, the types you declared. **The hard part was provenance, and it needed a mechanism, not a filter.** A database does not record WHO wrote a comment or a name. IDA's analyzer sets `; switch 73 cases` and `; s1` with the same `set_cmt` a person uses, and the ELF loader sets `elf_gnu_hash_nbuckets` and `File class: 64-bit` the same way. Four probes, all negative: the FF_COMM flag is identical, `get_cmt` returns them all, `generate_disasm_line` tags every one of them COLOR_REGCMT (not COLOR_AUTOCMT), and they survive with auto-comments switched off. A first cut filtered by shape and produced a report whose first screen was ELF header trivia and `; jumptable ... case 99`. So idatui journals its own edits (idatui/journal.py) into a netnode in the database: it rides along in the .i64, it is still there next session, and the report is then exactly what was done here -- 2 findings out of a database carrying 693 other annotations. Recorded at the choke points in edit_ctl (rename, name-address, comment, retype) and in the struct editor; flushed on save, on export and on quit, so no edit pays a round trip. Without a journal (a database worked on in the IDA GUI, or predating this) the report falls back to filtering by shape -- dummy names, imports, loader segments, the analyzer's stereotyped switch/jumptable strings -- and says so in the document rather than claiming authorship it cannot prove. idatui/findings.py splits gather (needs IDA) from render (does not), so the formatting, grouping, sorting, escaping and the empty cases are tested offline: tests/test_findings.py, 32 checks, no worker, 0.1s. The pilot scenario covers the round trip that matters -- edit through the UI, export, find it in the file, and reload the journal from the .i64. Full suite: 842 passed, 0 failed, 51.2s.
Diffstat (limited to 'idatui/rpc.py')
-rw-r--r--idatui/rpc.py22
1 files changed, 21 insertions, 1 deletions
diff --git a/idatui/rpc.py b/idatui/rpc.py
index 2a63812..98ff90f 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",
+ "define", "rename_many", "opfmt", "graph", "export",
}
# Self-documenting method table (returned by the 'methods' verb).
@@ -75,6 +75,8 @@ METHODS = {
"xrefs": "open the xref picker",
"symbols": "{query?} open the symbol palette",
"structs": "open the struct editor",
+ "export": "{path?,types?=true} write the session's comments/names/types as "
+ "a markdown report -> {path,comments,names,types}",
"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)",
@@ -1173,6 +1175,24 @@ class RpcServer:
return await self._press(
["ctrl+t"], lambda: type(app.screen).__name__ == "StructEditor",
timeout, "structs")
+ 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
+ # the path back, not a screenshot of a prompt closing.
+ from . import findings
+ path = params.get("path")
+ app.journal.load(app.program)
+ app.journal.flush(app.program)
+ out, f = await asyncio.to_thread(
+ findings.export, app.program, app._open_path or "",
+ str(path) if path else None,
+ types=bool(params.get("types", True)), journal=app.journal)
+ app._status(f"exported findings → {out}", priority=True)
+ await drain(app)
+ return {"path": out, "comments": len(f.comments),
+ "names": len(findings._user_names(f)),
+ "types": len(f.types), "functions": f.n_functions,
+ "bytes": os.path.getsize(out) if os.path.exists(out) else 0}
if method == "close":
return await self._press(["escape"], timeout=timeout)
if method == "save":