Skip to main content

jmap_server/
parse.rs

1//! Request parsing and ResultReference resolution (RFC 8620 §3.3, §3.7).
2
3use crate::{Invocation, JmapError, JmapRequest, ResultReference};
4use serde_json::Value;
5
6/// Parse and validate a JMAP request from a raw JSON value.
7///
8/// Validates:
9/// - The body deserializes as a [`JmapRequest`].
10/// - `using` is non-empty (RFC 8620 §3.3).
11/// - The number of method calls does not exceed `max_calls` (RFC 8620 §3.3).
12///
13/// Capability URI checking is NOT performed here — that is the caller's
14/// responsibility.
15///
16/// # Caller responsibility: `notJSON`
17///
18/// This function takes a pre-parsed [`serde_json::Value`], not raw bytes.  The
19/// caller is responsible for the initial JSON parse of the HTTP request body.
20/// If that parse fails (the body is not valid JSON), the caller must produce the
21/// `notJSON` error response itself — [`crate::error_invocation`] and
22/// [`crate::request_error`] with [`JmapError::not_json()`] handle that case.
23/// `parse_request` only validates the JMAP structure of an already-parsed value.
24///
25/// # Errors
26///
27/// Returns [`JmapError::not_request()`] if the value does not match the
28/// `JmapRequest` schema or if `using` is empty.  Returns
29/// [`JmapError::limit("maxCallsInRequest")`][JmapError::limit] if the method
30/// call count exceeds `max_calls`.
31pub fn parse_request(body: Value, max_calls: usize) -> Result<JmapRequest, JmapError> {
32    let req: JmapRequest = serde_json::from_value(body).map_err(|_| JmapError::not_request())?;
33
34    if req.using.is_empty() {
35        return Err(JmapError::not_request());
36    }
37
38    if req.method_calls.len() > max_calls {
39        return Err(JmapError::limit("maxCallsInRequest"));
40    }
41
42    Ok(req)
43}
44
45/// Resolve all `#key` ResultReference fields in `args` against `prior_responses`.
46///
47/// For every key in `args` that starts with `#`:
48/// 1. Parse the value as a [`ResultReference`].
49/// 2. Find the prior response whose call-id matches `rr.result_of` (index 2 of tuple).
50/// 3. Verify `rr.name` matches the method name of that response (index 0 of tuple).
51/// 4. Apply `rr.path` as an RFC 6901 JSON Pointer (with RFC 8620 §3.7 `*` extension)
52///    to the response args (index 1 of tuple).
53/// 5. Collect `(plain_key, resolved_value)` pairs.
54///
55/// This is two-phase atomic: `args` is not modified at all unless every
56/// `#key` resolves successfully.  If any resolution fails, `args` is returned
57/// unchanged and an error is returned.
58///
59/// `prior_responses` entries are `(method_name, response_args, call_id)` — same
60/// layout as [`Invocation`].
61pub fn resolve_args(args: &mut Value, prior_responses: &[Invocation]) -> Result<(), JmapError> {
62    let Some(obj) = args.as_object_mut() else {
63        return Ok(()); // non-object args cannot contain #-key references
64    };
65
66    // Collect (#key, value) pairs up front; cannot borrow obj mutably while iterating.
67    // obj.len() is an upper bound (not all keys need the # prefix).
68    let mut ref_pairs: Vec<(String, Value)> = Vec::with_capacity(obj.len());
69    ref_pairs.extend(
70        obj.iter()
71            .filter(|(k, _)| k.starts_with('#'))
72            .map(|(k, v)| (k.clone(), v.clone())),
73    );
74
75    if ref_pairs.is_empty() {
76        return Ok(());
77    }
78
79    // Phase 1: resolve every reference read-only; args are not touched yet.
80    // If any step fails, return the error immediately without modifying args.
81    let mut resolutions: Vec<(String, String, Value)> = Vec::with_capacity(ref_pairs.len());
82
83    for (ref_key, ref_value) in ref_pairs {
84        let plain_key = ref_key[1..].to_owned();
85
86        // Parse the value as a ResultReference.
87        let rr: ResultReference = serde_json::from_value(ref_value).map_err(|e| {
88            JmapError::invalid_arguments(format!("invalid ResultReference for #{plain_key}: {e}"))
89        })?;
90
91        // Find the prior response by call-id (index 2 of the Invocation tuple).
92        let (prior_method, prior_value) = prior_responses
93            .iter()
94            .find(|(_, _, call_id)| call_id == &rr.result_of)
95            .map(|(method, value, _)| (method.as_str(), value))
96            .ok_or_else(JmapError::invalid_result_reference)?;
97
98        // Verify the name field matches the method name (RFC 8620 §3.7).
99        if rr.name != prior_method {
100            return Err(JmapError::invalid_result_reference());
101        }
102
103        // Apply the RFC 6901 JSON Pointer path with RFC 8620 §3.7 `*` wildcard.
104        let resolved = json_pointer_ext(prior_value, &rr.path)
105            .ok_or_else(JmapError::invalid_result_reference)?;
106
107        // Check for key conflict: plain_key must not already exist in args.
108        if obj.contains_key(&plain_key) {
109            return Err(JmapError::invalid_arguments(format!(
110                "argument key conflict: '{}' and '#{}' both present",
111                plain_key, plain_key
112            )));
113        }
114
115        resolutions.push((ref_key, plain_key, resolved));
116    }
117
118    // Phase 2: all resolutions succeeded — apply mutations atomically.
119    for (ref_key, plain_key, resolved) in resolutions {
120        obj.remove(&ref_key);
121        obj.insert(plain_key, resolved);
122    }
123
124    Ok(())
125}
126
127/// Apply a path to a JSON value, supporting the RFC 8620 §3.7 `*` wildcard extension.
128///
129/// This is RFC 6901 JSON Pointer extended with `*` as an array-map operator.
130/// When the current value is an array and the token is `*`, the remaining tokens
131/// are applied to each element; array results are flattened into the output.
132fn json_pointer_ext(value: &Value, path: &str) -> Option<Value> {
133    if path.is_empty() {
134        return Some(value.clone());
135    }
136    if !path.starts_with('/') {
137        return None;
138    }
139
140    // Split off the first token.
141    let after_slash = &path[1..];
142    let (token, remaining) = match after_slash.find('/') {
143        Some(pos) => (&after_slash[..pos], &after_slash[pos..]),
144        None => (after_slash, ""),
145    };
146
147    if token == "*" {
148        // RFC 8620 §3.7 wildcard: map over array, flatten array results.
149        let arr = value.as_array()?;
150        let mut result: Vec<Value> = Vec::new();
151        for item in arr {
152            match json_pointer_ext(item, remaining) {
153                Some(Value::Array(inner)) => result.extend(inner),
154                Some(other) => result.push(other),
155                None => return None, // any failure = whole resolution fails
156            }
157        }
158        Some(Value::Array(result))
159    } else {
160        // RFC 6901: unescape ~1 → /, ~0 → ~ (in that order).
161        // Skip allocation when the token contains no ~ characters (common case).
162        let key: std::borrow::Cow<str> = if token.contains('~') {
163            token.replace("~1", "/").replace("~0", "~").into()
164        } else {
165            token.into()
166        };
167        let next = match value {
168            Value::Object(obj) => obj.get(key.as_ref())?,
169            Value::Array(arr) => {
170                // RFC 6901 §4: leading zeros are not allowed in array index tokens.
171                if key.len() > 1 && key.starts_with('0') {
172                    return None;
173                }
174                let idx: usize = key.parse().ok()?;
175                arr.get(idx)?
176            }
177            _ => return None,
178        };
179        json_pointer_ext(next, remaining)
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use serde_json::json;
187
188    // Oracle: RFC 8620 §3 (request format), §7.1 (error type strings).
189
190    #[test]
191    fn parse_request_valid() {
192        let body = json!({
193            "using": ["urn:ietf:params:jmap:core"],
194            "methodCalls": [
195                ["Foo/get", {"accountId": "a1"}, "0"]
196            ]
197        });
198        let req = parse_request(body, 16).expect("valid request must parse");
199        assert_eq!(req.using, vec!["urn:ietf:params:jmap:core"]);
200        assert_eq!(req.method_calls.len(), 1);
201    }
202
203    #[test]
204    fn parse_request_empty_using_is_error() {
205        let body = json!({
206            "using": [],
207            "methodCalls": []
208        });
209        let err = parse_request(body, 16).unwrap_err();
210        assert_eq!(
211            err.error_type, "notRequest",
212            "empty using violates request structure — must be notRequest per RFC 8620 §3.6.1"
213        );
214    }
215
216    #[test]
217    fn parse_request_too_many_calls() {
218        let call = json!(["Foo/get", {}, "0"]);
219        let calls: Vec<_> = (0..5).map(|_| call.clone()).collect();
220        let body = json!({
221            "using": ["urn:ietf:params:jmap:core"],
222            "methodCalls": calls
223        });
224        let err = parse_request(body, 4).unwrap_err();
225        assert_eq!(
226            err.error_type, "limit",
227            "exceeding maxCallsInRequest must return limit per RFC 8620 §3.6.1"
228        );
229    }
230
231    #[test]
232    fn parse_request_at_max_calls_is_ok() {
233        let call = json!(["Foo/get", {}, "0"]);
234        let calls: Vec<_> = (0..4).map(|_| call.clone()).collect();
235        let body = json!({
236            "using": ["urn:ietf:params:jmap:core"],
237            "methodCalls": calls
238        });
239        parse_request(body, 4).expect("exactly max_calls must be accepted");
240    }
241
242    #[test]
243    fn parse_request_malformed_body() {
244        let body = json!("not an object");
245        let err = parse_request(body, 16).unwrap_err();
246        assert_eq!(
247            err.error_type, "notRequest",
248            "malformed body does not match Request type — must be notRequest per RFC 8620 §3.6.1"
249        );
250    }
251
252    // Oracle: RFC 8620 §3.7 — #ids resolves to prior response's value at path.
253    #[test]
254    fn resolve_args_basic() {
255        let prior = vec![(
256            "Foo/get".to_owned(),
257            json!({"list": [{"id": "x1"}], "state": "s0"}),
258            "c0".to_owned(),
259        )];
260        let mut args = json!({
261            "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/list/0/id"}
262        });
263        resolve_args(&mut args, &prior).expect("must resolve");
264        assert_eq!(args, json!({"ids": "x1"}));
265    }
266
267    // Oracle: RFC 8620 §3.7 — unknown resultOf → invalidResultReference.
268    #[test]
269    fn resolve_args_unknown_result_of() {
270        let prior: Vec<Invocation> = vec![];
271        let mut args = json!({
272            "#ids": {"resultOf": "missing", "name": "Foo/get", "path": "/ids"}
273        });
274        let original = args.clone();
275        let err = resolve_args(&mut args, &prior).unwrap_err();
276        assert_eq!(err.error_type, "invalidResultReference");
277        // args must be unchanged on error (atomicity).
278        assert_eq!(args, original);
279    }
280
281    // Oracle: RFC 8620 §3.7 — name mismatch → invalidResultReference.
282    #[test]
283    fn resolve_args_name_mismatch() {
284        let prior = vec![("Foo/get".to_owned(), json!({"ids": ["a"]}), "c0".to_owned())];
285        let mut args = json!({
286            "#ids": {"resultOf": "c0", "name": "Bar/get", "path": "/ids"}
287        });
288        let original = args.clone();
289        let err = resolve_args(&mut args, &prior).unwrap_err();
290        assert_eq!(err.error_type, "invalidResultReference");
291        assert_eq!(args, original);
292    }
293
294    // Oracle: RFC 8620 §3.7 — path not found → invalidResultReference.
295    #[test]
296    fn resolve_args_path_not_found() {
297        let prior = vec![("Foo/get".to_owned(), json!({"ids": ["a"]}), "c0".to_owned())];
298        let mut args = json!({
299            "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/nonexistent"}
300        });
301        let original = args.clone();
302        let err = resolve_args(&mut args, &prior).unwrap_err();
303        assert_eq!(err.error_type, "invalidResultReference");
304        assert_eq!(args, original);
305    }
306
307    // Oracle: atomicity — if one of two refs fails, args must be completely unchanged.
308    #[test]
309    fn resolve_args_atomic_on_partial_failure() {
310        let prior = vec![(
311            "Foo/get".to_owned(),
312            json!({"ids": ["a", "b"]}),
313            "c0".to_owned(),
314        )];
315        // #ids is valid; #properties references a non-existent call.
316        let mut args = json!({
317            "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/ids"},
318            "#properties": {"resultOf": "missing", "name": "Foo/get", "path": "/props"}
319        });
320        let original = args.clone();
321        let err = resolve_args(&mut args, &prior).unwrap_err();
322        assert_eq!(err.error_type, "invalidResultReference");
323        assert_eq!(args, original);
324    }
325
326    // Oracle: non-object args pass through unchanged.
327    #[test]
328    fn resolve_args_non_object_passthrough() {
329        let prior: Vec<Invocation> = vec![];
330        let mut args = json!("not-an-object");
331        resolve_args(&mut args, &prior).expect("non-object must not error");
332        assert_eq!(args, json!("not-an-object"));
333    }
334
335    // Oracle: no #-prefixed keys → args unchanged, Ok returned.
336    #[test]
337    fn resolve_args_no_ref_keys() {
338        let prior: Vec<Invocation> = vec![];
339        let mut args = json!({"ids": ["a", "b"]});
340        resolve_args(&mut args, &prior).expect("no ref keys must not error");
341        assert_eq!(args, json!({"ids": ["a", "b"]}));
342    }
343
344    // Oracle: kith-jmap deviation — unknown capability URIs are silently accepted
345    // at this layer; capability checking is the caller's responsibility.
346    #[test]
347    fn parse_request_unknown_capability_accepted() {
348        let body = json!({
349            "using": ["urn:ietf:params:jmap:core", "urn:example:unknown"],
350            "methodCalls": [
351                ["Foo/get", {}, "0"]
352            ]
353        });
354        let req = parse_request(body, 16).expect("unknown capability must be accepted");
355        assert_eq!(req.using.len(), 2);
356    }
357
358    // Oracle: RFC 8620 §3.3 — `using` is valid with any non-empty array.
359    #[test]
360    fn parse_request_core_only_accepted() {
361        let body = json!({
362            "using": ["urn:ietf:params:jmap:core"],
363            "methodCalls": [
364                ["Foo/get", {}, "0"]
365            ]
366        });
367        parse_request(body, 16).expect("core-only using must be accepted");
368    }
369
370    // Oracle: boundary condition — max_calls=0, one call → limit (RFC 8620 §3.6.1).
371    #[test]
372    fn parse_request_zero_max_calls_rejects_any_call() {
373        let body = json!({
374            "using": ["urn:ietf:params:jmap:core"],
375            "methodCalls": [
376                ["Foo/get", {}, "0"]
377            ]
378        });
379        let err = parse_request(body, 0).unwrap_err();
380        assert_eq!(
381            err.error_type, "limit",
382            "zero max_calls means any call exceeds limit — must be limit per RFC 8620 §3.6.1"
383        );
384    }
385
386    // Oracle: RFC 8620 §3.7 — multiple #-keys in the same args object all resolve
387    // independently against the same prior response.
388    #[test]
389    fn resolve_args_multiple_refs_all_resolve() {
390        let prior = vec![(
391            "Foo/get".to_owned(),
392            json!({"list": [{"id": "x1"}], "state": "s0"}),
393            "c0".to_owned(),
394        )];
395        let mut args = json!({
396            "#ids":   {"resultOf": "c0", "name": "Foo/get", "path": "/list"},
397            "#state": {"resultOf": "c0", "name": "Foo/get", "path": "/state"}
398        });
399        resolve_args(&mut args, &prior).expect("both refs must resolve");
400        // No #-keys must remain.
401        let obj = args.as_object().expect("must still be an object");
402        assert!(!obj.contains_key("#ids"), "#ids must be removed");
403        assert!(!obj.contains_key("#state"), "#state must be removed");
404        assert_eq!(args["ids"], json!([{"id": "x1"}]));
405        assert_eq!(args["state"], json!("s0"));
406    }
407
408    // Oracle: RFC 8620 §3.7 — having both `key` and `#key` in the same args
409    // object is an error (key conflict).
410    #[test]
411    fn resolve_args_key_conflict_is_error() {
412        let prior = vec![("Foo/get".to_owned(), json!({"ids": ["a"]}), "c0".to_owned())];
413        let mut args = json!({
414            "ids":  "existing",
415            "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/ids"}
416        });
417        let original = args.clone();
418        let err = resolve_args(&mut args, &prior).unwrap_err();
419        assert_eq!(err.error_type, "invalidArguments");
420        // args must be completely unchanged on error (atomicity).
421        assert_eq!(args, original);
422    }
423
424    // Oracle: RFC 8620 §3.7 — `#key` value must be a valid ResultReference object;
425    // a non-object value is rejected with invalidArguments.
426    #[test]
427    fn resolve_args_invalid_ref_value_is_error() {
428        let prior: Vec<Invocation> = vec![];
429        let mut args = json!({"#ids": "not-an-object"});
430        let original = args.clone();
431        let err = resolve_args(&mut args, &prior).unwrap_err();
432        assert_eq!(err.error_type, "invalidArguments");
433        assert_eq!(args, original);
434    }
435
436    // Oracle: RFC 8620 §3.7, JSON Pointer RFC 6901 §4 — path pointing to an
437    // array resolves to that array value.
438    #[test]
439    fn resolve_args_array_path_resolves_to_array() {
440        let prior = vec![(
441            "List/query".to_owned(),
442            json!({"ids": ["a", "b", "c"]}),
443            "c0".to_owned(),
444        )];
445        let mut args = json!({
446            "#ids": {"resultOf": "c0", "name": "List/query", "path": "/ids"}
447        });
448        resolve_args(&mut args, &prior).expect("array path must resolve");
449        assert_eq!(args, json!({"ids": ["a", "b", "c"]}));
450    }
451
452    // Oracle: RFC 8620 §3.7, JSON Pointer RFC 6901 §4 — multi-segment path
453    // drills into nested structures.
454    #[test]
455    fn resolve_args_nested_path_resolves() {
456        let prior = vec![(
457            "Foo/get".to_owned(),
458            json!({"list": [{"id": "deep1"}]}),
459            "c0".to_owned(),
460        )];
461        let mut args = json!({
462            "#id": {"resultOf": "c0", "name": "Foo/get", "path": "/list/0/id"}
463        });
464        resolve_args(&mut args, &prior).expect("nested path must resolve");
465        assert_eq!(args, json!({"id": "deep1"}));
466    }
467
468    // Oracle: RFC 6901 §7 — an array index that is out of bounds causes the
469    // pointer to fail, which maps to invalidResultReference.
470    #[test]
471    fn resolve_args_path_array_oob_is_error() {
472        let prior = vec![("Foo/get".to_owned(), json!({"ids": ["a"]}), "c0".to_owned())];
473        let mut args = json!({
474            "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/ids/5"}
475        });
476        let original = args.clone();
477        let err = resolve_args(&mut args, &prior).unwrap_err();
478        assert_eq!(err.error_type, "invalidResultReference");
479        assert_eq!(args, original);
480    }
481
482    // Oracle: RFC 6901 §4 — array index tokens with a leading zero (other than
483    // the single character "0") MUST be rejected as invalid.
484    #[test]
485    fn resolve_args_path_leading_zero_index_is_error() {
486        let prior = vec![(
487            "Foo/get".to_owned(),
488            json!({"ids": ["a", "b"]}),
489            "c0".to_owned(),
490        )];
491        let mut args = json!({
492            "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/ids/01"}
493        });
494        let original = args.clone();
495        let err = resolve_args(&mut args, &prior).unwrap_err();
496        assert_eq!(err.error_type, "invalidResultReference");
497        assert_eq!(args, original, "args must be unchanged on error");
498    }
499
500    // Oracle: RFC 6901 §3 — `~1` is the escape sequence for `/` and `~0` for `~`
501    // in JSON Pointer tokens.
502    #[test]
503    fn resolve_args_path_tilde_escaping() {
504        let prior = vec![(
505            "Foo/get".to_owned(),
506            json!({"a/b": "slash-value"}),
507            "c0".to_owned(),
508        )];
509        let mut args = json!({
510            "#val": {"resultOf": "c0", "name": "Foo/get", "path": "/a~1b"}
511        });
512        resolve_args(&mut args, &prior).expect("tilde-escaped path must resolve");
513        assert_eq!(args, json!({"val": "slash-value"}));
514    }
515
516    // Oracle: RFC 6901 §3 — `~0` is the escape sequence for `~`.
517    // Replacement order must be ~1 first then ~0; otherwise `~01` would
518    // incorrectly become `/` instead of `~1`.
519    #[test]
520    fn resolve_args_path_tilde0_escaping() {
521        let prior = vec![(
522            "Foo/get".to_owned(),
523            json!({"a~b": "tilde-value"}),
524            "c0".to_owned(),
525        )];
526        let mut args = json!({
527            "#val": {"resultOf": "c0", "name": "Foo/get", "path": "/a~0b"}
528        });
529        resolve_args(&mut args, &prior).expect("~0-escaped path must resolve");
530        assert_eq!(args, json!({"val": "tilde-value"}));
531    }
532
533    // Oracle: RFC 6901 §3 — `~01` must decode to the literal string `~1`,
534    // NOT to `/`. ~1 is replaced first (yielding `~1`), then ~0 on what
535    // remains would replace `~0` — but after the first pass `~01` → `~1`
536    // there is no `~0` left; the result is `/`.
537    // Wait — `~01`: replace ~1 first: `~01` has no `~1` at position 0 (it's `~0` then `1`).
538    // So `~01` → replace ~1 → no match → `~01` → replace ~0 → `~` → result: `~1`.
539    // i.e. `~01` decodes to `~1` (literal tilde followed by 1), NOT to `/`.
540    #[test]
541    fn resolve_args_path_tilde01_decodes_to_tilde1() {
542        let prior = vec![(
543            "Foo/get".to_owned(),
544            json!({"~1": "tilde-one-value"}),
545            "c0".to_owned(),
546        )];
547        let mut args = json!({
548            "#val": {"resultOf": "c0", "name": "Foo/get", "path": "/~01"}
549        });
550        resolve_args(&mut args, &prior).expect("~01 must decode to literal key ~1");
551        assert_eq!(args, json!({"val": "tilde-one-value"}));
552    }
553
554    // Oracle: RFC 8620 §3.7 — /list/*/threadId maps threadId from each list element.
555    #[test]
556    fn resolve_args_wildcard_maps_over_array() {
557        let prior = vec![(
558            "Thread/get".to_owned(),
559            json!({
560                "list": [{"threadId": "t1"}, {"threadId": "t2"}]
561            }),
562            "c0".to_owned(),
563        )];
564        let mut args =
565            json!({"#ids": {"resultOf": "c0", "name": "Thread/get", "path": "/list/*/threadId"}});
566        resolve_args(&mut args, &prior).expect("wildcard must resolve");
567        assert_eq!(args, json!({"ids": ["t1", "t2"]}));
568    }
569
570    // Oracle: RFC 8620 §3.7 — when wildcard result is an array, it is flattened.
571    #[test]
572    fn resolve_args_wildcard_flattens_array_results() {
573        let prior = vec![(
574            "Email/get".to_owned(),
575            json!({
576                "list": [{"emailIds": ["e1", "e2"]}, {"emailIds": ["e3"]}]
577            }),
578            "c0".to_owned(),
579        )];
580        let mut args =
581            json!({"#ids": {"resultOf": "c0", "name": "Email/get", "path": "/list/*/emailIds"}});
582        resolve_args(&mut args, &prior).expect("wildcard flatten must resolve");
583        assert_eq!(args, json!({"ids": ["e1", "e2", "e3"]}));
584    }
585
586    // Oracle: RFC 6901 §4 — basic path navigation.
587    #[test]
588    fn json_pointer_ext_plain_path() {
589        let v = json!({"a": {"b": 42}});
590        assert_eq!(json_pointer_ext(&v, "/a/b"), Some(json!(42)));
591    }
592
593    // Oracle: RFC 6901 §4 — empty path returns whole document.
594    #[test]
595    fn json_pointer_ext_empty_path_returns_root() {
596        let v = json!({"x": 1});
597        assert_eq!(json_pointer_ext(&v, ""), Some(v.clone()));
598    }
599}