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.
- ida_pro_mcp/__init__.py +0 -0
- ida_pro_mcp/__main__.py +6 -0
- ida_pro_mcp/ida_mcp/__init__.py +68 -0
- ida_pro_mcp/ida_mcp/api_analysis.py +1296 -0
- ida_pro_mcp/ida_mcp/api_core.py +337 -0
- ida_pro_mcp/ida_mcp/api_debug.py +617 -0
- ida_pro_mcp/ida_mcp/api_memory.py +304 -0
- ida_pro_mcp/ida_mcp/api_modify.py +406 -0
- ida_pro_mcp/ida_mcp/api_python.py +179 -0
- ida_pro_mcp/ida_mcp/api_resources.py +295 -0
- ida_pro_mcp/ida_mcp/api_stack.py +167 -0
- ida_pro_mcp/ida_mcp/api_types.py +480 -0
- ida_pro_mcp/ida_mcp/auth.py +166 -0
- ida_pro_mcp/ida_mcp/cache.py +232 -0
- ida_pro_mcp/ida_mcp/config.py +228 -0
- ida_pro_mcp/ida_mcp/framework.py +547 -0
- ida_pro_mcp/ida_mcp/http.py +859 -0
- ida_pro_mcp/ida_mcp/port_utils.py +104 -0
- ida_pro_mcp/ida_mcp/rpc.py +187 -0
- ida_pro_mcp/ida_mcp/server_manager.py +339 -0
- ida_pro_mcp/ida_mcp/sync.py +233 -0
- ida_pro_mcp/ida_mcp/tests/__init__.py +14 -0
- ida_pro_mcp/ida_mcp/tests/test_api_analysis.py +336 -0
- ida_pro_mcp/ida_mcp/tests/test_api_core.py +237 -0
- ida_pro_mcp/ida_mcp/tests/test_api_memory.py +207 -0
- ida_pro_mcp/ida_mcp/tests/test_api_modify.py +123 -0
- ida_pro_mcp/ida_mcp/tests/test_api_resources.py +199 -0
- ida_pro_mcp/ida_mcp/tests/test_api_stack.py +77 -0
- ida_pro_mcp/ida_mcp/tests/test_api_types.py +249 -0
- ida_pro_mcp/ida_mcp/ui.py +357 -0
- ida_pro_mcp/ida_mcp/utils.py +1186 -0
- ida_pro_mcp/ida_mcp/zeromcp/__init__.py +5 -0
- ida_pro_mcp/ida_mcp/zeromcp/jsonrpc.py +384 -0
- ida_pro_mcp/ida_mcp/zeromcp/mcp.py +883 -0
- ida_pro_mcp/ida_mcp.py +186 -0
- ida_pro_mcp/idalib_server.py +354 -0
- ida_pro_mcp/idalib_session_manager.py +259 -0
- ida_pro_mcp/server.py +1060 -0
- ida_pro_mcp/test.py +170 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/METADATA +405 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/RECORD +45 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/WHEEL +5 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/entry_points.txt +4 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/licenses/LICENSE +21 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Tests for api_core API functions."""
|
|
2
|
+
|
|
3
|
+
# Import test framework from parent
|
|
4
|
+
from ..framework import (
|
|
5
|
+
test,
|
|
6
|
+
assert_has_keys,
|
|
7
|
+
assert_is_list,
|
|
8
|
+
assert_all_have_keys,
|
|
9
|
+
get_any_function,
|
|
10
|
+
get_data_address,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# Import functions under test
|
|
14
|
+
from ..api_core import (
|
|
15
|
+
lookup_funcs,
|
|
16
|
+
int_convert,
|
|
17
|
+
list_funcs,
|
|
18
|
+
list_globals,
|
|
19
|
+
imports,
|
|
20
|
+
find_regex,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Import sync module for IDAError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ============================================================================
|
|
27
|
+
# Tests for lookup_funcs
|
|
28
|
+
# ============================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@test()
|
|
32
|
+
def test_lookup_funcs_by_address():
|
|
33
|
+
"""lookup_funcs can find function by address"""
|
|
34
|
+
fn_addr = get_any_function()
|
|
35
|
+
if not fn_addr:
|
|
36
|
+
return # Skip if no functions
|
|
37
|
+
|
|
38
|
+
result = lookup_funcs(fn_addr)
|
|
39
|
+
assert_is_list(result, min_length=1)
|
|
40
|
+
assert result[0]["fn"] is not None
|
|
41
|
+
assert result[0]["error"] is None
|
|
42
|
+
assert_has_keys(result[0]["fn"], "addr", "name", "size")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@test()
|
|
46
|
+
def test_lookup_funcs_invalid():
|
|
47
|
+
"""lookup_funcs returns error for invalid address"""
|
|
48
|
+
# Use an address that's unlikely to be a valid function
|
|
49
|
+
result = lookup_funcs("0xDEADBEEFDEADBEEF")
|
|
50
|
+
assert_is_list(result, min_length=1)
|
|
51
|
+
assert result[0]["fn"] is None
|
|
52
|
+
assert result[0]["error"] is not None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@test()
|
|
56
|
+
def test_lookup_funcs_wildcard():
|
|
57
|
+
"""lookup_funcs with '*' returns all functions"""
|
|
58
|
+
result = lookup_funcs("*")
|
|
59
|
+
assert_is_list(result, min_length=1)
|
|
60
|
+
# All results should have query="*" and a function
|
|
61
|
+
for r in result:
|
|
62
|
+
assert r["query"] == "*"
|
|
63
|
+
assert r["fn"] is not None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@test()
|
|
67
|
+
def test_lookup_funcs_empty():
|
|
68
|
+
"""lookup_funcs with empty string returns all functions"""
|
|
69
|
+
result = lookup_funcs("")
|
|
70
|
+
assert_is_list(result, min_length=1)
|
|
71
|
+
assert result[0]["query"] == "*"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@test()
|
|
75
|
+
def test_lookup_funcs_malformed_hex():
|
|
76
|
+
"""lookup_funcs handles malformed hex address"""
|
|
77
|
+
# This looks like an address but isn't valid hex
|
|
78
|
+
result = lookup_funcs("0xZZZZ")
|
|
79
|
+
assert_is_list(result, min_length=1)
|
|
80
|
+
# Should return error since it's not a valid address or name
|
|
81
|
+
assert result[0]["error"] is not None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@test()
|
|
85
|
+
def test_lookup_funcs_data_address():
|
|
86
|
+
"""lookup_funcs with valid address but not a function"""
|
|
87
|
+
data_addr = get_data_address()
|
|
88
|
+
if not data_addr:
|
|
89
|
+
return # Skip if no data segments
|
|
90
|
+
|
|
91
|
+
result = lookup_funcs(data_addr)
|
|
92
|
+
assert_is_list(result, min_length=1)
|
|
93
|
+
# Should return "Not a function" error
|
|
94
|
+
assert result[0]["fn"] is None
|
|
95
|
+
assert "Not a function" in str(result[0]["error"]) or "Not found" in str(
|
|
96
|
+
result[0]["error"]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ============================================================================
|
|
101
|
+
# Tests for int_convert
|
|
102
|
+
# ============================================================================
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@test()
|
|
106
|
+
def test_int_convert():
|
|
107
|
+
"""int_convert properly converts numbers"""
|
|
108
|
+
result = int_convert({"text": "0x41"})
|
|
109
|
+
assert_is_list(result, min_length=1)
|
|
110
|
+
assert result[0]["error"] is None
|
|
111
|
+
assert result[0]["result"] is not None
|
|
112
|
+
conv = result[0]["result"]
|
|
113
|
+
assert_has_keys(conv, "decimal", "hexadecimal", "bytes", "binary")
|
|
114
|
+
assert conv["decimal"] == "65"
|
|
115
|
+
assert conv["hexadecimal"] == "0x41"
|
|
116
|
+
assert conv["ascii"] == "A"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@test()
|
|
120
|
+
def test_int_convert_invalid_text():
|
|
121
|
+
"""int_convert handles invalid number text"""
|
|
122
|
+
result = int_convert({"text": "not_a_number"})
|
|
123
|
+
assert_is_list(result, min_length=1)
|
|
124
|
+
assert result[0]["result"] is None
|
|
125
|
+
assert result[0]["error"] is not None
|
|
126
|
+
assert "Invalid number" in result[0]["error"]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@test()
|
|
130
|
+
def test_int_convert_overflow():
|
|
131
|
+
"""int_convert handles overflow with small size"""
|
|
132
|
+
# Try to fit a large number into 1 byte
|
|
133
|
+
result = int_convert({"text": "0xFFFF", "size": 1})
|
|
134
|
+
assert_is_list(result, min_length=1)
|
|
135
|
+
assert result[0]["result"] is None
|
|
136
|
+
assert result[0]["error"] is not None
|
|
137
|
+
assert "too big" in result[0]["error"]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@test()
|
|
141
|
+
def test_int_convert_non_ascii():
|
|
142
|
+
"""int_convert handles non-ASCII bytes"""
|
|
143
|
+
# 0x01 is not a printable ASCII character (control char)
|
|
144
|
+
result = int_convert({"text": "0x01"})
|
|
145
|
+
assert_is_list(result, min_length=1)
|
|
146
|
+
assert result[0]["error"] is None
|
|
147
|
+
# ascii should be None for non-printable bytes
|
|
148
|
+
assert result[0]["result"]["ascii"] is None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ============================================================================
|
|
152
|
+
# Tests for list_funcs
|
|
153
|
+
# ============================================================================
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@test()
|
|
157
|
+
def test_list_funcs():
|
|
158
|
+
"""list_funcs returns functions with proper structure"""
|
|
159
|
+
result = list_funcs({})
|
|
160
|
+
assert_is_list(result, min_length=1)
|
|
161
|
+
page = result[0]
|
|
162
|
+
assert_has_keys(page, "data", "offset", "count", "total")
|
|
163
|
+
if page["data"]:
|
|
164
|
+
assert_all_have_keys(page["data"], "addr", "name", "size")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@test()
|
|
168
|
+
def test_list_funcs_pagination():
|
|
169
|
+
"""list_funcs respects pagination parameters"""
|
|
170
|
+
result = list_funcs({"offset": 0, "count": 5})
|
|
171
|
+
assert_is_list(result, min_length=1)
|
|
172
|
+
page = result[0]
|
|
173
|
+
assert page["offset"] == 0
|
|
174
|
+
assert len(page["data"]) <= 5
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ============================================================================
|
|
178
|
+
# Tests for list_globals
|
|
179
|
+
# ============================================================================
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@test()
|
|
183
|
+
def test_list_globals():
|
|
184
|
+
"""list_globals returns globals with proper structure"""
|
|
185
|
+
result = list_globals({})
|
|
186
|
+
assert_is_list(result, min_length=1)
|
|
187
|
+
page = result[0]
|
|
188
|
+
assert_has_keys(page, "data", "offset", "count", "total")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@test()
|
|
192
|
+
def test_list_globals_pagination():
|
|
193
|
+
"""list_globals respects pagination parameters"""
|
|
194
|
+
result = list_globals({"offset": 0, "count": 5})
|
|
195
|
+
assert_is_list(result, min_length=1)
|
|
196
|
+
page = result[0]
|
|
197
|
+
assert page["offset"] == 0
|
|
198
|
+
assert len(page["data"]) <= 5
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ============================================================================
|
|
202
|
+
# Tests for imports
|
|
203
|
+
# ============================================================================
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@test()
|
|
207
|
+
def test_imports():
|
|
208
|
+
"""imports returns import list with proper structure"""
|
|
209
|
+
result = imports({})
|
|
210
|
+
assert_is_list(result, min_length=1)
|
|
211
|
+
page = result[0]
|
|
212
|
+
assert_has_keys(page, "data", "offset", "count", "total")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@test()
|
|
216
|
+
def test_imports_pagination():
|
|
217
|
+
"""imports respects pagination parameters"""
|
|
218
|
+
result = imports({"offset": 0, "count": 5})
|
|
219
|
+
assert_is_list(result, min_length=1)
|
|
220
|
+
page = result[0]
|
|
221
|
+
assert page["offset"] == 0
|
|
222
|
+
assert len(page["data"]) <= 5
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ============================================================================
|
|
226
|
+
# Tests for find_regex
|
|
227
|
+
# ============================================================================
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@test()
|
|
231
|
+
def test_find_regex():
|
|
232
|
+
"""find_regex can search for patterns"""
|
|
233
|
+
# Search for a common pattern that should exist in most binaries
|
|
234
|
+
result = find_regex({"pattern": ".*"})
|
|
235
|
+
assert_is_list(result, min_length=1)
|
|
236
|
+
# Result structure should have matches
|
|
237
|
+
assert_has_keys(result[0], "query", "matches", "error")
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Tests for api_memory API functions."""
|
|
2
|
+
|
|
3
|
+
# Import test framework from parent
|
|
4
|
+
from ..framework import (
|
|
5
|
+
test,
|
|
6
|
+
assert_has_keys,
|
|
7
|
+
assert_is_list,
|
|
8
|
+
get_any_string,
|
|
9
|
+
get_first_segment,
|
|
10
|
+
get_data_address,
|
|
11
|
+
get_unmapped_address,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Import functions under test
|
|
15
|
+
from ..api_memory import (
|
|
16
|
+
get_bytes,
|
|
17
|
+
get_int,
|
|
18
|
+
get_string,
|
|
19
|
+
get_global_value,
|
|
20
|
+
patch,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Import sync module for IDAError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ============================================================================
|
|
27
|
+
# Tests for get_bytes
|
|
28
|
+
# ============================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@test()
|
|
32
|
+
def test_get_bytes():
|
|
33
|
+
"""get_bytes reads bytes from a valid address"""
|
|
34
|
+
seg = get_first_segment()
|
|
35
|
+
if not seg:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
start_addr, _ = seg
|
|
39
|
+
result = get_bytes({"addr": start_addr, "size": 16})
|
|
40
|
+
assert_is_list(result, min_length=1)
|
|
41
|
+
r = result[0]
|
|
42
|
+
assert_has_keys(r, "addr", "hex", "error")
|
|
43
|
+
if r["error"] is None:
|
|
44
|
+
assert r["hex"] is not None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@test()
|
|
48
|
+
def test_get_bytes_invalid():
|
|
49
|
+
"""get_bytes handles invalid address"""
|
|
50
|
+
result = get_bytes({"addr": get_unmapped_address(), "size": 16})
|
|
51
|
+
assert_is_list(result, min_length=1)
|
|
52
|
+
r = result[0]
|
|
53
|
+
# Should have error or empty data
|
|
54
|
+
assert r.get("error") is not None or r.get("hex") == ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ============================================================================
|
|
58
|
+
# Tests for get_int
|
|
59
|
+
# ============================================================================
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@test()
|
|
63
|
+
def test_get_int_u8():
|
|
64
|
+
"""get_int reads 8-bit unsigned integer"""
|
|
65
|
+
seg = get_first_segment()
|
|
66
|
+
if not seg:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
start_addr, _ = seg
|
|
70
|
+
result = get_int({"addr": start_addr, "size": 1})
|
|
71
|
+
assert_is_list(result, min_length=1)
|
|
72
|
+
r = result[0]
|
|
73
|
+
assert_has_keys(r, "addr", "value", "error")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@test()
|
|
77
|
+
def test_get_int_u16():
|
|
78
|
+
"""get_int reads 16-bit unsigned integer"""
|
|
79
|
+
seg = get_first_segment()
|
|
80
|
+
if not seg:
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
start_addr, _ = seg
|
|
84
|
+
result = get_int({"addr": start_addr, "size": 2})
|
|
85
|
+
assert_is_list(result, min_length=1)
|
|
86
|
+
r = result[0]
|
|
87
|
+
assert_has_keys(r, "addr", "value", "error")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@test()
|
|
91
|
+
def test_get_int_u32():
|
|
92
|
+
"""get_int reads 32-bit unsigned integer"""
|
|
93
|
+
seg = get_first_segment()
|
|
94
|
+
if not seg:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
start_addr, _ = seg
|
|
98
|
+
result = get_int({"addr": start_addr, "size": 4})
|
|
99
|
+
assert_is_list(result, min_length=1)
|
|
100
|
+
r = result[0]
|
|
101
|
+
assert_has_keys(r, "addr", "value", "error")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@test()
|
|
105
|
+
def test_get_int_u64():
|
|
106
|
+
"""get_int reads 64-bit unsigned integer"""
|
|
107
|
+
seg = get_first_segment()
|
|
108
|
+
if not seg:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
start_addr, _ = seg
|
|
112
|
+
result = get_int({"addr": start_addr, "size": 8})
|
|
113
|
+
assert_is_list(result, min_length=1)
|
|
114
|
+
r = result[0]
|
|
115
|
+
assert_has_keys(r, "addr", "value", "error")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ============================================================================
|
|
119
|
+
# Tests for get_string
|
|
120
|
+
# ============================================================================
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@test()
|
|
124
|
+
def test_get_string():
|
|
125
|
+
"""get_string reads string from a valid address"""
|
|
126
|
+
str_addr = get_any_string()
|
|
127
|
+
if not str_addr:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
result = get_string(str_addr)
|
|
131
|
+
assert_is_list(result, min_length=1)
|
|
132
|
+
r = result[0]
|
|
133
|
+
assert_has_keys(r, "addr", "value", "error")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ============================================================================
|
|
137
|
+
# Tests for get_global_value
|
|
138
|
+
# ============================================================================
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@test()
|
|
142
|
+
def test_get_global_value():
|
|
143
|
+
"""get_global_value retrieves global variable value"""
|
|
144
|
+
# Try to get value at a data address
|
|
145
|
+
data_addr = get_data_address()
|
|
146
|
+
if not data_addr:
|
|
147
|
+
seg = get_first_segment()
|
|
148
|
+
if not seg:
|
|
149
|
+
return
|
|
150
|
+
data_addr = seg[0]
|
|
151
|
+
|
|
152
|
+
result = get_global_value(data_addr)
|
|
153
|
+
assert_is_list(result, min_length=1)
|
|
154
|
+
r = result[0]
|
|
155
|
+
assert_has_keys(r, "addr", "error")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ============================================================================
|
|
159
|
+
# Tests for patch
|
|
160
|
+
# ============================================================================
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@test(skip=True) # Skip by default as it modifies the database
|
|
164
|
+
def test_patch():
|
|
165
|
+
"""patch writes bytes to address"""
|
|
166
|
+
seg = get_first_segment()
|
|
167
|
+
if not seg:
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
start_addr, _ = seg
|
|
171
|
+
# Read original bytes first
|
|
172
|
+
original = get_bytes({"addr": start_addr, "size": 4})
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
result = patch({"addr": start_addr, "hex": "90909090"})
|
|
176
|
+
assert_is_list(result, min_length=1)
|
|
177
|
+
r = result[0]
|
|
178
|
+
assert_has_keys(r, "addr", "error")
|
|
179
|
+
finally:
|
|
180
|
+
# Restore original bytes
|
|
181
|
+
if original and original[0].get("hex"):
|
|
182
|
+
patch({"addr": start_addr, "hex": original[0]["hex"]})
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@test()
|
|
186
|
+
def test_patch_invalid_address():
|
|
187
|
+
"""patch handles invalid address"""
|
|
188
|
+
result = patch({"addr": get_unmapped_address(), "hex": "90"})
|
|
189
|
+
assert_is_list(result, min_length=1)
|
|
190
|
+
r = result[0]
|
|
191
|
+
# Should have error
|
|
192
|
+
assert r.get("error") is not None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@test()
|
|
196
|
+
def test_patch_invalid_hex_data():
|
|
197
|
+
"""patch handles invalid hex data"""
|
|
198
|
+
seg = get_first_segment()
|
|
199
|
+
if not seg:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
start_addr, _ = seg
|
|
203
|
+
result = patch({"addr": start_addr, "hex": "ZZZZ"})
|
|
204
|
+
assert_is_list(result, min_length=1)
|
|
205
|
+
r = result[0]
|
|
206
|
+
# Should have error for invalid hex
|
|
207
|
+
assert r.get("error") is not None
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Tests for api_modify API functions."""
|
|
2
|
+
|
|
3
|
+
# Import test framework from parent
|
|
4
|
+
from ..framework import (
|
|
5
|
+
test,
|
|
6
|
+
assert_has_keys,
|
|
7
|
+
assert_is_list,
|
|
8
|
+
get_any_function,
|
|
9
|
+
get_data_address,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# Import functions under test
|
|
13
|
+
from ..api_modify import (
|
|
14
|
+
set_comments,
|
|
15
|
+
patch_asm,
|
|
16
|
+
rename,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Import sync module for IDAError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ============================================================================
|
|
23
|
+
# Tests for set_comments
|
|
24
|
+
# ============================================================================
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@test()
|
|
28
|
+
def test_set_comment_roundtrip():
|
|
29
|
+
"""set_comments can add and remove comments"""
|
|
30
|
+
fn_addr = get_any_function()
|
|
31
|
+
if not fn_addr:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
# Add a comment
|
|
35
|
+
result = set_comments({"addr": fn_addr, "comment": "__TEST_COMMENT__"})
|
|
36
|
+
assert_is_list(result, min_length=1)
|
|
37
|
+
|
|
38
|
+
# Clear the comment
|
|
39
|
+
result = set_comments({"addr": fn_addr, "comment": ""})
|
|
40
|
+
assert_is_list(result, min_length=1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ============================================================================
|
|
44
|
+
# Tests for patch_asm
|
|
45
|
+
# ============================================================================
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@test(skip=True) # Skip by default as it modifies the database
|
|
49
|
+
def test_patch_asm():
|
|
50
|
+
"""patch_asm can patch assembly"""
|
|
51
|
+
fn_addr = get_any_function()
|
|
52
|
+
if not fn_addr:
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
# This is a risky test - patching assembly could corrupt the binary
|
|
56
|
+
result = patch_asm({"addr": fn_addr, "asm": "nop"})
|
|
57
|
+
assert_is_list(result, min_length=1)
|
|
58
|
+
r = result[0]
|
|
59
|
+
assert_has_keys(r, "addr", "error")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ============================================================================
|
|
63
|
+
# Tests for rename
|
|
64
|
+
# ============================================================================
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@test()
|
|
68
|
+
def test_rename_function_roundtrip():
|
|
69
|
+
"""rename function works and can be undone"""
|
|
70
|
+
fn_addr = get_any_function()
|
|
71
|
+
if not fn_addr:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
# Import to get original name
|
|
75
|
+
from ..api_core import lookup_funcs
|
|
76
|
+
|
|
77
|
+
# Get original name
|
|
78
|
+
lookup_result = lookup_funcs(fn_addr)
|
|
79
|
+
if not lookup_result or not lookup_result[0].get("fn"):
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
original_name = lookup_result[0]["fn"]["name"]
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# Rename
|
|
86
|
+
result = rename({"func": [{"addr": fn_addr, "name": "__test_rename__"}]})
|
|
87
|
+
assert isinstance(result, dict)
|
|
88
|
+
|
|
89
|
+
# Verify rename worked
|
|
90
|
+
lookup_result = lookup_funcs(fn_addr)
|
|
91
|
+
new_name = lookup_result[0]["fn"]["name"]
|
|
92
|
+
assert new_name == "__test_rename__"
|
|
93
|
+
finally:
|
|
94
|
+
# Restore
|
|
95
|
+
rename({"func": [{"addr": fn_addr, "name": original_name}]})
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@test()
|
|
99
|
+
def test_rename_global_roundtrip():
|
|
100
|
+
"""rename global variable works"""
|
|
101
|
+
data_addr = get_data_address()
|
|
102
|
+
if not data_addr:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
result = rename({"global": [{"addr": data_addr, "name": "__test_global__"}]})
|
|
107
|
+
assert isinstance(result, dict)
|
|
108
|
+
except Exception:
|
|
109
|
+
pass # May fail if no suitable global exists
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@test(skip=True) # Local variable renaming requires decompilation
|
|
113
|
+
def test_rename_local_roundtrip():
|
|
114
|
+
"""rename local variable works"""
|
|
115
|
+
fn_addr = get_any_function()
|
|
116
|
+
if not fn_addr:
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
# This requires the function to be decompilable and have local variables
|
|
120
|
+
result = rename(
|
|
121
|
+
{"local": [{"func": fn_addr, "name": "old_var", "new_name": "__test_local__"}]}
|
|
122
|
+
)
|
|
123
|
+
assert isinstance(result, dict)
|