aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/domain.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-22 23:21:16 +0200
committerblasty <blasty@local>2026-07-22 23:21:16 +0200
commitad22cf5beb0588c8f454bebfa0cc7c7897e4e7a3 (patch)
tree8800083d7627a998dce628fe47ac1c493ee76396 /idatui/domain.py
parentpane: reap leaked idalib workers; drive: friendlier name resolution (fixes sp... (diff)
downloadida-tui-ad22cf5beb0588c8f454bebfa0cc7c7897e4e7a3.tar.gz
ida-tui-ad22cf5beb0588c8f454bebfa0cc7c7897e4e7a3.tar.xz
ida-tui-ad22cf5beb0588c8f454bebfa0cc7c7897e4e7a3.zip
app: non-function regions — open a flat listing + c/p/u edit verbs (M0)
Navigating to an address not inside a function no longer refuses ("no function contains X"): _do_navigate now opens a region view (flat listing anchored at the EA, forced to disasm since there's no pseudocode). The server's no-function disasm already walks heads to the segment end, so DisasmModel is reused as-is; NavEntry gains is_region. Adds the IDA c/p/u structure-edit verbs on the disasm view: c = define_code (undefine-first, since create_insn won't carve a live item) p = define_func u = undefine wired via EditItemRequested -> _do_edit_item worker -> Program.define_code/ define_func/undefine. define_func upgrades the region to a real function view in place. New Program.bump_items() invalidates the item/function structure caches (disasm blocks, decomp, function indices + name gen) — broader than bump_names(), which only covers renames. Pilot: new `region_define` scenario undefines a small function, navigates to the bare region via the 'g' prompt (asserts it opens, not refused), then 'p' recreates the function and upgrades the view; restores the IDB in a finally. 107 passed / 1 pre-existing flaky (filter, fails on baseline).
Diffstat (limited to 'idatui/domain.py')
-rw-r--r--idatui/domain.py62
1 files changed, 62 insertions, 0 deletions
diff --git a/idatui/domain.py b/idatui/domain.py
index c374b71..53fd907 100644
--- a/idatui/domain.py
+++ b/idatui/domain.py
@@ -853,6 +853,68 @@ class Program:
for m in models:
m.invalidate()
+ def bump_items(self) -> None:
+ """Signal that item/function STRUCTURE changed (define code/data/func,
+ undefine). Unlike a rename this can move instruction boundaries and
+ change function membership anywhere, so drop the disasm block caches,
+ the decompilation cache and the cached function indices outright, and
+ bump the name generation too (labels/names may appear or vanish)."""
+ with self._lock:
+ self._name_gen += 1
+ self._indices.clear()
+ self._decomp.clear()
+ models = list(self._disasm.values())
+ self._disasm.clear()
+ for m in models:
+ m.invalidate()
+
+ # -- item / function structure edits (IDA c/d/u/p) --------------------- #
+ @staticmethod
+ def _first_result(payload) -> dict:
+ """Unwrap the first row of a batch tool response ({result:[...]} or a
+ bare list); soft per-item ``error`` fields ride along in the dict."""
+ data = payload.get("result", payload) if isinstance(payload, dict) else payload
+ if isinstance(data, list):
+ return data[0] if data and isinstance(data[0], dict) else {}
+ return data if isinstance(data, dict) else {}
+
+ def define_code(self, ea: int) -> None:
+ """Convert the bytes at ``ea`` into a code instruction (IDA's 'c').
+ Undefine first so it works even when the bytes are currently part of a
+ data/align item — ``create_insn`` refuses to carve into a live item."""
+ try:
+ self.client.call("undefine", items=[{"addr": hex(ea)}])
+ except IDAToolError:
+ pass # nothing defined here yet -> just try to create the insn
+ res = self._first_result(
+ self.client.call("define_code", items=[{"addr": hex(ea)}]))
+ if res.get("error"):
+ raise IDAToolError(f"define code @ {ea:#x}: {res['error']}")
+
+ def define_func(self, ea: int) -> None:
+ """Create a function starting at ``ea`` (IDA's 'p')."""
+ res = self._first_result(
+ self.client.call("define_func", items=[{"addr": hex(ea)}]))
+ if res.get("error"):
+ raise IDAToolError(f"create function @ {ea:#x}: {res['error']}")
+
+ def undefine(self, ea: int, size: int | None = None) -> None:
+ """Undefine the item at ``ea`` back to raw bytes (IDA's 'u')."""
+ item: dict = {"addr": hex(ea)}
+ if size:
+ item["size"] = int(size)
+ res = self._first_result(self.client.call("undefine", items=[item]))
+ if res.get("error"):
+ raise IDAToolError(f"undefine @ {ea:#x}: {res['error']}")
+
+ def region_label(self, ea: int) -> str:
+ """Display name for a non-function address (segment-qualified)."""
+ try:
+ sec = self.section_of(ea)
+ except Exception: # noqa: BLE001
+ sec = None
+ return f"{sec} @ {ea:#x}" if sec else f"<no function> @ {ea:#x}"
+
@staticmethod
def _fetch_output(url: str, timeout: float = 15.0):
"""GET the server's cached full-output blob (plain HTTP, not MCP)."""