sqry_daemon/ipc/
validation.rs1use serde_json::json;
12use thiserror::Error;
13
14use super::protocol::{JsonRpcRequest, JsonRpcResponse};
15
16#[derive(Debug, Error)]
18pub enum ValidationError {
19 #[error("parse error: {0}")]
23 ParseError(#[from] serde_json::Error),
24
25 #[error("invalid request: {reason}")]
28 InvalidRequest {
29 reason: &'static str,
30 context: Option<String>,
31 },
32}
33
34impl ValidationError {
35 #[must_use]
39 pub fn is_terminal(&self) -> bool {
40 matches!(self, Self::ParseError(_))
41 }
42
43 #[must_use]
46 pub fn into_jsonrpc_response(self) -> JsonRpcResponse {
47 match self {
48 Self::ParseError(e) => JsonRpcResponse::error(
49 None,
50 -32700,
51 "Parse error",
52 Some(json!({ "reason": e.to_string() })),
53 ),
54 Self::InvalidRequest { reason, context } => {
55 let mut data = json!({ "reason": reason });
56 if let Some(ctx) = context {
57 data["context"] = serde_json::Value::String(ctx);
58 }
59 JsonRpcResponse::error(None, -32600, "Invalid Request", Some(data))
60 }
61 }
62 }
63}
64
65pub fn validate_request_value(value: serde_json::Value) -> Result<JsonRpcRequest, ValidationError> {
80 let serde_json::Value::Object(obj) = &value else {
81 return Err(ValidationError::InvalidRequest {
82 reason: "request must be an object",
83 context: None,
84 });
85 };
86
87 match obj.get("jsonrpc").and_then(|v| v.as_str()) {
88 Some("2.0") => {}
89 Some(other) => {
90 return Err(ValidationError::InvalidRequest {
91 reason: "jsonrpc must be exactly \"2.0\"",
92 context: Some(other.to_owned()),
93 });
94 }
95 None => {
96 return Err(ValidationError::InvalidRequest {
97 reason: "missing jsonrpc field",
98 context: None,
99 });
100 }
101 }
102
103 match obj.get("method").and_then(|v| v.as_str()) {
104 Some(m) if !m.is_empty() => {}
105 Some(_) => {
106 return Err(ValidationError::InvalidRequest {
107 reason: "method must be non-empty",
108 context: None,
109 });
110 }
111 None => {
112 return Err(ValidationError::InvalidRequest {
113 reason: "missing method field",
114 context: None,
115 });
116 }
117 }
118
119 if let Some(id) = obj.get("id") {
120 let ok = id.is_null() || id.is_string() || id.is_i64() || id.is_u64();
121 if !ok {
122 return Err(ValidationError::InvalidRequest {
123 reason: "id must be null, string, or an integer number \
124 (no fractional parts, no exponent form)",
125 context: Some(id.to_string()),
126 });
127 }
128 }
129
130 if let Some(params) = obj.get("params")
131 && !(params.is_object() || params.is_array() || params.is_null())
132 {
133 return Err(ValidationError::InvalidRequest {
134 reason: "params must be object, array, or null",
135 context: None,
136 });
137 }
138
139 let req: JsonRpcRequest =
140 serde_json::from_value(value).map_err(|e| ValidationError::InvalidRequest {
141 reason: "request failed schema decode after validation",
142 context: Some(e.to_string()),
143 })?;
144 Ok(req)
145}
146
147#[must_use]
150pub fn parse_error_response(err: serde_json::Error) -> JsonRpcResponse {
151 ValidationError::ParseError(err).into_jsonrpc_response()
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 fn err_code(resp: &JsonRpcResponse) -> i32 {
162 match &resp.payload {
163 super::super::protocol::JsonRpcPayload::Error { error } => error.code,
164 super::super::protocol::JsonRpcPayload::Success { .. } => panic!("not an error"),
165 }
166 }
167
168 #[test]
169 fn valid_request_roundtrips() {
170 let v = serde_json::json!({
171 "jsonrpc": "2.0",
172 "id": 7,
173 "method": "daemon/status",
174 "params": {},
175 });
176 let req = validate_request_value(v).unwrap();
177 assert_eq!(req.method, "daemon/status");
178 }
179
180 #[test]
181 fn missing_jsonrpc_rejected() {
182 let v = serde_json::json!({
183 "id": 1,
184 "method": "x",
185 });
186 let e = validate_request_value(v).unwrap_err();
187 let resp = e.into_jsonrpc_response();
188 assert_eq!(err_code(&resp), -32600);
189 }
190
191 #[test]
192 fn wrong_jsonrpc_version_rejected() {
193 let v = serde_json::json!({
194 "jsonrpc": "1.0",
195 "id": 1,
196 "method": "x",
197 });
198 let resp = validate_request_value(v)
199 .unwrap_err()
200 .into_jsonrpc_response();
201 assert_eq!(err_code(&resp), -32600);
202 }
203
204 #[test]
205 fn missing_method_rejected() {
206 let v = serde_json::json!({"jsonrpc": "2.0", "id": 1});
207 let resp = validate_request_value(v)
208 .unwrap_err()
209 .into_jsonrpc_response();
210 assert_eq!(err_code(&resp), -32600);
211 }
212
213 #[test]
214 fn empty_method_rejected() {
215 let v = serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": ""});
216 let resp = validate_request_value(v)
217 .unwrap_err()
218 .into_jsonrpc_response();
219 assert_eq!(err_code(&resp), -32600);
220 }
221
222 #[test]
223 fn non_object_root_rejected() {
224 let v = serde_json::json!("not an object");
225 let resp = validate_request_value(v)
226 .unwrap_err()
227 .into_jsonrpc_response();
228 assert_eq!(err_code(&resp), -32600);
229 }
230
231 #[test]
232 fn numeric_id_shape_matrix() {
233 for v in [
235 serde_json::json!(0_i64),
236 serde_json::json!(1_i64),
237 serde_json::json!(-1_i64),
238 serde_json::json!(i64::MAX),
239 serde_json::json!(u64::MAX),
240 serde_json::json!("abc"),
241 serde_json::Value::Null,
242 ] {
243 let req = serde_json::json!({
244 "jsonrpc": "2.0",
245 "id": v,
246 "method": "x",
247 });
248 validate_request_value(req).expect("valid id shape must pass");
249 }
250 let fractional = serde_json::Number::from_f64(1.5).unwrap();
252 let rejected: &[serde_json::Value] = &[
253 serde_json::Value::Number(fractional.clone()),
254 serde_json::from_str(r#"1e3"#).unwrap(),
255 serde_json::from_str(r#"42.0E0"#).unwrap(),
256 serde_json::json!(true),
257 serde_json::json!({}),
258 serde_json::json!([]),
259 ];
260 for v in rejected {
261 let req = serde_json::json!({
262 "jsonrpc": "2.0",
263 "id": v,
264 "method": "x",
265 });
266 let resp = validate_request_value(req)
267 .unwrap_err()
268 .into_jsonrpc_response();
269 assert_eq!(err_code(&resp), -32600, "id shape {v:?} should be -32600");
270 }
271 }
272
273 #[test]
274 fn params_must_be_object_array_or_null() {
275 let req = serde_json::json!({
276 "jsonrpc": "2.0",
277 "id": 1,
278 "method": "x",
279 "params": "not-an-object",
280 });
281 let resp = validate_request_value(req)
282 .unwrap_err()
283 .into_jsonrpc_response();
284 assert_eq!(err_code(&resp), -32600);
285 }
286
287 #[test]
288 fn parse_error_response_has_id_null_and_32700() {
289 let bad = b"{not valid";
290 let err = serde_json::from_slice::<serde_json::Value>(bad).unwrap_err();
291 let resp = parse_error_response(err);
292 assert_eq!(err_code(&resp), -32700);
293 assert!(resp.id.is_none());
294 }
295}