diff options
| author | blasty <blasty@local> | 2026-08-07 22:55:27 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-08-07 22:55:27 +0200 |
| commit | 2eb2a0a8cff586fffecfcb068c53b65e8f6f9839 (patch) | |
| tree | 82ccb8e8fa9608c41862a13d04827d7c76ac99ee /idatui/domain.py | |
| parent | SPEED.md: the 85ms keypress, and what settle() still cannot see (diff) | |
| download | ida-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/domain.py')
| -rw-r--r-- | idatui/domain.py | 69 |
1 files changed, 69 insertions, 0 deletions
diff --git a/idatui/domain.py b/idatui/domain.py index 6fe73c2..15f4ef4 100644 --- a/idatui/domain.py +++ b/idatui/domain.py @@ -252,6 +252,37 @@ def link_name(raw: str) -> str: @dataclass(frozen=True) +class Comment: + """One comment somebody wrote into the database. + + ``line`` is the disassembly the comment is attached to, carried along so a + report can show what was being commented ON without a second round trip. + ``whole_func`` marks a function comment rather than an instruction one. + """ + addr: int + text: str + repeatable: bool = False + whole_func: bool = False + line: str = "" + seg: str = "" + func: str | None = None + func_addr: int | None = None + + +@dataclass(frozen=True) +class NamedItem: + """An address carrying a real name -- one you typed, or one the file's own + symbols supplied. IDA records both as "user" names and does not remember + which was which, so a report must say so rather than claim authorship.""" + addr: int + name: str + is_func: bool = False + size: int = 0 + proto: str | None = None + seg: str = "" + + +@dataclass(frozen=True) class Linkage: """One import or export: a name this binary takes from, or offers to, other modules. ``module`` is set for imports (the library IDA attributes it to), @@ -1903,6 +1934,44 @@ class Program: self._linkage = out return out + def annotations(self, limit: int = 4000) -> tuple[list["Comment"], list["NamedItem"]]: + """``(comments, names)`` -- everything a person added to this database. + + Not cached: it is the *current* state of your work, and the one caller + (the findings export) asks for it once. ``([], [])`` if the backend has + no such operation, so an alternate client degrades instead of breaking. + """ + try: + payload = self.client.invoke("list_annotations", limit=int(limit)) + except IDAToolError: + return ([], []) + if not isinstance(payload, dict): + return ([], []) + comments = [ + Comment(addr=_as_int(r.get("addr", 0)), text=str(r.get("text", "")), + repeatable=bool(r.get("repeatable")), + whole_func=bool(r.get("whole_func")), + line=str(r.get("line", "") or ""), + seg=str(r.get("seg", "") or ""), + func=(r.get("func") or None), + func_addr=(_as_int(r["func_addr"]) if r.get("func_addr") else None)) + for r in payload.get("comments", []) if isinstance(r, dict) and r.get("text")] + names = [ + NamedItem(addr=_as_int(r.get("addr", 0)), name=str(r.get("name", "")), + is_func=bool(r.get("func")), size=int(r.get("size", 0) or 0), + proto=(r.get("proto") or None), seg=str(r.get("seg", "") or "")) + for r in payload.get("names", []) if isinstance(r, dict) and r.get("name")] + return (comments, names) + + + def journal_get(self) -> str: + """The findings journal blob stored in this database ('' if none).""" + payload = self.client.invoke("journal_get") + return str(payload.get("data", "")) if isinstance(payload, dict) else "" + + def journal_put(self, data: str) -> None: + self.client.invoke("journal_put", data=str(data)) + def decomp_map(self, ea: int) -> list[list[int]]: """Per-pseudocode-line instruction coverage for the split-view region highlight: a list aligned to the decompiled lines, each the EAs the |
