1use serde::{Deserialize, Serialize};
4
5pub const PROTOCOL_VERSION: u32 = 1;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(untagged)]
15pub enum RpcId {
16 Null,
17 Number(i64),
18 String(String),
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct RpcRequest {
24 pub jsonrpc: JsonRpcVersion,
25 pub id: RpcId,
26 pub method: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub params: Option<serde_json::Value>,
29}
30
31#[derive(Debug, Clone, Serialize)]
42#[serde(untagged)]
43pub enum RpcResponse {
44 Success {
45 jsonrpc: JsonRpcVersion,
46 id: RpcId,
47 result: serde_json::Value,
48 },
49 Failure {
50 jsonrpc: JsonRpcVersion,
51 id: RpcId,
52 error: serde_json::Value,
53 },
54}
55
56impl<'de> Deserialize<'de> for RpcResponse {
57 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
58 let value = serde_json::Value::deserialize(d)?;
65 let obj = value
66 .as_object()
67 .ok_or_else(|| serde::de::Error::custom("JSON-RPC response must be a JSON object"))?;
68 for key in obj.keys() {
69 if !matches!(key.as_str(), "jsonrpc" | "id" | "result" | "error") {
70 return Err(serde::de::Error::custom(format!(
71 "unexpected field `{key}` in JSON-RPC response"
72 )));
73 }
74 }
75 let jsonrpc: JsonRpcVersion = obj
76 .get("jsonrpc")
77 .ok_or_else(|| serde::de::Error::custom("JSON-RPC response missing `jsonrpc`"))
78 .and_then(|v| serde_json::from_value(v.clone()).map_err(serde::de::Error::custom))?;
79 let id: RpcId = obj
80 .get("id")
81 .ok_or_else(|| serde::de::Error::custom("JSON-RPC response missing `id`"))
82 .and_then(|v| serde_json::from_value(v.clone()).map_err(serde::de::Error::custom))?;
83 match (obj.contains_key("result"), obj.contains_key("error")) {
84 (true, false) => Ok(RpcResponse::Success {
85 jsonrpc,
86 id,
87 result: obj["result"].clone(),
88 }),
89 (false, true) => Ok(RpcResponse::Failure {
90 jsonrpc,
91 id,
92 error: obj["error"].clone(),
93 }),
94 (true, true) => Err(serde::de::Error::custom(
95 "JSON-RPC response has both `result` and `error`; exactly one is required",
96 )),
97 (false, false) => Err(serde::de::Error::custom(
98 "JSON-RPC response has neither `result` nor `error`; exactly one is required",
99 )),
100 }
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct RpcNotification {
111 pub jsonrpc: JsonRpcVersion,
112 pub method: String,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub params: Option<serde_json::Value>,
115}
116
117pub const INIT_METHOD: &str = "worker/init";
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub struct JsonRpcVersion;
123
124impl Serialize for JsonRpcVersion {
125 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
126 s.serialize_str("2.0")
127 }
128}
129
130impl<'de> Deserialize<'de> for JsonRpcVersion {
131 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
132 let v = String::deserialize(d)?;
133 if v == "2.0" {
134 Ok(JsonRpcVersion)
135 } else {
136 Err(serde::de::Error::custom(format!(
137 "unsupported jsonrpc version: {v}"
138 )))
139 }
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147pub struct InitAnnounce {
148 pub protocol_version: u32,
149 pub ghidra_version: String,
150 pub worker_script_version: String,
151 pub token: String,
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn request_renders_jsonrpc_version_and_omits_absent_params() {
160 let req = RpcRequest {
161 jsonrpc: JsonRpcVersion,
162 id: RpcId::Number(7),
163 method: "decompile_function".to_string(),
164 params: None,
165 };
166 let json = serde_json::to_value(&req).unwrap();
167 assert_eq!(json["jsonrpc"], "2.0");
168 assert_eq!(json["id"], 7);
169 assert_eq!(json["method"], "decompile_function");
170 assert!(json.get("params").is_none());
171 }
172
173 #[test]
174 fn id_accepts_string_and_number() {
175 let s: RpcId = serde_json::from_str("\"abc\"").unwrap();
176 let n: RpcId = serde_json::from_str("42").unwrap();
177 assert_eq!(s, RpcId::String("abc".to_string()));
178 assert_eq!(n, RpcId::Number(42));
179 }
180
181 #[test]
182 fn rejects_wrong_jsonrpc_version() {
183 let bad = r#"{"jsonrpc":"1.0","id":1,"method":"x"}"#;
184 assert!(serde_json::from_str::<RpcRequest>(bad).is_err());
185 }
186
187 #[test]
188 fn init_announce_roundtrips_and_version_mismatch_is_detectable() {
189 let ann = InitAnnounce {
190 protocol_version: PROTOCOL_VERSION,
191 ghidra_version: "12.1.2".to_string(),
192 worker_script_version: "0.0.0".to_string(),
193 token: "0123456789abcdef0123456789abcdef".to_string(),
194 };
195 let s = serde_json::to_string(&ann).unwrap();
196 let back: InitAnnounce = serde_json::from_str(&s).unwrap();
197 assert_eq!(back, ann);
198 assert_eq!(back.protocol_version, PROTOCOL_VERSION);
199 assert_eq!(back.token, "0123456789abcdef0123456789abcdef");
200
201 let skewed = InitAnnounce {
202 protocol_version: PROTOCOL_VERSION + 1,
203 ..ann
204 };
205 assert_ne!(skewed.protocol_version, PROTOCOL_VERSION);
206 }
207
208 #[test]
209 fn response_is_result_xor_error_both_ways() {
210 let ok = RpcResponse::Success {
211 jsonrpc: JsonRpcVersion,
212 id: RpcId::Number(1),
213 result: serde_json::json!({"ok": true}),
214 };
215 let jv = serde_json::to_value(&ok).unwrap();
216 assert_eq!(jv["result"]["ok"], true);
217 assert!(jv.get("error").is_none());
218
219 let err = RpcResponse::Failure {
220 jsonrpc: JsonRpcVersion,
221 id: RpcId::Number(1),
222 error: serde_json::json!({"code": "X"}),
223 };
224 let jv = serde_json::to_value(&err).unwrap();
225 assert_eq!(jv["error"]["code"], "X");
226 assert!(jv.get("result").is_none());
227
228 let back_ok: RpcResponse = serde_json::from_value(serde_json::json!({
230 "jsonrpc": "2.0", "id": 1, "result": {"ok": true}
231 }))
232 .unwrap();
233 assert!(matches!(back_ok, RpcResponse::Success { .. }));
234 let back_err: RpcResponse = serde_json::from_value(serde_json::json!({
235 "jsonrpc": "2.0", "id": 1, "error": {"code": "X"}
236 }))
237 .unwrap();
238 assert!(matches!(back_err, RpcResponse::Failure { .. }));
239
240 let back_big: RpcResponse = serde_json::from_value(serde_json::json!({
243 "jsonrpc": "2.0", "id": 1, "result": {"n": 9_007_199_254_740_993_u64}
244 }))
245 .unwrap();
246 match back_big {
247 RpcResponse::Success { result, .. } => {
248 assert_eq!(result["n"].as_u64(), Some(9_007_199_254_740_993));
249 }
250 RpcResponse::Failure { .. } => panic!("expected Success"),
251 }
252 }
253
254 #[test]
255 fn response_rejects_both_result_and_error() {
256 let both = serde_json::json!({
260 "jsonrpc": "2.0", "id": 1, "result": {"ok": true}, "error": {"code": "BOOM"}
261 });
262 assert!(
263 serde_json::from_value::<RpcResponse>(both).is_err(),
264 "a response with BOTH result and error must be rejected"
265 );
266
267 let neither = serde_json::json!({ "jsonrpc": "2.0", "id": 1 });
269 assert!(
270 serde_json::from_value::<RpcResponse>(neither).is_err(),
271 "a response with NEITHER result nor error must be rejected"
272 );
273 }
274
275 #[test]
276 fn response_accepts_null_result_and_null_id() {
277 let null_result: RpcResponse = serde_json::from_value(serde_json::json!({
281 "jsonrpc": "2.0", "id": 1, "result": null
282 }))
283 .unwrap();
284 match null_result {
285 RpcResponse::Success { result, .. } => assert!(result.is_null()),
286 RpcResponse::Failure { .. } => panic!("`result: null` must deserialize as Success"),
287 }
288
289 let null_id: RpcResponse = serde_json::from_value(serde_json::json!({
292 "jsonrpc": "2.0", "id": null, "error": {"code": -32700, "message": "Parse error"}
293 }))
294 .unwrap();
295 match null_id {
296 RpcResponse::Failure { id, error, .. } => {
297 assert_eq!(id, RpcId::Null);
298 assert_eq!(error["code"], -32700);
299 }
300 RpcResponse::Success { .. } => panic!("expected Failure"),
301 }
302
303 let extra = serde_json::json!({ "jsonrpc": "2.0", "id": 1, "result": {}, "surprise": 9 });
305 assert!(
306 serde_json::from_value::<RpcResponse>(extra).is_err(),
307 "an unknown top-level field must be rejected"
308 );
309 }
310
311 #[test]
312 fn rpc_id_null_roundtrips() {
313 assert!(serde_json::to_value(RpcId::Null).unwrap().is_null());
314 let back: RpcId = serde_json::from_value(serde_json::json!(null)).unwrap();
315 assert_eq!(back, RpcId::Null);
316 }
317
318 #[test]
319 fn notification_without_params_roundtrips() {
320 let note = RpcNotification {
321 jsonrpc: JsonRpcVersion,
322 method: "worker/ping".to_string(),
323 params: None,
324 };
325 let s = serde_json::to_string(¬e).unwrap();
326 assert!(!s.contains("params")); let back: RpcNotification =
329 serde_json::from_str(r#"{"jsonrpc":"2.0","method":"worker/ping"}"#).unwrap();
330 assert_eq!(back.method, "worker/ping");
331 assert!(back.params.is_none());
332 }
333
334 #[test]
335 fn init_is_a_worker_init_notification() {
336 let ann = InitAnnounce {
337 protocol_version: PROTOCOL_VERSION,
338 ghidra_version: "12.1.2".to_string(),
339 worker_script_version: "0.0.0".to_string(),
340 token: "t".to_string(),
341 };
342 let note = RpcNotification {
343 jsonrpc: JsonRpcVersion,
344 method: INIT_METHOD.to_string(),
345 params: Some(serde_json::to_value(&ann).unwrap()),
346 };
347 let jv = serde_json::to_value(¬e).unwrap();
348 assert_eq!(jv["jsonrpc"], "2.0");
349 assert_eq!(jv["method"], "worker/init");
350 let parsed: RpcNotification = serde_json::from_value(jv).unwrap();
353 assert_eq!(parsed.method, INIT_METHOD);
354 let back: InitAnnounce = serde_json::from_value(parsed.params.unwrap()).unwrap();
355 assert_eq!(back, ann);
356 }
357}