ida-pro-mcp-xjoker 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. ida_pro_mcp/__init__.py +0 -0
  2. ida_pro_mcp/__main__.py +6 -0
  3. ida_pro_mcp/ida_mcp/__init__.py +68 -0
  4. ida_pro_mcp/ida_mcp/api_analysis.py +1296 -0
  5. ida_pro_mcp/ida_mcp/api_core.py +337 -0
  6. ida_pro_mcp/ida_mcp/api_debug.py +617 -0
  7. ida_pro_mcp/ida_mcp/api_memory.py +304 -0
  8. ida_pro_mcp/ida_mcp/api_modify.py +406 -0
  9. ida_pro_mcp/ida_mcp/api_python.py +179 -0
  10. ida_pro_mcp/ida_mcp/api_resources.py +295 -0
  11. ida_pro_mcp/ida_mcp/api_stack.py +167 -0
  12. ida_pro_mcp/ida_mcp/api_types.py +480 -0
  13. ida_pro_mcp/ida_mcp/auth.py +166 -0
  14. ida_pro_mcp/ida_mcp/cache.py +232 -0
  15. ida_pro_mcp/ida_mcp/config.py +228 -0
  16. ida_pro_mcp/ida_mcp/framework.py +547 -0
  17. ida_pro_mcp/ida_mcp/http.py +859 -0
  18. ida_pro_mcp/ida_mcp/port_utils.py +104 -0
  19. ida_pro_mcp/ida_mcp/rpc.py +187 -0
  20. ida_pro_mcp/ida_mcp/server_manager.py +339 -0
  21. ida_pro_mcp/ida_mcp/sync.py +233 -0
  22. ida_pro_mcp/ida_mcp/tests/__init__.py +14 -0
  23. ida_pro_mcp/ida_mcp/tests/test_api_analysis.py +336 -0
  24. ida_pro_mcp/ida_mcp/tests/test_api_core.py +237 -0
  25. ida_pro_mcp/ida_mcp/tests/test_api_memory.py +207 -0
  26. ida_pro_mcp/ida_mcp/tests/test_api_modify.py +123 -0
  27. ida_pro_mcp/ida_mcp/tests/test_api_resources.py +199 -0
  28. ida_pro_mcp/ida_mcp/tests/test_api_stack.py +77 -0
  29. ida_pro_mcp/ida_mcp/tests/test_api_types.py +249 -0
  30. ida_pro_mcp/ida_mcp/ui.py +357 -0
  31. ida_pro_mcp/ida_mcp/utils.py +1186 -0
  32. ida_pro_mcp/ida_mcp/zeromcp/__init__.py +5 -0
  33. ida_pro_mcp/ida_mcp/zeromcp/jsonrpc.py +384 -0
  34. ida_pro_mcp/ida_mcp/zeromcp/mcp.py +883 -0
  35. ida_pro_mcp/ida_mcp.py +186 -0
  36. ida_pro_mcp/idalib_server.py +354 -0
  37. ida_pro_mcp/idalib_session_manager.py +259 -0
  38. ida_pro_mcp/server.py +1060 -0
  39. ida_pro_mcp/test.py +170 -0
  40. ida_pro_mcp_xjoker-1.0.1.dist-info/METADATA +405 -0
  41. ida_pro_mcp_xjoker-1.0.1.dist-info/RECORD +45 -0
  42. ida_pro_mcp_xjoker-1.0.1.dist-info/WHEEL +5 -0
  43. ida_pro_mcp_xjoker-1.0.1.dist-info/entry_points.txt +4 -0
  44. ida_pro_mcp_xjoker-1.0.1.dist-info/licenses/LICENSE +21 -0
  45. ida_pro_mcp_xjoker-1.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,304 @@
1
+ """Memory reading and writing operations for IDA Pro MCP.
2
+
3
+ This module provides batch operations for reading and writing memory at various
4
+ granularities (bytes, integers, strings) and patching binary data.
5
+ """
6
+
7
+ import re
8
+
9
+ from typing import Annotated
10
+ import ida_bytes
11
+ import idaapi
12
+
13
+ from .rpc import tool
14
+ from .sync import idasync
15
+ from .utils import (
16
+ IntRead,
17
+ IntWrite,
18
+ MemoryPatch,
19
+ MemoryRead,
20
+ normalize_list_input,
21
+ parse_address,
22
+ )
23
+
24
+
25
+ # ============================================================================
26
+ # Memory Reading Operations
27
+ # ============================================================================
28
+
29
+
30
+ @tool
31
+ @idasync
32
+ def get_bytes(regions: list[MemoryRead] | MemoryRead) -> list[dict]:
33
+ """Read bytes from memory addresses"""
34
+ if isinstance(regions, dict):
35
+ regions = [regions]
36
+
37
+ results = []
38
+ for item in regions:
39
+ addr = item.get("addr", "")
40
+ size = item.get("size", 0)
41
+
42
+ try:
43
+ ea = parse_address(addr)
44
+ data = " ".join(f"{x:#02x}" for x in ida_bytes.get_bytes(ea, size))
45
+ results.append({"addr": addr, "data": data})
46
+ except Exception as e:
47
+ results.append({"addr": addr, "data": None, "error": str(e)})
48
+
49
+ return results
50
+
51
+
52
+ _INT_CLASS_RE = re.compile(r"^(?P<sign>[iu])(?P<bits>8|16|32|64)(?P<endian>le|be)?$")
53
+
54
+
55
+ def _parse_int_class(text: str) -> tuple[int, bool, str, str]:
56
+ if not text:
57
+ raise ValueError("Missing integer class")
58
+
59
+ cleaned = text.strip().lower()
60
+ match = _INT_CLASS_RE.match(cleaned)
61
+ if not match:
62
+ raise ValueError(f"Invalid integer class: {text}")
63
+
64
+ bits = int(match.group("bits"))
65
+ signed = match.group("sign") == "i"
66
+ endian = match.group("endian") or "le"
67
+ byte_order = "little" if endian == "le" else "big"
68
+ normalized = f"{'i' if signed else 'u'}{bits}{endian}"
69
+ return bits, signed, byte_order, normalized
70
+
71
+
72
+ def _parse_int_value(text: str, signed: bool, bits: int) -> int:
73
+ if text is None:
74
+ raise ValueError("Missing integer value")
75
+
76
+ value_text = str(text).strip()
77
+ try:
78
+ value = int(value_text, 0)
79
+ except ValueError:
80
+ raise ValueError(f"Invalid integer value: {text}")
81
+
82
+ if not signed and value < 0:
83
+ raise ValueError(f"Negative value not allowed for u{bits}")
84
+
85
+ return value
86
+
87
+
88
+ @tool
89
+ @idasync
90
+ def get_int(
91
+ queries: Annotated[
92
+ list[IntRead] | IntRead,
93
+ "Integer read requests (ty, addr). ty: i8/u64/i16le/i16be/etc",
94
+ ],
95
+ ) -> list[dict]:
96
+ """Read integer values from memory addresses"""
97
+ if isinstance(queries, dict):
98
+ queries = [queries]
99
+
100
+ results = []
101
+ for item in queries:
102
+ addr = item.get("addr", "")
103
+ ty = item.get("ty", "")
104
+
105
+ try:
106
+ bits, signed, byte_order, normalized = _parse_int_class(ty)
107
+ ea = parse_address(addr)
108
+ size = bits // 8
109
+ data = ida_bytes.get_bytes(ea, size)
110
+ if not data or len(data) != size:
111
+ raise ValueError(f"Failed to read {size} bytes at {addr}")
112
+
113
+ value = int.from_bytes(data, byte_order, signed=signed)
114
+ results.append(
115
+ {"addr": addr, "ty": normalized, "value": value, "error": None}
116
+ )
117
+ except Exception as e:
118
+ results.append({"addr": addr, "ty": ty, "value": None, "error": str(e)})
119
+
120
+ return results
121
+
122
+
123
+ @tool
124
+ @idasync
125
+ def get_string(
126
+ addrs: Annotated[list[str] | str, "Addresses to read strings from"],
127
+ ) -> list[dict]:
128
+ """Read strings from memory addresses"""
129
+ addrs = normalize_list_input(addrs)
130
+ results = []
131
+
132
+ for addr in addrs:
133
+ try:
134
+ ea = parse_address(addr)
135
+ raw = idaapi.get_strlit_contents(ea, -1, 0)
136
+ if not raw:
137
+ results.append(
138
+ {"addr": addr, "value": None, "error": "No string at address"}
139
+ )
140
+ continue
141
+ value = raw.decode("utf-8", errors="replace")
142
+ results.append({"addr": addr, "value": value})
143
+ except Exception as e:
144
+ results.append({"addr": addr, "value": None, "error": str(e)})
145
+
146
+ return results
147
+
148
+
149
+ def get_global_variable_value_internal(ea: int) -> str:
150
+ import ida_typeinf
151
+ import ida_nalt
152
+ import ida_bytes
153
+ from .sync import IDAError
154
+
155
+ tif = ida_typeinf.tinfo_t()
156
+ if not ida_nalt.get_tinfo(tif, ea):
157
+ if not ida_bytes.has_any_name(ea):
158
+ raise IDAError(f"Failed to get type information for variable at {ea:#x}")
159
+
160
+ size = ida_bytes.get_item_size(ea)
161
+ if size == 0:
162
+ raise IDAError(f"Failed to get type information for variable at {ea:#x}")
163
+ else:
164
+ size = tif.get_size()
165
+
166
+ if size == 0 and tif.is_array() and tif.get_array_element().is_decl_char():
167
+ raw = idaapi.get_strlit_contents(ea, -1, 0)
168
+ if not raw:
169
+ return '""'
170
+ return_string = raw.decode("utf-8", errors="replace").strip()
171
+ return f'"{return_string}"'
172
+ elif size == 1:
173
+ return hex(ida_bytes.get_byte(ea))
174
+ elif size == 2:
175
+ return hex(ida_bytes.get_word(ea))
176
+ elif size == 4:
177
+ return hex(ida_bytes.get_dword(ea))
178
+ elif size == 8:
179
+ return hex(ida_bytes.get_qword(ea))
180
+ else:
181
+ return " ".join(hex(x) for x in ida_bytes.get_bytes(ea, size))
182
+
183
+
184
+ @tool
185
+ @idasync
186
+ def get_global_value(
187
+ queries: Annotated[
188
+ list[str] | str, "Global variable addresses or names to read values from"
189
+ ],
190
+ ) -> list[dict]:
191
+ """Read global variable values by address or name
192
+ (auto-detects hex addresses vs names)"""
193
+ from .utils import looks_like_address
194
+
195
+ queries = normalize_list_input(queries)
196
+ results = []
197
+
198
+ for query in queries:
199
+ try:
200
+ ea = idaapi.BADADDR
201
+
202
+ # Try as address first if it looks like one
203
+ if looks_like_address(query):
204
+ try:
205
+ ea = parse_address(query)
206
+ except Exception:
207
+ ea = idaapi.BADADDR
208
+
209
+ # Fall back to name lookup
210
+ if ea == idaapi.BADADDR:
211
+ ea = idaapi.get_name_ea(idaapi.BADADDR, query)
212
+
213
+ if ea == idaapi.BADADDR:
214
+ results.append({"query": query, "value": None, "error": "Not found"})
215
+ continue
216
+
217
+ value = get_global_variable_value_internal(ea)
218
+ results.append({"query": query, "value": value, "error": None})
219
+ except Exception as e:
220
+ results.append({"query": query, "value": None, "error": str(e)})
221
+
222
+ return results
223
+
224
+
225
+ # ============================================================================
226
+ # Batch Data Operations
227
+ # ============================================================================
228
+
229
+
230
+ @tool
231
+ @idasync
232
+ def patch(patches: list[MemoryPatch] | MemoryPatch) -> list[dict]:
233
+ """Patch bytes at memory addresses with hex data"""
234
+ if isinstance(patches, dict):
235
+ patches = [patches]
236
+
237
+ results = []
238
+
239
+ for patch in patches:
240
+ try:
241
+ ea = parse_address(patch["addr"])
242
+ data = bytes.fromhex(patch["data"])
243
+
244
+ ida_bytes.patch_bytes(ea, data)
245
+ results.append(
246
+ {"addr": patch["addr"], "size": len(data), "ok": True, "error": None}
247
+ )
248
+
249
+ except Exception as e:
250
+ results.append({"addr": patch.get("addr"), "size": 0, "error": str(e)})
251
+
252
+ return results
253
+
254
+
255
+ @tool
256
+ @idasync
257
+ def put_int(
258
+ items: Annotated[
259
+ list[IntWrite] | IntWrite,
260
+ "Integer write requests (ty, addr, value). value is a string; supports 0x.. and negatives",
261
+ ],
262
+ ) -> list[dict]:
263
+ """Write integer values to memory addresses"""
264
+ if isinstance(items, dict):
265
+ items = [items]
266
+
267
+ results = []
268
+ for item in items:
269
+ addr = item.get("addr", "")
270
+ ty = item.get("ty", "")
271
+ value_text = item.get("value")
272
+
273
+ try:
274
+ bits, signed, byte_order, normalized = _parse_int_class(ty)
275
+ value = _parse_int_value(value_text, signed, bits)
276
+ size = bits // 8
277
+ try:
278
+ data = value.to_bytes(size, byte_order, signed=signed)
279
+ except OverflowError:
280
+ raise ValueError(f"Value {value_text} does not fit in {normalized}")
281
+
282
+ ea = parse_address(addr)
283
+ ida_bytes.patch_bytes(ea, data)
284
+ results.append(
285
+ {
286
+ "addr": addr,
287
+ "ty": normalized,
288
+ "value": str(value_text),
289
+ "ok": True,
290
+ "error": None,
291
+ }
292
+ )
293
+ except Exception as e:
294
+ results.append(
295
+ {
296
+ "addr": addr,
297
+ "ty": ty,
298
+ "value": str(value_text) if value_text is not None else None,
299
+ "ok": False,
300
+ "error": str(e),
301
+ }
302
+ )
303
+
304
+ return results
@@ -0,0 +1,406 @@
1
+ import idaapi
2
+ import idautils
3
+ import idc
4
+ import ida_hexrays
5
+ import ida_bytes
6
+ import ida_typeinf
7
+ import ida_frame
8
+ import ida_dirtree
9
+
10
+ from .rpc import tool
11
+ from .sync import idasync, IDAError
12
+ from .cache import invalidate_function_caches, decompile_cache
13
+ from .utils import (
14
+ parse_address,
15
+ decompile_checked,
16
+ refresh_decompiler_ctext,
17
+ CommentOp,
18
+ AsmPatchOp,
19
+ FunctionRename,
20
+ GlobalRename,
21
+ LocalRename,
22
+ StackRename,
23
+ RenameBatch,
24
+ )
25
+
26
+
27
+ # ============================================================================
28
+ # Modification Operations
29
+ # ============================================================================
30
+
31
+
32
+ @tool
33
+ @idasync
34
+ def set_comments(items: list[CommentOp] | CommentOp):
35
+ """Set comments at addresses (both disassembly and decompiler views)"""
36
+ if isinstance(items, dict):
37
+ items = [items]
38
+
39
+ results = []
40
+ for item in items:
41
+ addr_str = item.get("addr", "")
42
+ comment = item.get("comment", "")
43
+
44
+ try:
45
+ ea = parse_address(addr_str)
46
+
47
+ # Invalidate decompile cache for this function
48
+ func = idaapi.get_func(ea)
49
+ if func:
50
+ decompile_cache.invalidate(hex(func.start_ea))
51
+
52
+ if not idaapi.set_cmt(ea, comment, False):
53
+ results.append(
54
+ {
55
+ "addr": addr_str,
56
+ "error": f"Failed to set disassembly comment at {hex(ea)}",
57
+ }
58
+ )
59
+ continue
60
+
61
+ if not ida_hexrays.init_hexrays_plugin():
62
+ results.append({"addr": addr_str, "ok": True})
63
+ continue
64
+
65
+ try:
66
+ cfunc = decompile_checked(ea)
67
+ except IDAError:
68
+ results.append({"addr": addr_str, "ok": True})
69
+ continue
70
+
71
+ if ea == cfunc.entry_ea:
72
+ idc.set_func_cmt(ea, comment, True)
73
+ cfunc.refresh_func_ctext()
74
+ results.append({"addr": addr_str, "ok": True})
75
+ continue
76
+
77
+ eamap = cfunc.get_eamap()
78
+ if ea not in eamap:
79
+ results.append(
80
+ {
81
+ "addr": addr_str,
82
+ "ok": True,
83
+ "error": f"Failed to set decompiler comment at {hex(ea)}",
84
+ }
85
+ )
86
+ continue
87
+ nearest_ea = eamap[ea][0].ea
88
+
89
+ if cfunc.has_orphan_cmts():
90
+ cfunc.del_orphan_cmts()
91
+ cfunc.save_user_cmts()
92
+
93
+ tl = idaapi.treeloc_t()
94
+ tl.ea = nearest_ea
95
+ for itp in range(idaapi.ITP_SEMI, idaapi.ITP_COLON):
96
+ tl.itp = itp
97
+ cfunc.set_user_cmt(tl, comment)
98
+ cfunc.save_user_cmts()
99
+ cfunc.refresh_func_ctext()
100
+ if not cfunc.has_orphan_cmts():
101
+ results.append({"addr": addr_str, "ok": True})
102
+ break
103
+ cfunc.del_orphan_cmts()
104
+ cfunc.save_user_cmts()
105
+ else:
106
+ results.append(
107
+ {
108
+ "addr": addr_str,
109
+ "ok": True,
110
+ "error": f"Failed to set decompiler comment at {hex(ea)}",
111
+ }
112
+ )
113
+ except Exception as e:
114
+ results.append({"addr": addr_str, "error": str(e)})
115
+
116
+ return results
117
+
118
+
119
+ @tool
120
+ @idasync
121
+ def patch_asm(items: list[AsmPatchOp] | AsmPatchOp) -> list[dict]:
122
+ """Patch assembly instructions at addresses"""
123
+ if isinstance(items, dict):
124
+ items = [items]
125
+
126
+ results = []
127
+ for item in items:
128
+ addr_str = item.get("addr", "")
129
+ instructions = item.get("asm", "")
130
+
131
+ try:
132
+ ea = parse_address(addr_str)
133
+ assembles = instructions.split(";")
134
+ for assemble in assembles:
135
+ assemble = assemble.strip()
136
+ try:
137
+ (check_assemble, bytes_to_patch) = idautils.Assemble(ea, assemble)
138
+ if not check_assemble:
139
+ results.append(
140
+ {
141
+ "addr": addr_str,
142
+ "error": f"Failed to assemble: {assemble}",
143
+ }
144
+ )
145
+ break
146
+ ida_bytes.patch_bytes(ea, bytes_to_patch)
147
+ ea += len(bytes_to_patch)
148
+ except Exception as e:
149
+ results.append(
150
+ {"addr": addr_str, "error": f"Failed at {hex(ea)}: {e}"}
151
+ )
152
+ break
153
+ else:
154
+ results.append({"addr": addr_str, "ok": True})
155
+ except Exception as e:
156
+ results.append({"addr": addr_str, "error": str(e)})
157
+
158
+ return results
159
+
160
+
161
+ @tool
162
+ @idasync
163
+ def rename(batch: RenameBatch) -> dict:
164
+ """Unified rename operation for functions, globals, locals, and stack variables"""
165
+
166
+ def _normalize_items(items):
167
+ """Convert single item or None to list"""
168
+ if items is None:
169
+ return []
170
+ return [items] if isinstance(items, dict) else items
171
+
172
+ def _has_user_name(ea: int) -> bool:
173
+ flags = idaapi.get_flags(ea)
174
+ checker = getattr(idaapi, "has_user_name", None)
175
+ if checker is not None:
176
+ return checker(flags)
177
+ try:
178
+ import ida_name
179
+
180
+ checker = getattr(ida_name, "has_user_name", None)
181
+ if checker is not None:
182
+ return checker(flags)
183
+ except Exception:
184
+ pass
185
+ return False
186
+
187
+ def _place_func_in_vibe_dir(ea: int) -> tuple[bool, str | None]:
188
+ tree = ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS)
189
+ if tree is None:
190
+ return False, "Function dirtree not available"
191
+
192
+ if not tree.load():
193
+ return False, "Failed to load function dirtree"
194
+
195
+ vibe_path = "/vibe/"
196
+ if not tree.isdir(vibe_path):
197
+ err = tree.mkdir(vibe_path)
198
+ if err not in (ida_dirtree.DTE_OK, ida_dirtree.DTE_ALREADY_EXISTS):
199
+ return False, f"mkdir failed: {err}"
200
+
201
+ old_cwd = tree.getcwd()
202
+ try:
203
+ if tree.chdir(vibe_path) != ida_dirtree.DTE_OK:
204
+ return False, "Failed to chdir to vibe"
205
+ err = tree.link(ea)
206
+ if err not in (ida_dirtree.DTE_OK, ida_dirtree.DTE_ALREADY_EXISTS):
207
+ return False, f"link failed: {err}"
208
+ if not tree.save():
209
+ return False, "Failed to save function dirtree"
210
+ finally:
211
+ if old_cwd:
212
+ tree.chdir(old_cwd)
213
+
214
+ return True, None
215
+
216
+ def _rename_funcs(items: list[FunctionRename]) -> list[dict]:
217
+ results = []
218
+ for item in items:
219
+ try:
220
+ ea = parse_address(item["addr"])
221
+ had_user_name = _has_user_name(ea)
222
+ success = idaapi.set_name(ea, item["name"], idaapi.SN_CHECK)
223
+ if success:
224
+ # Invalidate caches for this function
225
+ invalidate_function_caches(ea)
226
+ func = idaapi.get_func(ea)
227
+ if func:
228
+ refresh_decompiler_ctext(func.start_ea)
229
+ if not had_user_name and func:
230
+ placed, place_error = _place_func_in_vibe_dir(func.start_ea)
231
+ else:
232
+ placed, place_error = None, None
233
+ results.append(
234
+ {
235
+ "addr": item["addr"],
236
+ "name": item["name"],
237
+ "ok": success,
238
+ "error": None if success else "Rename failed",
239
+ "dir": "vibe" if success and placed else None,
240
+ "dir_error": place_error if success else None,
241
+ }
242
+ )
243
+ except Exception as e:
244
+ results.append({"addr": item.get("addr"), "error": str(e)})
245
+ return results
246
+
247
+ def _rename_globals(items: list[GlobalRename]) -> list[dict]:
248
+ results = []
249
+ for item in items:
250
+ try:
251
+ ea = idaapi.get_name_ea(idaapi.BADADDR, item["old"])
252
+ if ea == idaapi.BADADDR:
253
+ results.append(
254
+ {
255
+ "old": item["old"],
256
+ "new": item["new"],
257
+ "ok": False,
258
+ "error": f"Global '{item['old']}' not found",
259
+ }
260
+ )
261
+ continue
262
+ success = idaapi.set_name(ea, item["new"], idaapi.SN_CHECK)
263
+ results.append(
264
+ {
265
+ "old": item["old"],
266
+ "new": item["new"],
267
+ "ok": success,
268
+ "error": None if success else "Rename failed",
269
+ }
270
+ )
271
+ except Exception as e:
272
+ results.append({"old": item.get("old"), "error": str(e)})
273
+ return results
274
+
275
+ def _rename_locals(items: list[LocalRename]) -> list[dict]:
276
+ results = []
277
+ for item in items:
278
+ try:
279
+ func = idaapi.get_func(parse_address(item["func_addr"]))
280
+ if not func:
281
+ results.append(
282
+ {
283
+ "func_addr": item["func_addr"],
284
+ "old": item["old"],
285
+ "new": item["new"],
286
+ "ok": False,
287
+ "error": "No function found",
288
+ }
289
+ )
290
+ continue
291
+ success = ida_hexrays.rename_lvar(
292
+ func.start_ea, item["old"], item["new"]
293
+ )
294
+ if success:
295
+ refresh_decompiler_ctext(func.start_ea)
296
+ results.append(
297
+ {
298
+ "func_addr": item["func_addr"],
299
+ "old": item["old"],
300
+ "new": item["new"],
301
+ "ok": success,
302
+ "error": None if success else "Rename failed",
303
+ }
304
+ )
305
+ except Exception as e:
306
+ results.append({"func_addr": item.get("func_addr"), "error": str(e)})
307
+ return results
308
+
309
+ def _rename_stack(items: list[StackRename]) -> list[dict]:
310
+ results = []
311
+ for item in items:
312
+ try:
313
+ func = idaapi.get_func(parse_address(item["func_addr"]))
314
+ if not func:
315
+ results.append(
316
+ {
317
+ "func_addr": item["func_addr"],
318
+ "old": item["old"],
319
+ "new": item["new"],
320
+ "ok": False,
321
+ "error": "No function found",
322
+ }
323
+ )
324
+ continue
325
+
326
+ frame_tif = ida_typeinf.tinfo_t()
327
+ if not ida_frame.get_func_frame(frame_tif, func):
328
+ results.append(
329
+ {
330
+ "func_addr": item["func_addr"],
331
+ "old": item["old"],
332
+ "new": item["new"],
333
+ "ok": False,
334
+ "error": "No frame",
335
+ }
336
+ )
337
+ continue
338
+
339
+ idx, udm = frame_tif.get_udm(item["old"])
340
+ if not udm:
341
+ results.append(
342
+ {
343
+ "func_addr": item["func_addr"],
344
+ "old": item["old"],
345
+ "new": item["new"],
346
+ "ok": False,
347
+ "error": f"'{item['old']}' not found",
348
+ }
349
+ )
350
+ continue
351
+
352
+ tid = frame_tif.get_udm_tid(idx)
353
+ if ida_frame.is_special_frame_member(tid):
354
+ results.append(
355
+ {
356
+ "func_addr": item["func_addr"],
357
+ "old": item["old"],
358
+ "new": item["new"],
359
+ "ok": False,
360
+ "error": "Special frame member",
361
+ }
362
+ )
363
+ continue
364
+
365
+ udm = ida_typeinf.udm_t()
366
+ frame_tif.get_udm_by_tid(udm, tid)
367
+ offset = udm.offset // 8
368
+ if ida_frame.is_funcarg_off(func, offset):
369
+ results.append(
370
+ {
371
+ "func_addr": item["func_addr"],
372
+ "old": item["old"],
373
+ "new": item["new"],
374
+ "ok": False,
375
+ "error": "Argument member",
376
+ }
377
+ )
378
+ continue
379
+
380
+ sval = ida_frame.soff_to_fpoff(func, offset)
381
+ success = ida_frame.define_stkvar(func, item["new"], sval, udm.type)
382
+ results.append(
383
+ {
384
+ "func_addr": item["func_addr"],
385
+ "old": item["old"],
386
+ "new": item["new"],
387
+ "ok": success,
388
+ "error": None if success else "Rename failed",
389
+ }
390
+ )
391
+ except Exception as e:
392
+ results.append({"func_addr": item.get("func_addr"), "error": str(e)})
393
+ return results
394
+
395
+ # Process each category
396
+ result = {}
397
+ if "func" in batch:
398
+ result["func"] = _rename_funcs(_normalize_items(batch["func"]))
399
+ if "data" in batch:
400
+ result["data"] = _rename_globals(_normalize_items(batch["data"]))
401
+ if "local" in batch:
402
+ result["local"] = _rename_locals(_normalize_items(batch["local"]))
403
+ if "stack" in batch:
404
+ result["stack"] = _rename_stack(_normalize_items(batch["stack"]))
405
+
406
+ return result