diff options
| author | blasty <blasty@local> | 2026-07-10 09:53:56 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-10 09:53:56 +0200 |
| commit | 29f8a238d0e316c4a0fe4081c5412a18f5bb3c64 (patch) | |
| tree | f380c21b8c59e78962bf0d6bfe1d2be365ae93c6 /idatui/domain.py | |
| parent | decompiler: fall back to disassembly when decompilation fails (diff) | |
| download | ida-tui-29f8a238d0e316c4a0fe4081c5412a18f5bb3c64.tar.gz ida-tui-29f8a238d0e316c4a0fe4081c5412a18f5bb3c64.tar.xz ida-tui-29f8a238d0e316c4a0fe4081c5412a18f5bb3c64.zip | |
struct editor: C-style CRUD for local types (Ctrl+T)
New StructEditor overlay: left is the list of local structs/unions, right is an
editable C definition. Enter loads a struct (reconstructed as C from its member
layout via type_inspect), Ctrl+S declares it (create or update via declare_type),
Ctrl+N starts a new one, Del deletes, Esc returns to the list then closes.
Domain: Struct model + Program.list_structs / struct_source / declare_type /
delete_type (anonymous $-types filtered).
Delete needs a 'del_type' tool the ida-pro-mcp server doesn't currently expose;
delete_type detects its absence and reports a clear message instead of failing.
Create/read/update work fully.
Pilot checks: open, list, view C, create, update-in-place, delete(-or-report),
close. full suite 84/84.
Diffstat (limited to 'idatui/domain.py')
| -rw-r--r-- | idatui/domain.py | 77 |
1 files changed, 77 insertions, 0 deletions
diff --git a/idatui/domain.py b/idatui/domain.py index cdbdd67..f52535c 100644 --- a/idatui/domain.py +++ b/idatui/domain.py @@ -92,6 +92,25 @@ class Xref: fn_addr: int | None +@dataclass(frozen=True) +class Struct: + name: str + size: int + is_union: bool + members: int # field count + ordinal: int + + @classmethod + def from_raw(cls, d: dict) -> "Struct": + return cls( + name=d.get("name", ""), + size=int(d.get("size", 0) or 0), + is_union=bool(d.get("is_union", False)), + members=int(d.get("cardinality", 0) or 0), + ordinal=int(d.get("ordinal", 0) or 0), + ) + + @dataclass class Decompilation: ea: int @@ -431,6 +450,64 @@ class Program: return secs[i][2] return None + # -- structs / local types -------------------------------------------- # + def list_structs(self, filter: str = "") -> list[Struct]: + """All local structs/unions (optionally name-substring filtered), sorted + by name.""" + payload = self.client.call("search_structs", filter=filter) + res = payload.get("result", []) if isinstance(payload, dict) else [] + out = [Struct.from_raw(d) for d in res + if isinstance(d, dict) and d.get("name") + and not str(d["name"]).startswith("$")] # skip anonymous UDTs + out.sort(key=lambda s: s.name.lower()) + return out + + def struct_source(self, name: str) -> str: + """A C definition for ``name`` reconstructed from its member layout + (the server exposes members, not printable source). Faithful to IDA's + field names/types; array dims are moved after the field name.""" + payload = self.client.call( + "type_inspect", queries=[{"name": name, "include_members": True}]) + res = payload.get("result", []) if isinstance(payload, dict) else [] + info = res[0] if res and isinstance(res[0], dict) else {} + kw = "union" if info.get("is_union") else "struct" + lines = [f"{kw} {name}", "{"] + for m in info.get("members", []) or []: + if not isinstance(m, dict): + continue + t = str(m.get("type", "")).strip() + fn = m.get("name", "") + base, arr = t, "" + am = re.match(r"^(.*?)((?:\s*\[\d+\])+)\s*$", t) + if am: + base, arr = am.group(1).rstrip(), am.group(2).replace(" ", "") + sep = "" if base.endswith("*") else " " + lines.append(f" {base}{sep}{fn}{arr};") + lines.append("};") + return "\n".join(lines) + + def declare_type(self, decl: str) -> str | None: + """Create or update a C type. Returns None on success, else the parse + error. (Re-declaring a name updates it in place.)""" + payload = self.client.call("declare_type", decls=decl) + res = payload.get("result", []) if isinstance(payload, dict) else [] + if res and isinstance(res[0], dict): + return res[0].get("error") + return None + + def delete_type(self, name: str) -> str | None: + """Delete a named type. Returns None on success, else an error string. + Requires a server-side ``del_type`` tool; if absent, a clear message is + returned instead of raising.""" + try: + self.client.call("del_type", name=name) + return None + except IDAToolError as e: + msg = e.message + if "not found" in msg.lower() and "del_type" in msg: + return "delete needs a 'del_type' tool on the ida-pro-mcp server" + return msg + # -- disassembly ------------------------------------------------------- # def disasm(self, ea: int, name: str | None = None) -> DisasmModel: with self._lock: |
