1use ghidra_ipc::address::AddressCanonicalizer;
6use ghidra_ipc::protocol::FunctionContext;
7use serde_json::Value;
8
9fn canon_addr(canon: &AddressCanonicalizer, raw: &str) -> String {
12 canon.canonicalize(raw).unwrap_or_else(|_| raw.to_string())
13}
14
15pub fn canonicalize_function_context(
18 canon: &AddressCanonicalizer,
19 mut fc: FunctionContext,
20) -> FunctionContext {
21 fc.entry_point = canon_addr(canon, &fc.entry_point);
22 fc.thunked_to = fc.thunked_to.map(|a| canon_addr(canon, &a));
23 for e in fc.callers.iter_mut() {
24 e.address = canon_addr(canon, &e.address);
25 }
26 for e in fc.callees.iter_mut() {
27 e.address = canon_addr(canon, &e.address);
28 }
29 for e in fc.data_refs.iter_mut() {
30 e.address = canon_addr(canon, &e.address);
31 }
32 for e in fc.comments.iter_mut() {
33 e.address = canon_addr(canon, &e.address);
34 }
35 fc
36}
37
38pub fn canonicalize_candidates(canon: &AddressCanonicalizer, details: &mut Value) {
40 if let Some(cands) = details.get_mut("candidates").and_then(|c| c.as_array_mut()) {
41 for c in cands.iter_mut() {
42 if let Some(a) = c.get("address").and_then(|a| a.as_str()) {
43 let canon_a = canon_addr(canon, a);
44 c["address"] = Value::String(canon_a);
45 }
46 }
47 }
48}
49
50pub const DESC_LIST_PROJECT_PROGRAMS: &str = "\
51List the programs (binaries) in the open Ghidra project as VFS paths (e.g. \"/add.exe\"). Use this \
52first to discover a program_path to pass to attach_program. Results are paginated: pass offset and \
53limit (default 200); the reply's `total` and `truncated` tell you if more remain. Read-only; no \
54program needs to be attached.";
55
56pub const DESC_ATTACH_PROGRAM: &str = "\
57Attach the worker to a program so inspect_function targets it. `program_path` is the absolute VFS path \
58from list_project_programs (must begin with '/', e.g. \"/add.exe\"). One program is attached at a time; \
59attaching switches. On boot a bootstrap program is already attached, so you can inspect_function \
60immediately without calling this first.";
61
62pub const DESC_INSPECT_FUNCTION: &str = "\
63Decompile and describe one function in the attached program. `function` is EITHER a symbol name \
64(e.g. \"main\") OR a canonical address. Addresses use the form space:offset in hex — the default code \
65space renders as a bare 0x value like \"0x00401000\", and other spaces keep their prefix like \
66\"EXTERNAL:0x00000001\". If a name matches multiple functions you get an AMBIGUOUS_SYMBOL error listing \
67each candidate's address — re-call with the disambiguating address. The reply includes the decompiled \
68C (capped from the top by max_c_lines, default 2000; c_truncated=true means the body was longer — raise \
69max_c_lines to see more), the signature, calling convention, thunk info, and callers/callees. A caller \
70or callee with external=true is an imported library function with NO body — identify it by name; do not \
71pass it back to inspect_function. To reach a function you can't name, navigate callers/callees from one \
72you can (e.g. an entry point or export).";
73
74pub const DESC_FIND_FUNCTIONS: &str = "\
75List defined functions in the attached program, optionally narrowed by a `filter` matched against the \
76function name IN the worker (so only matches cross the wire). The filter is a substring match by \
77default; pass regex:true for a regular-expression match (linear-time, length-capped). Returns cheap \
78metadata per hit (name, canonical entry_point, signature, calling_convention, is_thunk, size, external) \
79— NOT the decompiled C or call edges; use inspect_function for those. Paginated: pass offset and limit \
80(default 100); page until has_more is false.";
81
82pub fn canonicalize_functions(canon: &AddressCanonicalizer, v: &mut Value) {
84 if let Some(arr) = v.get_mut("functions").and_then(|x| x.as_array_mut()) {
85 for f in arr {
86 if let Some(a) = f.get("entry_point").and_then(|a| a.as_str()) {
87 let c = canon_addr(canon, a);
88 f["entry_point"] = Value::String(c);
89 }
90 if let Some(a) = f.get("thunked_to").and_then(|a| a.as_str()) {
91 let c = canon_addr(canon, a);
92 f["thunked_to"] = Value::String(c);
93 }
94 }
95 }
96}
97
98pub const DESC_RESOLVE_SYMBOL: &str = "\
99Resolve a symbol NAME to its address(es); enumerates every match; 0 → SYMBOL_NOT_FOUND; call this \
100after an AMBIGUOUS_SYMBOL from inspect_function.";
101
102pub const DESC_LIST_SYMBOLS: &str = "\
103List raw symbol-table entries in the attached program, optionally narrowed by `kind` (import | export | \
104defined | all, default all) and by a `filter` matched against the symbol name IN the worker (so only \
105matches cross the wire). The filter is a substring match by default; pass regex:true for a \
106regular-expression match (linear-time, length-capped). For a function's signature, calling convention, \
107or size, use find_functions instead — this returns raw symbol-table entries, not function metadata. \
108Paginated: pass offset and limit (default 100); page until has_more is false.";
109
110pub const DESC_LIST_STRINGS: &str = "\
111List defined string data in the attached program, optionally narrowed by a `filter` matched against \
112the string value IN the worker (so only matches cross the wire). The filter is a substring match by \
113default; pass regex:true for a regular-expression match (linear-time, length-capped) — e.g. to find \
114URL-shaped strings without pulling every string across the wire, use filter:\"https?://.*\", regex:true. \
115Paginated: pass offset and limit (default 200); page until has_more is false.";
116
117pub fn canonicalize_addr_array(
120 canon: &AddressCanonicalizer,
121 v: &mut Value,
122 array_key: &str,
123 field: &str,
124) {
125 if let Some(arr) = v.get_mut(array_key).and_then(|x| x.as_array_mut()) {
126 for e in arr {
127 if let Some(a) = e.get(field).and_then(|a| a.as_str()) {
128 let c = canon_addr(canon, a);
129 e[field] = Value::String(c);
130 }
131 }
132 }
133}
134
135pub const DESC_GET_XREFS: &str = "\
136List cross-references to or from `target`, a name-or-address selector (any symbol — function, data \
137label, or string, not just functions). `direction` is one of to | from | both (default both): `to` \
138returns references INTO target, `from` returns references OUT of target. If `target` names multiple \
139symbols you get an AMBIGUOUS_SYMBOL error enumerating each candidate's address — re-call with the \
140disambiguating address. Each result row has from_address, to_address, ref_type (e.g. CALL, DATA, READ), \
141and from_function when the reference's source falls inside a defined function. Paginated: pass offset \
142and limit (default 100); page until has_more is false.";
143
144pub const DESC_GET_DISASSEMBLY: &str = "\
145List the disassembled instructions of one function in the attached program. `target` is EITHER a \
146symbol name OR a canonical address, resolved the same way as inspect_function's `function` parameter. \
147If `target` names multiple functions you get an AMBIGUOUS_SYMBOL error enumerating each candidate's \
148address — re-call with the disambiguating address. Each instruction has address, bytes (lowercase hex), \
149mnemonic, operands, and comment when an EOL comment is present. The reply's `total` is the EXACT \
150instruction count of the function's body. Paginated: pass offset and limit (default 200); page until \
151has_more is false.";
152
153pub fn canonicalize_addr_field(canon: &AddressCanonicalizer, v: &mut Value, field: &str) {
156 if let Some(a) = v.get(field).and_then(|a| a.as_str()) {
157 let c = canon_addr(canon, a);
158 v[field] = Value::String(c);
159 }
160}
161
162pub const DESC_READ_BYTES: &str = "\
163Read raw bytes at `address` in the attached program, optionally shifted by a signed DECIMAL byte \
164`offset` (default 0) — the reply's `address` echoes address+offset, canonicalized. `length` is the \
165number of bytes to read (default 256), clamped to a maximum of 4096; the reply's `truncated` flag is \
166true when the clamp applied, and `length` then reflects the actual (clamped) byte count returned. \
167`bytes` is lowercase hex. ADDRESS_NOT_FOUND if address+offset is out of bounds or the memory there is \
168not readable.";
169
170pub const DESC_GET_DATATYPE: &str = "\
171Look up a data type DEFINITION by name (e.g. \"Rect\", \"Color\") in the attached program's data type \
172manager — for defined data INSTANCES/globals use list_data_items instead. Returns `kind` \
173(\"struct\"|\"union\"|\"enum\"|other) and `size`; struct/union kinds also return `members` (name, type, \
174offset, size) and enum kinds return `values` (name, value — value is a STRING, not a number, since \
175Ghidra enum constants can carry full 64-bit values that would lose precision as a bare JSON number). If \
176`name` matches more than one type you get an AMBIGUOUS_DATATYPE error listing each candidate's \
177fully-qualified path in details.candidates — re-call get_datatype with that qualified name.";
178
179pub const DESC_LIST_SEGMENTS: &str = "\
180List the memory map (segments/blocks) of the attached program: name, start/end (canonical addresses), \
181size, and the readable/writable/executable/initialized flags. NOT paginated — segment counts are small. \
182Use this to distinguish code (e.g. .text, executable=true) from data/bss segments before reading bytes \
183or listing data items.";
184
185pub const DESC_LIST_DATA_ITEMS: &str = "\
186List defined data INSTANCES/globals in the attached program (address, name when a listing label is \
187present, data_type, value when cheaply renderable, size) — for type DEFINITIONS (struct/union/enum \
188layouts) use get_datatype instead. Optionally narrowed by a `filter` matched against the item's label IN \
189the worker (so only matches cross the wire). The filter is a substring match by default; pass \
190regex:true for a regular-expression match (linear-time, length-capped). Paginated: pass offset and \
191limit (default 200); page until has_more is false.";
192
193pub const DESC_DESCRIBE_ADDRESS: &str = "\
194address → what is here (dual of resolve_symbol); total — mapped:false for an unmapped address, never \
195an error. `address` is a canonical space:offset string. On a mapped address the reply also carries \
196`segment` (the containing memory block's name), `is_code` (true when an instruction starts there), \
197`containing_function` (name + entry_point, when the address falls inside a defined function), `symbol` \
198(name + kind, the primary symbol at the address, when one exists), and `data_type` (when defined data \
199starts there). Use this to identify an address you got from get_xrefs, read_bytes, or elsewhere without \
200knowing in advance whether it names a function, data, or neither.";
201
202pub const DESC_RENAME: &str = "\
203Rename the primary symbol (function or label) at an address. Compare-and-swap: pass expected_name = the \
204symbol's CURRENT name; the rename applies only if it still matches (else RENAME_CONFLICT with the actual \
205name). If it already equals new_name you get status:already_applied. The edit is saved to disk (durable). \
206new_name must be a bare local name (no namespaces). target is an address or the current name.";
207
208pub const DESC_COMMENT: &str = "\
209Set or clear a comment on the code unit at an address. comment_type selects the channel \
210(eol|pre|post|plate|repeatable); plate is the function/block header. new_comment is the text; an empty \
211string clears it. Idempotent: if the comment already equals new_comment you get status:already_applied. \
212Pass expected_comment ONLY to guard against drift (else STATE_CONFLICT with the actual value); you do not \
213need to echo the current comment on the normal path. The edit is saved to disk (durable). target is an \
214address or a symbol name.";
215
216pub const DESC_SET_DATATYPE: &str = "\
217Apply a named data type to the DATA at an address (retype a global/data location). datatype_name is \
218resolved by name (DATATYPE_NOT_FOUND / AMBIGUOUS_DATATYPE with candidates). The literal \"undefined\" \
219CLEARS the defined data at the address (frees the space). Idempotent: if the location already has an \
220equivalent type you get status:already_applied. Pass expected_datatype ONLY to guard against drift (its \
221value is the current type's display name, or \"undefined\" to assert the location is untyped). Retyping a \
222function PARAMETER is part of set_prototype (whole-signature); retyping a function LOCAL variable is \
223set_local. The edit is saved to \
224disk (durable). target is an address or a symbol name.";
225
226pub const DESC_SET_PROTOTYPE: &str = "\
227Replace a function's whole signature at once: return type, parameters, calling convention, and varargs. \
228Supply the signature as STRUCTURED fields, not a C string. `target` is the function's address or current \
229name. `return_type` and each `params[].type` are resolved by name (DATATYPE_NOT_FOUND / AMBIGUOUS_DATATYPE \
230with candidates); a function-pointer or anonymous type you spell out (e.g. \"void (*)(int)\") is \
231synthesized on the fly. `params` is an ordered list of {name?, type}; an empty list [] means no arguments \
232(do NOT pass [{type:\"void\"}]). `calling_convention` must be one the program recognizes (e.g. __cdecl, \
233__stdcall, __fastcall, or Ghidra's default/unknown) — an unknown one returns INVALID_PARAMS listing the \
234valid set. `variadic:true` adds trailing ... . Read the current signature as these same fields from \
235inspect_function's `prototype`, change what you need, and send the whole thing back. Idempotent: if the \
236signature is already equivalent you get status:already_applied. Pass expected_signature (the current \
237`signature` string from inspect_function) ONLY to guard against drift (else STATE_CONFLICT). Correcting a \
238wrong signature is the highest-leverage edit — it re-derives the decompiled C (fixes in_<reg>/extraout_<reg> \
239noise). A thunk, an external import, or a function with custom variable storage is rejected with \
240INVALID_PARAMS — do not retry it. The edit is saved to disk (durable).";
241
242pub const DESC_SET_LOCAL: &str = "\
243Retype and/or rename ONE local variable of a function (a decompiler body variable like uVar1 or \
244local_18 — NOT a parameter, which is set_prototype's job, and NOT a global, which is set_datatype's). \
245`target` is the function's address or name. `variable` is the local's STORAGE handle (e.g. \"stack:-0x18\", \
246\"EAX:4\") as shown in inspect_function's `locals[]` — prefer it, because the decompiler silently renames \
247unlocked locals across edits; a bare current name also works but is a weaker handle. Supply `new_type` \
248(resolved by name, DATATYPE_NOT_FOUND / AMBIGUOUS_DATATYPE; a spelled-out fn-pointer/anon type is \
249synthesized) and/or `new_name` (bare, no namespaces); at least one is required. Idempotent: if every \
250field you provide already matches you get status:already_applied. Pass expected_type / expected_name ONLY \
251to guard against drift (else STATE_CONFLICT). A too-large or overlapping type, or a name that collides \
252with another local, returns INVALID_PARAMS with the reason — pick a smaller type or a free name. A miss \
253on the storage/name handle returns VARIABLE_NOT_FOUND (re-read locals[]). The edit is saved to disk \
254(durable), and re-derives the decompiled C. Read locals from inspect_function, change one, send it back.";
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use ghidra_ipc::protocol::{CallEdge, CommentEntry, DataRef};
260
261 fn canon() -> AddressCanonicalizer {
262 AddressCanonicalizer::new("ram", 4)
263 }
264
265 #[test]
266 fn canonicalizes_all_addresses_in_context() {
267 let fc = FunctionContext {
268 name: "f".into(),
269 entry_point: "ram:401000".into(),
270 signature: "s".into(),
271 calling_convention: "__cdecl".into(),
272 is_thunk: true,
273 thunked_to: Some("ram:401100".into()),
274 c: "…".into(),
275 c_truncated: false,
276 callers: vec![CallEdge {
277 name: "m".into(),
278 address: "ram:401040".into(),
279 external: false,
280 }],
281 callees: vec![CallEdge {
282 name: "X".into(),
283 address: "EXTERNAL:1".into(),
284 external: true,
285 }],
286 data_refs: vec![DataRef {
287 address: "ram:403000".into(),
288 name: Some("g_banner".into()),
289 ref_type: "DATA".into(),
290 }],
291 data_refs_truncated: false,
292 comments: vec![CommentEntry {
293 address: "ram:404000".into(),
294 kind: "plate".into(),
295 comment: "hdr".into(),
296 }],
297 comments_truncated: false,
298 prototype: None,
299 locals: None,
300 locals_truncated: None,
301 };
302 let out = canonicalize_function_context(&canon(), fc);
303 assert_eq!(out.entry_point, "0x00401000");
304 assert_eq!(out.thunked_to.unwrap(), "0x00401100");
305 assert_eq!(out.callers[0].address, "0x00401040");
306 assert_eq!(out.callees[0].address, "EXTERNAL:0x00000001");
307 assert_eq!(out.data_refs[0].address, "0x00403000");
308 assert_eq!(out.comments[0].address, "0x00404000");
309 }
310}