aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/remote_ops.py
diff options
context:
space:
mode:
Diffstat (limited to 'idatui/remote_ops.py')
-rw-r--r--idatui/remote_ops.py1683
1 files changed, 1683 insertions, 0 deletions
diff --git a/idatui/remote_ops.py b/idatui/remote_ops.py
new file mode 100644
index 0000000..a67b2b7
--- /dev/null
+++ b/idatui/remote_ops.py
@@ -0,0 +1,1683 @@
+"""Typed remote operations executed through ida-nexus."""
+
+from __future__ import annotations
+# ruff: noqa
+
+import threading
+from collections.abc import Callable
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+if TYPE_CHECKING:
+ from ida_domain import Database
+
+
+def operation_label() -> str:
+ """Display attribution for the current call; ready for per-user context."""
+ return "IDA TUI"
+
+
+def data_type(db: Database, **a: Any) -> Any:
+ ea = int(str(a["addr"]), 16)
+ try:
+ tif = db.types.get_at(ea)
+ fn = db.functions.get_at(ea)
+ result = {
+ "addr": hex(ea),
+ "name": db.names.get_at(ea) or "",
+ "type": tif.dstr() if tif else "",
+ "size": int(db.heads.size(ea)) if db.heads.is_head(ea) else 0,
+ "is_func": bool(fn),
+ }
+ except Exception as exc:
+ result = {"addr": hex(ea), "error": str(exc)}
+ return result
+
+
+def declare_type(db: Database, **a: Any) -> Any:
+ import ida_typeinf
+
+ decls = a.get("decls", "")
+ if isinstance(decls, str):
+ decls = [decls]
+ rows = []
+ for declaration in decls:
+ try:
+ errors = int(
+ db.types.parse_declarations(ida_typeinf.get_idati(), declaration)
+ )
+ rows.append(
+ {
+ "ok": errors == 0,
+ **({} if errors == 0 else {"error": f"{errors} parse error(s)"}),
+ }
+ )
+ except Exception as exc:
+ rows.append({"ok": False, "error": str(exc)})
+ result = {"result": rows}
+ return result
+
+
+def decomp_error(db: Database, **a: Any) -> Any:
+ import ida_hexrays, ida_ida
+
+ ea = int(str(a["addr"]), 16)
+ fn = db.functions.get_at(ea)
+ result = {"addr": hex(ea), "bitness": ida_ida.inf_get_app_bitness()}
+ if fn is None:
+ result["reason"] = "no function here"
+ else:
+ try:
+ failure = ida_hexrays.hexrays_failure_t()
+ cfunc = ida_hexrays.decompile_func(fn, failure)
+ if cfunc is not None:
+ result["reason"] = ""
+ else:
+ result.update(
+ {
+ "reason": failure.desc() or f"error {failure.code}",
+ "code": int(failure.code),
+ "errea": hex(int(failure.errea)),
+ }
+ )
+ except Exception as exc:
+ result["reason"] = f"{type(exc).__name__}: {exc}"
+ return result
+
+
+def define_code(db: Database, **a: Any) -> Any:
+ import ida_ua
+
+ rows = []
+ for item in a.get("items", []):
+ ea = int(str(item["addr"]), 16)
+ size = int(ida_ua.create_insn(ea))
+ rows.append(
+ {
+ "addr": hex(ea),
+ "ok": size > 0,
+ "size": size,
+ **({} if size > 0 else {"error": "instruction did not decode"}),
+ }
+ )
+ result = {"result": rows}
+ return result
+
+
+def define_code_run(db: Database, **a: Any) -> Any:
+ import ida_bytes, ida_idp, ida_segment, ida_ua, idaapi
+
+ ea, limit = int(str(a["addr"]), 16), max(1, min(int(a.get("limit", 20000)), 200000))
+ seg = ida_segment.getseg(ea)
+ if seg is None:
+ result = {"addr": a["addr"], "error": "no segment", "count": 0}
+ else:
+ start, count, stopped, hi = ea, 0, "limit", int(seg.end_ea)
+ while count < limit:
+ if ea >= hi:
+ stopped = "segment"
+ break
+ flags = ida_bytes.get_flags(ea)
+ if ida_bytes.is_code(flags) or ida_bytes.is_data(flags):
+ stopped = "defined"
+ break
+ size = int(ida_ua.create_insn(ea))
+ if size <= 0:
+ stopped = "undecodable"
+ break
+ count += 1
+ insn = ida_ua.insn_t()
+ if ida_ua.decode_insn(insn, ea) > 0:
+ try:
+ is_ret = bool(ida_idp.is_ret_insn(insn))
+ except Exception:
+ is_ret = False
+ if is_ret or (insn.get_canon_feature() & idaapi.CF_STOP):
+ ea += size
+ stopped = "flow"
+ break
+ ea += size
+ result = {
+ "start": hex(start),
+ "end": hex(ea),
+ "count": count,
+ "stopped": stopped,
+ }
+ return result
+
+
+def define_func(db: Database, **a: Any) -> Any:
+ rows = []
+ for item in a.get("items", []):
+ ea = int(str(item["addr"]), 16)
+ ok = bool(db.functions.create(ea))
+ rows.append(
+ {
+ "addr": hex(ea),
+ "ok": ok,
+ **({} if ok else {"error": "IDA refused the function"}),
+ }
+ )
+ result = {"result": rows}
+ return result
+
+
+def define_func_run(db: Database, **a: Any) -> Any:
+ import ida_bytes, ida_funcs, ida_segment
+
+ ea = int(str(a["addr"]), 16)
+ fn = db.functions.get_at(ea)
+ if fn is not None and int(fn.start_ea) == ea:
+ result = {
+ "addr": hex(ea),
+ "ok": True,
+ "start": hex(ea),
+ "end": hex(int(fn.end_ea)),
+ "how": "existed",
+ }
+ else:
+ automatic = bool(db.functions.create(ea))
+ if not automatic:
+ seg = db.segments.get_at(ea)
+ end = ea
+ hi = int(seg.end_ea) if seg else ea
+ while end < hi and ida_bytes.is_code(ida_bytes.get_flags(end)):
+ nxt = int(ida_bytes.get_item_end(end))
+ if nxt <= end:
+ break
+ end = nxt
+ ok = bool(end > ea and ida_funcs.add_func(ea, end))
+ else:
+ ok = True
+ fn = db.functions.get_at(ea)
+ result = (
+ {
+ "addr": hex(ea),
+ "ok": True,
+ "start": hex(int(fn.start_ea)),
+ "end": hex(int(fn.end_ea)),
+ "how": "auto" if automatic else "explicit-end",
+ }
+ if ok and fn is not None
+ else {
+ "addr": hex(ea),
+ "ok": False,
+ "error": f"IDA refused a function at {ea:#x}",
+ }
+ )
+ return result
+
+
+def del_type(db: Database, **a: Any) -> Any:
+ import ida_typeinf
+
+ name = str(a["name"])
+ ok = bool(
+ ida_typeinf.del_named_type(ida_typeinf.get_idati(), name, ida_typeinf.NTF_TYPE)
+ )
+ result = {
+ "name": name,
+ "deleted": ok,
+ **({} if ok else {"error": f"Type {name!r} not found or could not be deleted"}),
+ }
+ return result
+
+
+def disasm(db: Database, **a: Any) -> Any:
+ ea = int(str(a["addr"]), 16)
+ fn = db.functions.get_at(ea)
+ if fn is None:
+ result = {"instructions": [], "total_instructions": 0, "instruction_count": 0}
+ else:
+ instructions = list(db.functions.get_instructions(fn))
+ limit = max(1, int(a.get("max_instructions", len(instructions) or 1)))
+ rows = [
+ {
+ "addr": hex(int(insn.ea)),
+ "instruction": db.instructions.get_disassembly(insn),
+ }
+ for insn in instructions[:limit]
+ ]
+ result = {
+ "instructions": rows,
+ "total_instructions": len(instructions),
+ "instruction_count": len(instructions),
+ }
+ return result
+
+
+def file_regions(db: Database, **a: Any) -> Any:
+ import idaapi
+
+ rows = []
+ for seg in db.segments.get_all():
+ try:
+ file_off = int(idaapi.get_fileregion_offset(seg.start_ea))
+ except Exception:
+ file_off = -1
+ if file_off < 0 or file_off >= (1 << 48):
+ file_off = -1
+ rows.append(
+ {
+ "start": hex(int(seg.start_ea)),
+ "end": hex(int(seg.end_ea)),
+ "file_off": file_off,
+ "name": db.segments.get_name(seg) or "",
+ }
+ )
+ result = {"regions": rows}
+ return result
+
+
+def flowchart(db: Database, **a: Any) -> Any:
+ import ida_funcs, ida_gdl
+
+ ea = int(str(a["addr"]), 16)
+ fn = ida_funcs.get_func(ea)
+ if fn is None:
+ result = {"addr": hex(ea), "error": "no function at that address", "blocks": []}
+ else:
+ fc = ida_gdl.FlowChart(fn, flags=ida_gdl.FC_PREDS)
+ index, order = {}, []
+ for bb in fc:
+ index[bb.start_ea] = len(order)
+ order.append(bb)
+ blocks = []
+ for bb in order:
+ sl = [s for s in bb.succs() if s.start_ea in index]
+ succs = []
+ for s in sl:
+ # Edge kind is what the graph view colours by: an n-way dispatch is
+ # "switch", a successor that is literally the next address falls
+ # through, anything else is a taken branch.
+ if len(sl) > 2:
+ kind = "switch"
+ elif s.start_ea == bb.end_ea:
+ kind = "fall"
+ else:
+ kind = "jump"
+ succs.append([index[s.start_ea], kind])
+ blocks.append(
+ {
+ "id": index[bb.start_ea],
+ "start": hex(int(bb.start_ea)),
+ "end": hex(int(bb.end_ea)),
+ "succs": succs,
+ }
+ )
+ result = {
+ "addr": hex(ea),
+ "func": {
+ "addr": hex(int(fn.start_ea)),
+ "end": hex(int(fn.end_ea)),
+ "name": ida_funcs.get_func_name(fn.start_ea) or "",
+ },
+ "entry": index.get(fn.start_ea, 0),
+ "blocks": blocks,
+ }
+ return result
+
+
+def force_recompile(db: Database, **a: Any) -> Any:
+ import ida_hexrays
+
+ rows = []
+ for item in a.get("items", []):
+ ea = int(str(item["addr"]), 16)
+ ida_hexrays.mark_cfunc_dirty(ea, False)
+ rows.append({"addr": hex(ea), "ok": True})
+ result = {"result": rows}
+ return result
+
+
+def func_types(db: Database, **a: Any) -> Any:
+ import ida_typeinf
+
+ ea = int(str(a["addr"]), 16)
+ fn = db.functions.get_at(ea)
+ if fn is None:
+ result = {"addr": a["addr"], "error": "no function at address"}
+ else:
+ pseudo = db.pseudocode.decompile(fn)
+ name = db.functions.get_name(fn) or ""
+ tif = pseudo.get_func_type()
+ try:
+ prototype = (
+ ida_typeinf.print_tinfo(
+ "", 0, 0, ida_typeinf.PRTYPE_1LINE, tif, name, ""
+ )
+ if tif
+ else ""
+ )
+ except Exception:
+ prototype = tif.dstr() if tif else ""
+ lvars = [
+ {
+ "name": var.name,
+ "type": var.type_info.dstr() if var.type_info else "",
+ "is_arg": bool(var.is_arg),
+ }
+ for var in pseudo.local_variables
+ ]
+ result = {
+ "addr": hex(int(fn.start_ea)),
+ "name": name,
+ "prototype": (prototype or "").strip(),
+ "lvars": lvars,
+ }
+ return result
+
+
+def get_bytes(db: Database, **a: Any) -> Any:
+ rows = []
+ for region in a.get("regions", []):
+ ea, size = int(str(region["addr"]), 16), int(region["size"])
+ raw = db.bytes.get_bytes_at(ea, size) or b""
+ rows.append({"addr": region["addr"], "data": " ".join(f"{b:02x}" for b in raw)})
+ result = {"result": rows}
+ return result
+
+
+def journal_get(db: Database, **a: Any) -> Any:
+ import ida_netnode
+
+ n = ida_netnode.netnode(a.get("node", "$ idatui.journal"))
+ blob = n.getblob(0, "I") if ida_netnode.exist(n) else None
+ result = {"data": blob.decode("utf-8", "replace") if blob else ""}
+ return result
+
+
+def journal_put(db: Database, **a: Any) -> Any:
+ import ida_netnode
+
+ n = ida_netnode.netnode(a.get("node", "$ idatui.journal"), 0, True)
+ payload = (a.get("data") or "").encode("utf-8")
+ n.setblob(payload, 0, "I")
+ result = {"ok": True, "bytes": len(payload)}
+ return result
+
+
+def list_annotations(db: Database, **a: Any) -> Any:
+ import ida_bytes, ida_funcs, ida_lines, ida_nalt, ida_name
+ import ida_segment, ida_typeinf, idautils
+
+ limit = max(1, int(a.get("limit", 4000)))
+ max_scan = max(1000, int(a.get("max_scan", 2000000)))
+ comments, names = [], []
+ scanned = 0
+
+ def _line(ea):
+ try:
+ txt = ida_lines.generate_disasm_line(ea, ida_lines.GENDSM_REMOVE_TAGS)
+ except Exception:
+ txt = ""
+ return " ".join((txt or "").split())
+
+ for ea, nm in idautils.Names():
+ if len(names) >= limit:
+ break
+ if not nm or not ida_bytes.has_user_name(ida_bytes.get_flags(ea)):
+ continue
+ fn = ida_funcs.get_func(ea)
+ is_fn = fn is not None and int(fn.start_ea) == int(ea)
+ proto = None
+ if is_fn:
+ try:
+ ti = ida_typeinf.tinfo_t()
+ if ida_nalt.get_tinfo(ti, ea):
+ proto = str(ti)
+ except Exception:
+ proto = None
+ seg = ida_segment.getseg(ea)
+ names.append(
+ {
+ "addr": hex(int(ea)),
+ "name": nm,
+ "func": is_fn,
+ "size": (int(fn.end_ea - fn.start_ea) if is_fn else 0),
+ "proto": proto,
+ "seg": (ida_segment.get_segm_name(seg) if seg else ""),
+ }
+ )
+
+ for i in range(ida_segment.get_segm_qty()):
+ seg = ida_segment.getnseg(i)
+ if seg is None or len(comments) >= limit or scanned >= max_scan:
+ continue
+ for ea in idautils.Heads(seg.start_ea, seg.end_ea):
+ scanned += 1
+ if len(comments) >= limit or scanned >= max_scan:
+ break
+ if not ida_bytes.has_cmt(ida_bytes.get_flags(ea)):
+ continue
+ for rep in (False, True):
+ text = ida_bytes.get_cmt(ea, rep)
+ if text:
+ fn = ida_funcs.get_func(ea)
+ comments.append(
+ {
+ "addr": hex(int(ea)),
+ "text": text,
+ "repeatable": rep,
+ "line": _line(ea),
+ "seg": ida_segment.get_segm_name(seg),
+ "func": (
+ ida_funcs.get_func_name(fn.start_ea) if fn else None
+ ),
+ "func_addr": (hex(int(fn.start_ea)) if fn else None),
+ }
+ )
+
+ # Whole-function comments are not on the byte flags, so the scan cannot see them.
+ for fn_ea in idautils.Functions():
+ fn = ida_funcs.get_func(fn_ea)
+ if fn is None or len(comments) >= limit:
+ continue
+ for rep in (False, True):
+ text = ida_funcs.get_func_cmt(fn, rep)
+ if text:
+ seg = ida_segment.getseg(fn_ea)
+ comments.append(
+ {
+ "addr": hex(int(fn_ea)),
+ "text": text,
+ "repeatable": rep,
+ "line": "",
+ "whole_func": True,
+ "seg": (ida_segment.get_segm_name(seg) if seg else ""),
+ "func": ida_funcs.get_func_name(fn_ea),
+ "func_addr": hex(int(fn_ea)),
+ }
+ )
+ result = {
+ "comments": comments,
+ "names": names,
+ "scanned": scanned,
+ "truncated": (
+ len(comments) >= limit or len(names) >= limit or scanned >= max_scan
+ ),
+ }
+ return result
+
+
+def list_funcs(db: Database, **a: Any) -> Any:
+ import fnmatch
+
+ queries = a.get("queries") or [{}]
+ q = queries[0]
+ offset, count = max(0, int(q.get("offset", 0))), max(1, int(q.get("count", 500)))
+ pattern = str(q.get("filter") or "").lower()
+ if pattern and not any(ch in pattern for ch in "*?["):
+ pattern = "*" + pattern + "*"
+ rows = []
+ for fn in db.functions.get_all():
+ name = db.functions.get_name(fn) or f"sub_{int(fn.start_ea):X}"
+ if pattern and not fnmatch.fnmatchcase(name.lower(), pattern):
+ continue
+ rows.append(
+ {
+ "addr": hex(int(fn.start_ea)),
+ "name": name,
+ "size": int(fn.end_ea) - int(fn.start_ea),
+ }
+ )
+ page = rows[offset : offset + count]
+ result = {
+ "result": [
+ {"data": page, "next_offset": offset + len(page), "total": len(rows)}
+ ]
+ }
+ return result
+
+
+def list_linkage(db: Database, **a: Any) -> Any:
+ imports = [
+ {"addr": hex(int(item.address)), "name": item.name, "module": item.module_name}
+ for item in db.imports.get_all_imports()
+ if item.name
+ ]
+ exports = [
+ {
+ "addr": hex(int(item.address)),
+ "name": item.name,
+ "ordinal": int(item.ordinal),
+ }
+ for item in db.entries.get_all()
+ if item.name
+ ]
+ result = {
+ "imports": imports,
+ "exports": exports,
+ "n_imports": len(imports),
+ "n_exports": len(exports),
+ }
+ return result
+
+
+def list_strings(db: Database, **a: Any) -> Any:
+ from ida_domain.strings import StringListConfig
+
+ offset, count, min_len = (
+ max(0, int(a.get("offset", 0))),
+ max(1, int(a.get("count", 2000))),
+ max(1, int(a.get("min_len", 4))),
+ )
+ if offset == 0 or a.get("refresh"):
+ from ida_domain.strings import StringType
+
+ db.strings.rebuild(
+ StringListConfig(
+ string_types=list(StringType), min_len=min_len, only_ascii_7bit=False
+ )
+ )
+ items = list(db.strings.get_all())
+ page = items[offset : offset + count]
+ rows = []
+ for item in page:
+ try:
+ text = str(item)
+ except Exception:
+ text = item.contents.decode("utf-8", "replace") if item.contents else ""
+ rows.append(
+ {
+ "addr": hex(int(item.address)),
+ "text": text,
+ "len": int(item.length),
+ "type": item.type.name,
+ }
+ )
+ result = {"strings": rows, "total": len(items), "next_offset": offset + len(rows)}
+ return result
+
+
+def lookup_funcs(db: Database, **a: Any) -> Any:
+ rows = []
+ for query in a.get("queries", []):
+ raw = str(query)
+ try:
+ ea = int(raw, 16)
+ except ValueError:
+ fn = db.functions.get_by_name(raw)
+ ea = int(fn.start_ea) if fn else None
+ else:
+ fn = db.functions.get_at(ea)
+ if fn is None:
+ rows.append({"query": raw, "fn": None})
+ else:
+ rows.append(
+ {
+ "query": raw,
+ "fn": {
+ "addr": hex(int(fn.start_ea)),
+ "name": db.functions.get_name(fn)
+ or f"sub_{int(fn.start_ea):X}",
+ "size": int(fn.end_ea) - int(fn.start_ea),
+ },
+ }
+ )
+ result = {"result": rows}
+ return result
+
+
+def make_data(db: Database, **a: Any) -> Any:
+ import ida_bytes, ida_idaapi, ida_typeinf
+ from ida_domain.types import TypeApplyFlags
+
+ rows = []
+ for item in a.get("items", []):
+ ea, declaration = int(str(item["addr"]), 16), str(item["type"])
+ try:
+ tif = db.types.parse_one_declaration(ida_typeinf.get_idati(), declaration)
+ size = max(1, int(tif.get_size()))
+ saved_names = [
+ (addr, name)
+ for addr, name in db.names.get_all()
+ if ea <= int(addr) < ea + size
+ ]
+ ida_bytes.del_items(
+ ea,
+ ida_bytes.DELIT_EXPAND | ida_bytes.DELIT_DELNAMES,
+ max(size, int(ida_bytes.get_item_size(ea) or 1)),
+ )
+ created = bool(
+ ida_bytes.create_data(ea, ida_bytes.FF_BYTE, size, ida_idaapi.BADADDR)
+ )
+ ok = created and bool(db.types.apply_at(tif, ea, TypeApplyFlags.DEFINITE))
+ for address, name in saved_names:
+ db.names.set_name(int(address), name)
+ if ok and item.get("name"):
+ ok = bool(db.names.set_name(ea, str(item["name"])))
+ rows.append(
+ {
+ "addr": hex(ea),
+ "ok": ok,
+ "size": size,
+ **({} if ok else {"error": "IDA rejected the data type"}),
+ }
+ )
+ except Exception as exc:
+ rows.append({"addr": hex(ea), "ok": False, "error": str(exc)})
+ result = {"result": rows}
+ return result
+
+
+def make_string(db: Database, **a: Any) -> Any:
+ from ida_domain.strings import StringType
+
+ ea, length = int(str(a["addr"]), 16), max(0, int(a.get("length", 0)))
+ kind = {
+ "c": StringType.C,
+ "c16": StringType.C_16,
+ "c32": StringType.C_32,
+ "pascal": StringType.PASCAL,
+ }.get(str(a.get("kind", "c")).lower(), StringType.C)
+ import ida_bytes
+
+ try:
+ ida_bytes.del_items(ea, ida_bytes.DELIT_SIMPLE, length if length > 0 else 1)
+ except Exception:
+ pass
+ try:
+ ok = bool(db.bytes.create_string_at(ea, length or None, kind))
+ text = db.bytes.get_string_at(ea) or "" if ok else ""
+ result = {
+ "addr": hex(ea),
+ "ok": ok,
+ "size": int(db.heads.size(ea)) if ok else 0,
+ "text": text,
+ }
+ except Exception as exc:
+ result = {"addr": hex(ea), "ok": False, "error": str(exc)}
+ return result
+
+
+def read_raw(db: Database, **a: Any) -> Any:
+ import ida_bytes
+
+ ea, size = int(str(a["addr"]), 16), max(0, int(a["size"]))
+ raw = ida_bytes.get_bytes(ea, size) or b""
+ raw = raw[:size] + b"\xff" * max(0, size - len(raw))
+ data = bytearray(raw)
+ for index, value in enumerate(data):
+ if value == 0xFF and not ida_bytes.is_loaded(ea + index):
+ data[index] = 0
+ result = {"addr": a["addr"], "hex": bytes(data).hex(), "n": len(data)}
+ return result
+
+
+def rename(db: Database, **a: Any) -> Any:
+ import idaapi, ida_hexrays, ida_name
+
+ batch = a.get("batch") or {}
+ dry_run = bool(batch.get("dry_run", False))
+ allow_overwrite = bool(batch.get("allow_overwrite", False))
+ stop_on_error = bool(batch.get("stop_on_error", False))
+
+ def _items(value):
+ if value is None:
+ return []
+ if isinstance(value, dict):
+ return [value]
+ if isinstance(value, list):
+ return [i for i in value if isinstance(i, dict)]
+ return []
+
+ def _set_name_checked(ea, new):
+ conflict = idaapi.get_name_ea(idaapi.BADADDR, new)
+ if conflict != idaapi.BADADDR and conflict != ea and not allow_overwrite:
+ return (
+ False,
+ f"can't rename at {hex(ea)} as {new!r}: name already used at {hex(conflict)}",
+ )
+ if dry_run:
+ return True, None
+ flags = idaapi.SN_CHECK
+ if allow_overwrite:
+ flags |= int(getattr(idaapi, "SN_FORCE", 0))
+ if not idaapi.set_name(ea, new, flags):
+ return False, (
+ f"Rename failed at {hex(ea)}: IDA rejected name {new!r} "
+ "(invalid identifier or internal conflict)"
+ )
+ return True, None
+
+ def _refresh_ctext(fn_addr):
+ # A renamed function must invalidate Hex-Rays' cache, which is per function
+ # and persisted in the .i64: without this the pseudocode keeps calling the
+ # old name forever while every other readback reports the new one.
+ if not ida_hexrays.init_hexrays_plugin():
+ return
+ failure = ida_hexrays.hexrays_failure_t()
+ cfunc = ida_hexrays.decompile_func(
+ fn_addr, failure, ida_hexrays.DECOMP_WARNINGS
+ )
+ if cfunc:
+ cfunc.refresh_func_ctext()
+
+ out = {}
+ ok_count = failed = 0
+ halted = False
+ for category in ("func", "data", "local", "stack"):
+ if category not in batch:
+ continue
+ rows = []
+ for edit in _items(batch.get(category)):
+ try:
+ if category == "func":
+ addr_text = (
+ edit.get("addr") or edit.get("func_addr") or edit.get("func")
+ )
+ new = edit.get("name") or edit.get("new") or edit.get("new_name")
+ if not addr_text or not new:
+ row = {
+ "addr": addr_text,
+ "name": new,
+ "error": "Function rename requires addr + name",
+ }
+ else:
+ ea = int(str(addr_text), 16)
+ fn = idaapi.get_func(ea)
+ if fn is None:
+ row = {
+ "addr": addr_text,
+ "name": new,
+ "error": "Function not found",
+ }
+ else:
+ old = idaapi.get_name(fn.start_ea) or None
+ ok, err = _set_name_checked(fn.start_ea, str(new))
+ row = {"addr": addr_text, "old": old, "name": str(new)}
+ if err:
+ row["error"] = err
+ if dry_run:
+ row["dry_run"] = True
+ if ok and not dry_run:
+ _refresh_ctext(fn.start_ea)
+ elif category == "data":
+ addr_text = edit.get("addr")
+ old = edit.get("old") or edit.get("old_name")
+ new = edit.get("new") or edit.get("new_name") or edit.get("name")
+ if not new and new != "":
+ row = {
+ "old": old,
+ "new": None,
+ "error": "Global rename requires target and new name",
+ }
+ else:
+ if addr_text is not None:
+ ea = int(str(addr_text), 16)
+ old = old or (idaapi.get_name(ea) or None)
+ else:
+ ea = idaapi.get_name_ea(idaapi.BADADDR, str(old or ""))
+ if ea == idaapi.BADADDR:
+ row = {
+ "old": old,
+ "new": str(new),
+ "error": f"Global {old!r} not found",
+ }
+ else:
+ # An empty new name CLEARS the label; that is a real
+ # request (tests revert with it), not a missing argument.
+ if str(new) == "":
+ ok = bool(ida_name.set_name(ea, "", idaapi.SN_CHECK))
+ err = (
+ None
+ if ok
+ else f"Failed to clear the name at {hex(ea)}"
+ )
+ else:
+ ok, err = _set_name_checked(ea, str(new))
+ row = {"addr": hex(ea), "old": old, "new": str(new)}
+ if err:
+ row["error"] = err
+ if dry_run:
+ row["dry_run"] = True
+ else:
+ fa, old, new = (
+ edit.get("func_addr"),
+ edit.get("old"),
+ edit.get("new"),
+ )
+ if not fa or not old or not new:
+ row = {
+ "old": old,
+ "new": new,
+ "error": f"{category} rename requires func_addr + old + new",
+ }
+ else:
+ ea = int(str(fa), 16)
+ pseudo = db.pseudocode.decompile(ea)
+ var = pseudo.find_local_variable(str(old))
+ if var is None:
+ row = {
+ "func_addr": fa,
+ "old": old,
+ "new": new,
+ "error": f"no local {old!r} in that function",
+ }
+ elif dry_run:
+ row = {
+ "func_addr": fa,
+ "old": old,
+ "new": new,
+ "dry_run": True,
+ }
+ else:
+ var.set_user_name(str(new))
+ ok = bool(
+ pseudo.save_local_variable_info(var, save_name=True)
+ )
+ row = {"func_addr": fa, "old": old, "new": new}
+ if not ok:
+ row["error"] = "IDA rejected the local variable name"
+ except Exception as exc:
+ row = {"addr": edit.get("addr"), "error": str(exc)}
+ rows.append(row)
+ if row.get("error"):
+ failed += 1
+ else:
+ ok_count += 1
+ if row.get("error") and stop_on_error:
+ halted = True
+ break
+ out[category] = rows
+ if halted:
+ break
+ out["summary"] = {"ok": ok_count, "failed": failed}
+ if dry_run:
+ out["summary"]["dry_run"] = True
+ if halted:
+ out["summary"]["halted"] = True
+ result = out
+ return result
+
+
+def resolve_names(db: Database, **a: Any) -> Any:
+ import ida_idaapi, ida_name
+
+ rows = []
+ for query in a.get("queries", []):
+ name = str(query).strip()
+ ea = ida_name.get_name_ea(ida_idaapi.BADADDR, name)
+ rows.append(
+ {"query": name, "ea": hex(int(ea)) if ea != ida_idaapi.BADADDR else None}
+ )
+ result = {"result": rows}
+ return result
+
+
+def search_bytes(db: Database, **a: Any) -> Any:
+ import ida_bytes, ida_funcs, ida_idaapi, ida_lines, ida_segment
+
+ pat = str(a.get("pattern", "")).strip()
+ limit = max(1, int(a.get("limit", 500)))
+ lo = int(a.get("start", 0))
+ hi = int(a.get("end", 0)) or ida_idaapi.BADADDR
+ flags = ida_bytes.BIN_SEARCH_FORWARD | ida_bytes.BIN_SEARCH_NOSHOW
+ if a.get("case"):
+ flags |= ida_bytes.BIN_SEARCH_CASE
+ rows, err, ea = [], None, lo
+ while len(rows) < limit:
+ try:
+ hit = ida_bytes.find_bytes(pat, range_start=ea, range_end=hi, flags=flags)
+ except Exception as exc:
+ err = str(exc) or exc.__class__.__name__
+ break
+ if hit is None or hit == ida_idaapi.BADADDR:
+ break
+ head = ida_bytes.get_item_head(hit)
+ fn = ida_funcs.get_func(hit)
+ seg = ida_segment.getseg(hit)
+ try:
+ line = (
+ ida_lines.generate_disasm_line(head, ida_lines.GENDSM_REMOVE_TAGS) or ""
+ )
+ except Exception:
+ line = ""
+ rows.append(
+ {
+ "addr": hex(int(hit)),
+ "head": hex(int(head)),
+ "line": " ".join(line.split()),
+ "func": (ida_funcs.get_func_name(fn.start_ea) if fn else None),
+ "func_addr": (hex(int(fn.start_ea)) if fn else None),
+ "seg": (ida_segment.get_segm_name(seg) if seg else ""),
+ }
+ )
+ ea = int(hit) + 1
+ result = {"hits": rows, "error": err, "truncated": len(rows) >= limit}
+ return result
+
+
+def search_structs(db: Database, **a: Any) -> Any:
+ needle = str(a.get("filter") or "").lower()
+ rows = []
+ for tif in db.types.get_all():
+ name = tif.get_type_name() or ""
+ if not name or needle not in name.lower() or not tif.is_udt():
+ continue
+ members = list(db.types.get_udt_members(tif))
+ rows.append(
+ {
+ "name": name,
+ "size": int(tif.get_size()),
+ "is_union": bool(tif.is_union()),
+ "cardinality": len(members),
+ "ordinal": int(tif.get_ordinal()),
+ }
+ )
+ result = {"result": rows}
+ return result
+
+
+def search_text(db: Database, **a: Any) -> Any:
+ import ida_lines, ida_funcs, ida_segment, idautils
+ import re as _re
+
+ q = str(a.get("query", ""))
+ limit = max(1, int(a.get("limit", 500)))
+ max_scan = max(1000, int(a.get("max_scan", 3000000)))
+ ci = (not a.get("case")) and q.islower() # smartcase, like the in-view search
+ rx, err = None, None
+ if a.get("regex"):
+ try:
+ rx = _re.compile(q, _re.I if ci else 0)
+ except Exception as exc:
+ err = "bad regex: " + str(exc)
+ needle = q.lower() if ci else q
+ rows, scanned = [], 0
+ if err is None and q:
+ for i in range(ida_segment.get_segm_qty()):
+ seg = ida_segment.getnseg(i)
+ if seg is None or len(rows) >= limit or scanned >= max_scan:
+ continue
+ for ea in idautils.Heads(seg.start_ea, seg.end_ea):
+ scanned += 1
+ if len(rows) >= limit or scanned >= max_scan:
+ break
+ try:
+ line = (
+ ida_lines.generate_disasm_line(ea, ida_lines.GENDSM_REMOVE_TAGS)
+ or ""
+ )
+ except Exception:
+ continue
+ # Match what the user SEES, not IDA's column padding: nobody types
+ # "call" + four spaces + "cs:getenv_ptr".
+ line = " ".join(line.split())
+ hay = line.lower() if ci else line
+ if rx.search(line) if rx is not None else (needle in hay):
+ fn = ida_funcs.get_func(ea)
+ rows.append(
+ {
+ "addr": hex(int(ea)),
+ "head": hex(int(ea)),
+ "line": line,
+ "func": (
+ ida_funcs.get_func_name(fn.start_ea) if fn else None
+ ),
+ "func_addr": (hex(int(fn.start_ea)) if fn else None),
+ "seg": ida_segment.get_segm_name(seg),
+ }
+ )
+ result = {
+ "hits": rows,
+ "error": err,
+ "scanned": scanned,
+ "truncated": len(rows) >= limit or scanned >= max_scan,
+ }
+ return result
+
+
+def set_comments(db: Database, **a: Any) -> Any:
+ import idaapi, idc, ida_hexrays
+
+ rows = []
+ for item in a.get("items", []):
+ addr_s = str(item.get("addr", ""))
+ text = str(item.get("comment") or "")
+ try:
+ ea = int(addr_s, 16)
+ if not idaapi.set_cmt(ea, text, False):
+ rows.append(
+ {
+ "addr": addr_s,
+ "error": f"Failed to set disassembly comment at {hex(ea)}",
+ }
+ )
+ continue
+ if not ida_hexrays.init_hexrays_plugin():
+ rows.append({"addr": addr_s})
+ continue
+ try:
+ cfunc = ida_hexrays.decompile(ea)
+ except Exception:
+ cfunc = None
+ if cfunc is None:
+ rows.append({"addr": addr_s})
+ continue
+ if ea == cfunc.entry_ea:
+ # The signature line carries no ctree item: it is a function comment.
+ idc.set_func_cmt(ea, text, True)
+ cfunc.refresh_func_ctext()
+ rows.append({"addr": addr_s})
+ continue
+ eamap = cfunc.get_eamap()
+ if ea not in eamap:
+ rows.append(
+ {
+ "addr": addr_s,
+ "error": f"Failed to set decompiler comment at {hex(ea)}",
+ }
+ )
+ continue
+ nearest_ea = eamap[ea][0].ea
+ if cfunc.has_orphan_cmts():
+ cfunc.del_orphan_cmts()
+ cfunc.save_user_cmts()
+ tl = idaapi.treeloc_t()
+ tl.ea = nearest_ea
+ placed = False
+ for itp in range(idaapi.ITP_SEMI, idaapi.ITP_COLON):
+ tl.itp = itp
+ cfunc.set_user_cmt(tl, text)
+ cfunc.save_user_cmts()
+ cfunc.refresh_func_ctext()
+ if not cfunc.has_orphan_cmts():
+ placed = True
+ break
+ cfunc.del_orphan_cmts()
+ cfunc.save_user_cmts()
+ rows.append(
+ {"addr": addr_s}
+ if placed
+ else {
+ "addr": addr_s,
+ "error": f"Failed to set decompiler comment at {hex(ea)}",
+ }
+ )
+ except Exception as exc:
+ rows.append({"addr": addr_s, "error": str(exc)})
+ result = {"result": rows}
+ return result
+
+
+def set_lvar_type(db: Database, **a: Any) -> Any:
+ import ida_typeinf
+
+ ea, variable, declaration = (
+ int(str(a["addr"]), 16),
+ str(a["variable"]),
+ str(a["type"]),
+ )
+ fn = db.functions.get_at(ea)
+ if fn is None:
+ result = {"error": "no function at address"}
+ else:
+ pseudo = db.pseudocode.decompile(fn)
+ var = pseudo.find_local_variable(variable)
+ if var is None:
+ result = {"error": f"local variable {variable!r} not found"}
+ else:
+ try:
+ tif = db.types.parse_one_declaration(
+ ida_typeinf.get_idati(), declaration
+ )
+ accepted = bool(var.set_type(tif))
+ saved = (
+ bool(pseudo.save_local_variable_info(var, save_type=True))
+ if accepted
+ else False
+ )
+ result = {
+ "addr": hex(int(fn.start_ea)),
+ "variable": variable,
+ "type": declaration,
+ "ok": accepted and saved,
+ }
+ except Exception as exc:
+ result = {"error": f"bad type {declaration!r}: {exc}"}
+ return result
+
+
+def set_thumb(db: Database, **a: Any) -> Any:
+ import ida_bytes, ida_ida, ida_idp, ida_segment, ida_segregs
+
+ ea = int(str(a["addr"]), 16)
+ treg = ida_idp.str2reg("T")
+ seg = ida_segment.getseg(ea)
+ if treg is None or treg < 0:
+ result = {"addr": hex(ea), "error": "no T register (not an ARM database)"}
+ elif seg is None:
+ result = {"addr": hex(ea), "error": "no segment"}
+ else:
+ current = ida_segregs.get_sreg(ea, treg)
+ current = 0 if current in (None, 0xFFFFFFFF, -1) else int(current)
+ want = {"on": 1, "off": 0}.get(
+ str(a.get("mode", "toggle")).lower(), 0 if current else 1
+ )
+ changed = False
+ if want and seg.bitness != 1:
+ ida_segment.set_segm_addressing(seg, 1)
+ changed = True
+ size = max(int(ida_bytes.get_item_size(ea)), 2)
+ ida_bytes.del_items(ea, 0, size)
+ ok = bool(ida_segregs.split_sreg_range(ea, treg, want, ida_segregs.SR_user))
+ now = ida_segregs.get_sreg(ea, treg)
+ result = {
+ "addr": hex(ea),
+ "thumb": bool(now),
+ "was": bool(current),
+ "ok": ok,
+ "bitness": ida_segment.getseg(ea).bitness,
+ "forced_32bit": changed,
+ "db_64bit": bool(ida_ida.inf_get_app_bitness() == 64 and want),
+ }
+ return result
+
+
+def set_type(db: Database, **a: Any) -> Any:
+ from ida_domain.types import TypeApplyFlags
+
+ rows = []
+ for edit in a.get("edits", []):
+ ea = int(str(edit["addr"]), 16)
+ declaration = str(edit.get("signature") or edit.get("type") or "")
+ try:
+ ok = bool(
+ db.types.apply_declaration_at(ea, declaration, TypeApplyFlags.DEFINITE)
+ )
+ rows.append(
+ {
+ "addr": hex(ea),
+ "ok": ok,
+ **({} if ok else {"error": "IDA rejected the type"}),
+ }
+ )
+ except Exception as exc:
+ rows.append({"addr": hex(ea), "ok": False, "error": str(exc)})
+ result = {"result": rows}
+ return result
+
+
+def survey_binary(db: Database, **a: Any) -> Any:
+ segments = []
+ for seg in db.segments.get_all():
+ segments.append(
+ {
+ "start": hex(int(seg.start_ea)),
+ "end": hex(int(seg.end_ea)),
+ "name": db.segments.get_name(seg) or "",
+ }
+ )
+ result = {"segments": segments}
+ return result
+
+
+def thumb_scan(db: Database, **a: Any) -> Any:
+ import ida_bytes, ida_funcs, ida_idp, ida_segment, ida_segregs, ida_ua
+
+ lo, hi = int(str(a["start"]), 16), int(str(a["end"]), 16)
+ apply, limit = bool(a.get("apply", True)), int(a.get("limit", 512))
+ treg = ida_idp.str2reg("T")
+ found = []
+ applied = 0
+ cursor = lo
+ while cursor + 4 <= hi and len(found) < limit:
+ at = cursor
+ value = int(ida_bytes.get_dword(cursor))
+ cursor += 4
+ if not value & 1:
+ continue
+ target = value & ~1
+ seg = ida_segment.getseg(target)
+ if seg is None or not (seg.perm & ida_segment.SEGPERM_EXEC or seg.perm == 0):
+ continue
+ flags = ida_bytes.get_flags(target)
+ if ida_bytes.is_data(flags):
+ continue
+ item = {
+ "at": hex(at),
+ "value": hex(value),
+ "target": hex(target),
+ "was_code": bool(ida_bytes.is_code(flags)),
+ }
+ found.append(item)
+ if not apply:
+ continue
+ if treg is not None and treg >= 0:
+ ida_segregs.split_sreg_range(target, treg, 1, ida_segregs.SR_user)
+ if not ida_bytes.is_code(ida_bytes.get_flags(target)):
+ ida_bytes.del_items(target, 0, 2)
+ if ida_ua.create_insn(target) <= 0:
+ item["decoded"] = False
+ continue
+ item["decoded"] = True
+ item["function"] = bool(
+ db.functions.get_at(target) or db.functions.create(target)
+ )
+ applied += 1
+ result = {
+ "start": hex(lo),
+ "end": hex(hi),
+ "found": found,
+ "applied": applied,
+ "n": len(found),
+ }
+ return result
+
+
+def type_inspect(db: Database, **a: Any) -> Any:
+ rows = []
+ for query in a.get("queries", []):
+ name = str(query.get("name") or "")
+ tif = db.types.get_by_name(name)
+ if tif is None:
+ rows.append({"name": name, "error": "type not found"})
+ continue
+ members = (
+ [
+ {
+ "name": m.name,
+ "type": m.type.dstr() or str(m.type),
+ "offset": int(m.offset),
+ "size": int(m.size),
+ }
+ for m in db.types.get_udt_members(tif)
+ ]
+ if tif.is_udt()
+ else []
+ )
+ rows.append(
+ {
+ "name": name,
+ "size": int(tif.get_size()),
+ "is_union": bool(tif.is_union()),
+ "members": members,
+ }
+ )
+ result = {"result": rows}
+ return result
+
+
+def undefine(db: Database, **a: Any) -> Any:
+ import ida_bytes
+
+ rows = []
+ for item in a.get("items", []):
+ ea = int(str(item["addr"]), 16)
+ size = max(1, int(item.get("size") or ida_bytes.get_item_size(ea) or 1))
+ ok = bool(ida_bytes.del_items(ea, ida_bytes.DELIT_SIMPLE, size))
+ rows.append(
+ {
+ "addr": hex(ea),
+ "ok": ok,
+ **({} if ok else {"error": "delete items failed"}),
+ }
+ )
+ result = {"result": rows}
+ return result
+
+
+def xref_query(db: Database, **a: Any) -> Any:
+ import idaapi, idautils, ida_bytes, ida_funcs
+
+ def _fn(ea):
+ f = ida_funcs.get_func(ea)
+ return (
+ {
+ "addr": hex(int(f.start_ea)),
+ "name": ida_funcs.get_func_name(f.start_ea) or "",
+ }
+ if f
+ else None
+ )
+
+ queries = a.get("queries") or []
+ all_results = []
+ for query in queries:
+ raw = str(query.get("addr", "")).strip()
+ direction = str(query.get("direction", "both") or "both").lower()
+ if direction not in ("to", "from", "both"):
+ direction = "both"
+ xref_type = str(query.get("xref_type", "any") or "any").lower()
+ if xref_type not in ("any", "code", "data"):
+ xref_type = "any"
+ include_fn = bool(query.get("include_fn", True))
+ dedup = bool(query.get("dedup", True))
+ sort_by = str(query.get("sort_by", "addr") or "addr")
+ descending = bool(query.get("descending", False))
+ try:
+ offset = max(0, int(query.get("offset", 0) or 0))
+ except (TypeError, ValueError):
+ offset = 0
+ try:
+ count = max(0, min(int(query.get("count", 200) or 200), 5000))
+ except (TypeError, ValueError):
+ count = 200
+ try:
+ try:
+ target = int(raw, 16)
+ except ValueError:
+ target = idaapi.get_name_ea(idaapi.BADADDR, raw)
+ if target == idaapi.BADADDR:
+ raise ValueError(f"Failed to resolve address/name: {raw}")
+ if not ida_bytes.is_mapped(target):
+ raise ValueError(f"Address not mapped: {raw}")
+ rows = []
+ if direction in ("to", "both"):
+ for xr in idautils.XrefsTo(target, 0):
+ kind = "code" if xr.iscode else "data"
+ if xref_type != "any" and kind != xref_type:
+ continue
+ row = {
+ "direction": "to",
+ "addr": hex(int(xr.frm)),
+ "from": hex(int(xr.frm)),
+ "to": hex(int(target)),
+ "type": kind,
+ }
+ if include_fn:
+ row["fn"] = _fn(xr.frm)
+ rows.append(row)
+ if direction in ("from", "both"):
+ for xr in idautils.XrefsFrom(target, 0):
+ kind = "code" if xr.iscode else "data"
+ if xref_type != "any" and kind != xref_type:
+ continue
+ row = {
+ "direction": "from",
+ "addr": hex(int(xr.to)),
+ "from": hex(int(target)),
+ "to": hex(int(xr.to)),
+ "type": kind,
+ }
+ if include_fn:
+ row["fn"] = _fn(xr.to)
+ rows.append(row)
+ if dedup:
+ seen, deduped = set(), []
+ for row in rows:
+ key = (row["direction"], row["from"], row["to"], row["type"])
+ if key in seen:
+ continue
+ seen.add(key)
+ deduped.append(row)
+ rows = deduped
+ if sort_by == "type":
+ rows.sort(
+ key=lambda r: (str(r.get("type", "")), int(str(r["addr"]), 16)),
+ reverse=descending,
+ )
+ else:
+ rows.sort(key=lambda r: int(str(r["addr"]), 16), reverse=descending)
+ page = rows[offset : offset + count] if count else rows[offset:]
+ nxt = offset + len(page)
+ all_results.append(
+ {
+ "target": raw,
+ "resolved_addr": hex(int(target)),
+ "direction": direction,
+ "xref_type": xref_type,
+ "data": page,
+ "next_offset": nxt if nxt < len(rows) else None,
+ "total": len(rows),
+ "error": None,
+ }
+ )
+ except Exception as exc:
+ all_results.append(
+ {
+ "target": raw,
+ "resolved_addr": None,
+ "direction": direction,
+ "xref_type": xref_type,
+ "data": [],
+ "next_offset": None,
+ "total": 0,
+ "error": str(exc),
+ }
+ )
+ result = {"result": all_results}
+ return result
+
+
+def xref_types(db: Database, **a: Any) -> Any:
+ import idaapi, idautils, ida_bytes, ida_funcs, ida_xref
+
+ code_kind = {
+ ida_xref.fl_CF: "call",
+ ida_xref.fl_CN: "call",
+ ida_xref.fl_JF: "jump",
+ ida_xref.fl_JN: "jump",
+ ida_xref.fl_F: "flow",
+ }
+ data_kind = {
+ ida_xref.dr_O: "offset",
+ ida_xref.dr_W: "write",
+ ida_xref.dr_R: "read",
+ ida_xref.dr_T: "text",
+ ida_xref.dr_I: "info",
+ }
+
+ def _kind(xr):
+ return (code_kind if xr.iscode else data_kind).get(
+ xr.type, "code" if xr.iscode else "data"
+ )
+
+ def _fn(ea):
+ f = ida_funcs.get_func(ea)
+ return (
+ {
+ "addr": hex(int(f.start_ea)),
+ "name": ida_funcs.get_func_name(f.start_ea) or "",
+ }
+ if f
+ else None
+ )
+
+ queries = a.get("queries") or []
+ all_results = []
+ for query in queries:
+ query = query if isinstance(query, dict) else {"addr": query}
+ raw = str(query.get("addr", "")).strip()
+ direction = str(query.get("direction", "to") or "to").lower()
+ include_fn = bool(query.get("include_fn", True))
+ dedup = bool(query.get("dedup", True))
+ try:
+ count = int(query.get("count", 2000) or 2000)
+ except (TypeError, ValueError):
+ count = 2000
+ try:
+ target = int(raw, 16)
+ except ValueError:
+ target = idaapi.get_name_ea(idaapi.BADADDR, raw)
+ rows = []
+ if (
+ target is not None
+ and target != idaapi.BADADDR
+ and ida_bytes.is_mapped(target)
+ ):
+ if direction in ("to", "both"):
+ for xr in idautils.XrefsTo(target, 0):
+ row = {
+ "direction": "to",
+ "addr": hex(int(xr.frm)),
+ "from": hex(int(xr.frm)),
+ "to": hex(int(target)),
+ "type": "code" if xr.iscode else "data",
+ "kind": _kind(xr),
+ }
+ if include_fn:
+ row["fn"] = _fn(xr.frm)
+ rows.append(row)
+ if direction in ("from", "both"):
+ for xr in idautils.XrefsFrom(target, 0):
+ row = {
+ "direction": "from",
+ "addr": hex(int(xr.to)),
+ "from": hex(int(target)),
+ "to": hex(int(xr.to)),
+ "type": "code" if xr.iscode else "data",
+ "kind": _kind(xr),
+ }
+ if include_fn:
+ row["fn"] = _fn(xr.to)
+ rows.append(row)
+ if dedup:
+ seen, deduped = set(), []
+ for r in rows:
+ k = (r["direction"], r["from"], r["to"], r["kind"])
+ if k in seen:
+ continue
+ seen.add(k)
+ deduped.append(r)
+ rows = deduped
+ rows = rows[:count]
+ all_results.append({"query": raw, "data": rows, "next_offset": None})
+ result = {"result": all_results}
+ return result
+
+
+def op_format(addr: str, mode: str = "cycle", col: int = -1, n: int = -1) -> dict: ...
+
+
+def pc_nums(addr: str) -> dict: ...
+
+
+def decompile(addr, include_addresses=True) -> dict: ...
+
+
+def decomp_map(addr: str) -> dict: ...
+
+
+def pc_num_format(
+ addr: str,
+ mode: str = "cycle",
+ line: int = -1,
+ col: int = -1,
+ ea: str = "",
+ opnum: int = -1,
+) -> dict: ...
+
+
+def segment_index(
+ addr: str,
+ end: str = "",
+ page_rows: int = 500,
+ detail: bool = False,
+) -> dict: ...
+
+
+def heads(
+ addr: str,
+ count: int = 200,
+ offset: int = 0,
+ end: str = "",
+ back: bool = False,
+ annotate: bool = False,
+ expect: str = "",
+ text: bool = True,
+) -> dict: ...
+
+
+def profile_remote(operation: str, args: dict[str, Any], reps: int = 5) -> dict: ...
+
+
+OPERATIONS: dict[str, Callable[..., Any]] = {
+ "data_type": data_type,
+ "declare_type": declare_type,
+ "decomp_error": decomp_error,
+ "define_code": define_code,
+ "define_code_run": define_code_run,
+ "define_func": define_func,
+ "define_func_run": define_func_run,
+ "del_type": del_type,
+ "disasm": disasm,
+ "file_regions": file_regions,
+ "flowchart": flowchart,
+ "force_recompile": force_recompile,
+ "func_types": func_types,
+ "get_bytes": get_bytes,
+ "heads": heads,
+ "journal_get": journal_get,
+ "journal_put": journal_put,
+ "list_annotations": list_annotations,
+ "list_funcs": list_funcs,
+ "list_linkage": list_linkage,
+ "list_strings": list_strings,
+ "lookup_funcs": lookup_funcs,
+ "make_data": make_data,
+ "make_string": make_string,
+ "op_format": op_format,
+ "pc_nums": pc_nums,
+ "decompile": decompile,
+ "decomp_map": decomp_map,
+ "pc_num_format": pc_num_format,
+ "profile_remote": profile_remote,
+ "read_raw": read_raw,
+ "rename": rename,
+ "resolve_names": resolve_names,
+ "search_bytes": search_bytes,
+ "search_structs": search_structs,
+ "search_text": search_text,
+ "segment_index": segment_index,
+ "set_comments": set_comments,
+ "set_lvar_type": set_lvar_type,
+ "set_thumb": set_thumb,
+ "set_type": set_type,
+ "survey_binary": survey_binary,
+ "thumb_scan": thumb_scan,
+ "type_inspect": type_inspect,
+ "undefine": undefine,
+ "xref_query": xref_query,
+ "xref_types": xref_types,
+}
+
+_MODULE_DECLARATIONS = frozenset(
+ (
+ heads,
+ segment_index,
+ op_format,
+ pc_nums,
+ decompile,
+ decomp_map,
+ pc_num_format,
+ profile_remote,
+ )
+)
+_BOUND: dict[Callable[..., Any], Any] | None = None
+_BIND_LOCK = threading.Lock()
+
+
+def _bindings() -> dict[Callable[..., Any], Any]:
+ global _BOUND
+ with _BIND_LOCK:
+ if _BOUND is not None:
+ return _BOUND
+ from ida_nexus import RemoteModule
+
+ operations_module = RemoteModule(
+ Path(__file__), operation_label=operation_label, codec="json"
+ )
+ tools_module = RemoteModule(
+ Path(__file__).with_name("remote_tools.py"),
+ operation_label=operation_label,
+ codec="json",
+ )
+ bound: dict[Callable[..., Any], Any] = {}
+ for declaration in OPERATIONS.values():
+ if declaration in _MODULE_DECLARATIONS:
+ bound[declaration] = tools_module.function(
+ declaration,
+ timeout=15.0 if declaration is decompile else None,
+ )
+ else:
+ bound[declaration] = operations_module.function(declaration)
+ _BOUND = bound
+ return bound
+
+
+def bind(function: Callable[..., Any]) -> Any:
+ """Return the lazily constructed remote callable for one declaration."""
+ return _bindings()[function]