1use crate::{Invocation, JmapError, JmapRequest, ResultReference};
4use serde_json::Value;
5
6pub fn parse_request(body: Value, max_calls: usize) -> Result<JmapRequest, JmapError> {
64 let req: JmapRequest = serde_json::from_value(body).map_err(|_| JmapError::not_request())?;
65
66 if req.method_calls.len() > max_calls {
67 return Err(JmapError::limit("maxCallsInRequest"));
68 }
69
70 Ok(req)
71}
72
73pub fn check_known_capabilities<S: AsRef<str>>(
104 req: &JmapRequest,
105 known: &[S],
106) -> Result<(), JmapError> {
107 for uri in &req.using {
108 if !known.iter().any(|k| k.as_ref() == uri.as_str()) {
109 return Err(JmapError::unknown_capability_with_detail(uri));
110 }
111 }
112 Ok(())
113}
114
115pub fn resolve_args(args: &mut Value, prior_responses: &[Invocation]) -> Result<(), JmapError> {
132 let Some(obj) = args.as_object_mut() else {
133 return Ok(()); };
135
136 let mut ref_pairs: Vec<(String, Value)> = Vec::with_capacity(obj.len());
139 ref_pairs.extend(
140 obj.iter()
141 .filter(|(k, _)| k.starts_with('#'))
142 .map(|(k, v)| (k.clone(), v.clone())),
143 );
144
145 if ref_pairs.is_empty() {
146 return Ok(());
147 }
148
149 let mut resolutions: Vec<(String, String, Value)> = Vec::with_capacity(ref_pairs.len());
152
153 for (ref_key, ref_value) in ref_pairs {
154 let plain_key = ref_key[1..].to_owned();
155
156 let rr: ResultReference = serde_json::from_value(ref_value).map_err(|e| {
158 JmapError::invalid_arguments(format!("invalid ResultReference for #{plain_key}: {e}"))
159 })?;
160
161 let (prior_method, prior_value) = prior_responses
163 .iter()
164 .find(|(_, _, call_id)| call_id == &rr.result_of)
165 .map(|(method, value, _)| (method.as_str(), value))
166 .ok_or_else(JmapError::invalid_result_reference)?;
167
168 if rr.name != prior_method {
170 return Err(JmapError::invalid_result_reference());
171 }
172
173 let resolved = json_pointer_ext(prior_value, &rr.path)
175 .ok_or_else(JmapError::invalid_result_reference)?;
176
177 if obj.contains_key(&plain_key) {
179 return Err(JmapError::invalid_arguments(format!(
180 "argument key conflict: '{}' and '#{}' both present",
181 plain_key, plain_key
182 )));
183 }
184
185 resolutions.push((ref_key, plain_key, resolved));
186 }
187
188 for (ref_key, plain_key, resolved) in resolutions {
190 obj.remove(&ref_key);
191 obj.insert(plain_key, resolved);
192 }
193
194 Ok(())
195}
196
197const MAX_JSON_POINTER_DEPTH: usize = 32;
209
210fn json_pointer_ext(value: &Value, path: &str) -> Option<Value> {
220 json_pointer_ext_inner(value, path, 0)
221}
222
223fn json_pointer_ext_inner(value: &Value, path: &str, depth: usize) -> Option<Value> {
224 if depth > MAX_JSON_POINTER_DEPTH {
225 return None;
230 }
231 if path.is_empty() {
232 return Some(value.clone());
233 }
234 if !path.starts_with('/') {
235 return None;
236 }
237
238 let after_slash = &path[1..];
240 let (token, remaining) = match after_slash.find('/') {
241 Some(pos) => (&after_slash[..pos], &after_slash[pos..]),
242 None => (after_slash, ""),
243 };
244
245 if token == "*" {
246 let arr = value.as_array()?;
248 let mut result: Vec<Value> = Vec::new();
249 for item in arr {
250 match json_pointer_ext_inner(item, remaining, depth + 1) {
251 Some(Value::Array(inner)) => result.extend(inner),
252 Some(other) => result.push(other),
253 None => return None, }
255 }
256 Some(Value::Array(result))
257 } else {
258 let key: std::borrow::Cow<str> = if token.contains('~') {
261 token.replace("~1", "/").replace("~0", "~").into()
262 } else {
263 token.into()
264 };
265 let next = match value {
266 Value::Object(obj) => obj.get(key.as_ref())?,
267 Value::Array(arr) => {
268 if key.len() > 1 && key.starts_with('0') {
270 return None;
271 }
272 let idx: usize = key.parse().ok()?;
273 arr.get(idx)?
274 }
275 _ => return None,
276 };
277 json_pointer_ext_inner(next, remaining, depth + 1)
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use serde_json::json;
285
286 #[test]
289 fn parse_request_valid() {
290 let body = json!({
291 "using": ["urn:ietf:params:jmap:core"],
292 "methodCalls": [
293 ["Foo/get", {"accountId": "a1"}, "0"]
294 ]
295 });
296 let req = parse_request(body, 16).expect("valid request must parse");
297 assert_eq!(req.using, vec!["urn:ietf:params:jmap:core"]);
298 assert_eq!(req.method_calls.len(), 1);
299 }
300
301 #[test]
304 fn parse_request_empty_using_is_ok() {
305 let body = json!({
306 "using": [],
307 "methodCalls": []
308 });
309 parse_request(body, 16)
310 .expect("empty using must be accepted — unknownMethod is dispatcher's job");
311 }
312
313 #[test]
314 fn parse_request_too_many_calls() {
315 let call = json!(["Foo/get", {}, "0"]);
316 let calls: Vec<_> = (0..5).map(|_| call.clone()).collect();
317 let body = json!({
318 "using": ["urn:ietf:params:jmap:core"],
319 "methodCalls": calls
320 });
321 let err = parse_request(body, 4).unwrap_err();
322 assert_eq!(
323 err.error_type, "limit",
324 "exceeding maxCallsInRequest must return limit per RFC 8620 §3.6.1"
325 );
326 }
327
328 #[test]
329 fn parse_request_at_max_calls_is_ok() {
330 let call = json!(["Foo/get", {}, "0"]);
331 let calls: Vec<_> = (0..4).map(|_| call.clone()).collect();
332 let body = json!({
333 "using": ["urn:ietf:params:jmap:core"],
334 "methodCalls": calls
335 });
336 parse_request(body, 4).expect("exactly max_calls must be accepted");
337 }
338
339 #[test]
340 fn parse_request_malformed_body() {
341 let body = json!("not an object");
342 let err = parse_request(body, 16).unwrap_err();
343 assert_eq!(
344 err.error_type, "notRequest",
345 "malformed body does not match Request type — must be notRequest per RFC 8620 §3.6.1"
346 );
347 }
348
349 #[test]
351 fn resolve_args_basic() {
352 let prior = vec![(
353 "Foo/get".to_owned(),
354 json!({"list": [{"id": "x1"}], "state": "s0"}),
355 "c0".to_owned(),
356 )];
357 let mut args = json!({
358 "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/list/0/id"}
359 });
360 resolve_args(&mut args, &prior).expect("must resolve");
361 assert_eq!(args, json!({"ids": "x1"}));
362 }
363
364 #[test]
366 fn resolve_args_unknown_result_of() {
367 let prior: Vec<Invocation> = vec![];
368 let mut args = json!({
369 "#ids": {"resultOf": "missing", "name": "Foo/get", "path": "/ids"}
370 });
371 let original = args.clone();
372 let err = resolve_args(&mut args, &prior).unwrap_err();
373 assert_eq!(err.error_type, "invalidResultReference");
374 assert_eq!(args, original);
376 }
377
378 #[test]
380 fn resolve_args_name_mismatch() {
381 let prior = vec![("Foo/get".to_owned(), json!({"ids": ["a"]}), "c0".to_owned())];
382 let mut args = json!({
383 "#ids": {"resultOf": "c0", "name": "Bar/get", "path": "/ids"}
384 });
385 let original = args.clone();
386 let err = resolve_args(&mut args, &prior).unwrap_err();
387 assert_eq!(err.error_type, "invalidResultReference");
388 assert_eq!(args, original);
389 }
390
391 #[test]
393 fn resolve_args_path_not_found() {
394 let prior = vec![("Foo/get".to_owned(), json!({"ids": ["a"]}), "c0".to_owned())];
395 let mut args = json!({
396 "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/nonexistent"}
397 });
398 let original = args.clone();
399 let err = resolve_args(&mut args, &prior).unwrap_err();
400 assert_eq!(err.error_type, "invalidResultReference");
401 assert_eq!(args, original);
402 }
403
404 #[test]
406 fn resolve_args_atomic_on_partial_failure() {
407 let prior = vec![(
408 "Foo/get".to_owned(),
409 json!({"ids": ["a", "b"]}),
410 "c0".to_owned(),
411 )];
412 let mut args = json!({
414 "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/ids"},
415 "#properties": {"resultOf": "missing", "name": "Foo/get", "path": "/props"}
416 });
417 let original = args.clone();
418 let err = resolve_args(&mut args, &prior).unwrap_err();
419 assert_eq!(err.error_type, "invalidResultReference");
420 assert_eq!(args, original);
421 }
422
423 #[test]
425 fn resolve_args_non_object_passthrough() {
426 let prior: Vec<Invocation> = vec![];
427 let mut args = json!("not-an-object");
428 resolve_args(&mut args, &prior).expect("non-object must not error");
429 assert_eq!(args, json!("not-an-object"));
430 }
431
432 #[test]
434 fn resolve_args_no_ref_keys() {
435 let prior: Vec<Invocation> = vec![];
436 let mut args = json!({"ids": ["a", "b"]});
437 resolve_args(&mut args, &prior).expect("no ref keys must not error");
438 assert_eq!(args, json!({"ids": ["a", "b"]}));
439 }
440
441 #[test]
444 fn parse_request_unknown_capability_accepted() {
445 let body = json!({
446 "using": ["urn:ietf:params:jmap:core", "urn:example:unknown"],
447 "methodCalls": [
448 ["Foo/get", {}, "0"]
449 ]
450 });
451 let req = parse_request(body, 16).expect("unknown capability must be accepted");
452 assert_eq!(req.using.len(), 2);
453 }
454
455 #[test]
457 fn parse_request_core_only_accepted() {
458 let body = json!({
459 "using": ["urn:ietf:params:jmap:core"],
460 "methodCalls": [
461 ["Foo/get", {}, "0"]
462 ]
463 });
464 parse_request(body, 16).expect("core-only using must be accepted");
465 }
466
467 #[test]
469 fn parse_request_zero_max_calls_rejects_any_call() {
470 let body = json!({
471 "using": ["urn:ietf:params:jmap:core"],
472 "methodCalls": [
473 ["Foo/get", {}, "0"]
474 ]
475 });
476 let err = parse_request(body, 0).unwrap_err();
477 assert_eq!(
478 err.error_type, "limit",
479 "zero max_calls means any call exceeds limit — must be limit per RFC 8620 §3.6.1"
480 );
481 }
482
483 #[test]
486 fn resolve_args_multiple_refs_all_resolve() {
487 let prior = vec![(
488 "Foo/get".to_owned(),
489 json!({"list": [{"id": "x1"}], "state": "s0"}),
490 "c0".to_owned(),
491 )];
492 let mut args = json!({
493 "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/list"},
494 "#state": {"resultOf": "c0", "name": "Foo/get", "path": "/state"}
495 });
496 resolve_args(&mut args, &prior).expect("both refs must resolve");
497 let obj = args.as_object().expect("must still be an object");
499 assert!(!obj.contains_key("#ids"), "#ids must be removed");
500 assert!(!obj.contains_key("#state"), "#state must be removed");
501 assert_eq!(args["ids"], json!([{"id": "x1"}]));
502 assert_eq!(args["state"], json!("s0"));
503 }
504
505 #[test]
508 fn resolve_args_key_conflict_is_error() {
509 let prior = vec![("Foo/get".to_owned(), json!({"ids": ["a"]}), "c0".to_owned())];
510 let mut args = json!({
511 "ids": "existing",
512 "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/ids"}
513 });
514 let original = args.clone();
515 let err = resolve_args(&mut args, &prior).unwrap_err();
516 assert_eq!(err.error_type, "invalidArguments");
517 assert_eq!(args, original);
519 }
520
521 #[test]
524 fn resolve_args_invalid_ref_value_is_error() {
525 let prior: Vec<Invocation> = vec![];
526 let mut args = json!({"#ids": "not-an-object"});
527 let original = args.clone();
528 let err = resolve_args(&mut args, &prior).unwrap_err();
529 assert_eq!(err.error_type, "invalidArguments");
530 assert_eq!(args, original);
531 }
532
533 #[test]
536 fn resolve_args_array_path_resolves_to_array() {
537 let prior = vec![(
538 "List/query".to_owned(),
539 json!({"ids": ["a", "b", "c"]}),
540 "c0".to_owned(),
541 )];
542 let mut args = json!({
543 "#ids": {"resultOf": "c0", "name": "List/query", "path": "/ids"}
544 });
545 resolve_args(&mut args, &prior).expect("array path must resolve");
546 assert_eq!(args, json!({"ids": ["a", "b", "c"]}));
547 }
548
549 #[test]
552 fn resolve_args_nested_path_resolves() {
553 let prior = vec![(
554 "Foo/get".to_owned(),
555 json!({"list": [{"id": "deep1"}]}),
556 "c0".to_owned(),
557 )];
558 let mut args = json!({
559 "#id": {"resultOf": "c0", "name": "Foo/get", "path": "/list/0/id"}
560 });
561 resolve_args(&mut args, &prior).expect("nested path must resolve");
562 assert_eq!(args, json!({"id": "deep1"}));
563 }
564
565 #[test]
568 fn resolve_args_path_array_oob_is_error() {
569 let prior = vec![("Foo/get".to_owned(), json!({"ids": ["a"]}), "c0".to_owned())];
570 let mut args = json!({
571 "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/ids/5"}
572 });
573 let original = args.clone();
574 let err = resolve_args(&mut args, &prior).unwrap_err();
575 assert_eq!(err.error_type, "invalidResultReference");
576 assert_eq!(args, original);
577 }
578
579 #[test]
582 fn resolve_args_path_leading_zero_index_is_error() {
583 let prior = vec![(
584 "Foo/get".to_owned(),
585 json!({"ids": ["a", "b"]}),
586 "c0".to_owned(),
587 )];
588 let mut args = json!({
589 "#ids": {"resultOf": "c0", "name": "Foo/get", "path": "/ids/01"}
590 });
591 let original = args.clone();
592 let err = resolve_args(&mut args, &prior).unwrap_err();
593 assert_eq!(err.error_type, "invalidResultReference");
594 assert_eq!(args, original, "args must be unchanged on error");
595 }
596
597 #[test]
600 fn resolve_args_path_tilde_escaping() {
601 let prior = vec![(
602 "Foo/get".to_owned(),
603 json!({"a/b": "slash-value"}),
604 "c0".to_owned(),
605 )];
606 let mut args = json!({
607 "#val": {"resultOf": "c0", "name": "Foo/get", "path": "/a~1b"}
608 });
609 resolve_args(&mut args, &prior).expect("tilde-escaped path must resolve");
610 assert_eq!(args, json!({"val": "slash-value"}));
611 }
612
613 #[test]
617 fn resolve_args_path_tilde0_escaping() {
618 let prior = vec![(
619 "Foo/get".to_owned(),
620 json!({"a~b": "tilde-value"}),
621 "c0".to_owned(),
622 )];
623 let mut args = json!({
624 "#val": {"resultOf": "c0", "name": "Foo/get", "path": "/a~0b"}
625 });
626 resolve_args(&mut args, &prior).expect("~0-escaped path must resolve");
627 assert_eq!(args, json!({"val": "tilde-value"}));
628 }
629
630 #[test]
638 fn resolve_args_path_tilde01_decodes_to_tilde1() {
639 let prior = vec![(
640 "Foo/get".to_owned(),
641 json!({"~1": "tilde-one-value"}),
642 "c0".to_owned(),
643 )];
644 let mut args = json!({
645 "#val": {"resultOf": "c0", "name": "Foo/get", "path": "/~01"}
646 });
647 resolve_args(&mut args, &prior).expect("~01 must decode to literal key ~1");
648 assert_eq!(args, json!({"val": "tilde-one-value"}));
649 }
650
651 #[test]
653 fn resolve_args_wildcard_maps_over_array() {
654 let prior = vec![(
655 "Thread/get".to_owned(),
656 json!({
657 "list": [{"threadId": "t1"}, {"threadId": "t2"}]
658 }),
659 "c0".to_owned(),
660 )];
661 let mut args =
662 json!({"#ids": {"resultOf": "c0", "name": "Thread/get", "path": "/list/*/threadId"}});
663 resolve_args(&mut args, &prior).expect("wildcard must resolve");
664 assert_eq!(args, json!({"ids": ["t1", "t2"]}));
665 }
666
667 #[test]
671 fn resolve_args_wildcard_over_flat_string_array() {
672 let prior = vec![(
675 "Email/query".to_owned(),
676 json!({ "ids": ["a", "b", "c"] }),
677 "c0".to_owned(),
678 )];
679 let mut args = json!({"#ids": {"resultOf": "c0", "name": "Email/query", "path": "/ids/*"}});
680 resolve_args(&mut args, &prior).expect("flat-array wildcard must resolve");
681 assert_eq!(args, json!({"ids": ["a", "b", "c"]}));
683 }
684
685 #[test]
687 fn resolve_args_wildcard_flattens_array_results() {
688 let prior = vec![(
689 "Email/get".to_owned(),
690 json!({
691 "list": [{"emailIds": ["e1", "e2"]}, {"emailIds": ["e3"]}]
692 }),
693 "c0".to_owned(),
694 )];
695 let mut args =
696 json!({"#ids": {"resultOf": "c0", "name": "Email/get", "path": "/list/*/emailIds"}});
697 resolve_args(&mut args, &prior).expect("wildcard flatten must resolve");
698 assert_eq!(args, json!({"ids": ["e1", "e2", "e3"]}));
699 }
700
701 #[test]
703 fn json_pointer_ext_plain_path() {
704 let v = json!({"a": {"b": 42}});
705 assert_eq!(json_pointer_ext(&v, "/a/b"), Some(json!(42)));
706 }
707
708 #[test]
710 fn json_pointer_ext_empty_path_returns_root() {
711 let v = json!({"x": 1});
712 assert_eq!(json_pointer_ext(&v, ""), Some(v.clone()));
713 }
714
715 #[test]
727 fn json_pointer_ext_rejects_deep_path() {
728 const DEPTH: usize = 1000;
729 let mut value = json!(42);
731 for _ in 0..DEPTH {
732 value = json!({ "a": value });
733 }
734 let path: String = "/a".repeat(DEPTH);
736 assert_eq!(
737 json_pointer_ext(&value, &path),
738 None,
739 "pointer with {DEPTH} tokens must be rejected by the depth cap"
740 );
741 }
742
743 #[test]
748 fn json_pointer_ext_accepts_path_within_depth_cap() {
749 const LEN: usize = MAX_JSON_POINTER_DEPTH - 1;
752 let mut value = json!("leaf");
753 for _ in 0..LEN {
754 value = json!({ "a": value });
755 }
756 let path: String = "/a".repeat(LEN);
757 assert_eq!(
758 json_pointer_ext(&value, &path),
759 Some(json!("leaf")),
760 "pointer with {LEN} tokens must still resolve under the depth cap"
761 );
762 }
763
764 #[test]
770 fn check_known_capabilities_unknown_uri_is_error() {
771 let req = JmapRequest::new(
772 vec![
773 "urn:ietf:params:jmap:core".into(),
774 "urn:example:unknown".into(),
775 ],
776 vec![],
777 None,
778 );
779 let known = &["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"];
780 let err = check_known_capabilities(&req, known).unwrap_err();
781 assert_eq!(
782 err.error_type, "unknownCapability",
783 "unrecognised URI must produce unknownCapability per RFC 8620 §3.3"
784 );
785 assert_eq!(
786 err.description.as_deref(),
787 Some("urn:example:unknown"),
788 "unknownCapability error must name the unrecognised URI in description"
789 );
790 }
791
792 #[test]
794 fn check_known_capabilities_all_known_is_ok() {
795 let req = JmapRequest::new(
796 vec![
797 "urn:ietf:params:jmap:core".into(),
798 "urn:ietf:params:jmap:mail".into(),
799 ],
800 vec![],
801 None,
802 );
803 let known = &["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"];
804 check_known_capabilities(&req, known).expect("all URIs are in known — must return Ok");
805 }
806
807 #[test]
809 fn check_known_capabilities_empty_using_is_ok() {
810 let req = JmapRequest::new(vec![], vec![], None);
811 let known = &["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"];
812 check_known_capabilities(&req, known)
813 .expect("empty using[] must return Ok even when known is non-empty");
814 }
815}