jmap_server/helpers.rs
1//! Shared helper utilities for JMAP method handlers.
2
3use jmap_types::{Id, JmapError};
4use serde_json::{Map, Value};
5
6/// Serialize any [`serde::Serialize`] type to a [`serde_json::Value`],
7/// mapping serialization errors to [`JmapError::server_fail`].
8pub fn ser<T: serde::Serialize>(val: T) -> Result<serde_json::Value, JmapError> {
9 serde_json::to_value(val).map_err(|e| JmapError::server_fail(e.to_string()))
10}
11
12/// Convert a slice of [`Id`]s to a JSON `notFound` value.
13///
14/// RFC 8620 §5.1 specifies `notFound` as `Id[]` — always an array, never
15/// `null`. Returns an empty array when all requested ids were found.
16pub fn not_found_json(ids: &[Id]) -> Value {
17 Value::Array(
18 ids.iter()
19 .map(|id| Value::String(id.as_ref().to_owned()))
20 .collect(),
21 )
22}
23
24/// Extract `accountId` from a JMAP method arguments envelope and return both
25/// the extracted [`Id`] and the remaining argument map.
26///
27/// The caller passes the full `args: Value` from the method invocation by
28/// value; this function destructures it once, so handlers do not have to
29/// repeat the `let Value::Object(mut args) = args else { ... }` pattern after
30/// every call.
31///
32/// Returns `invalidArguments` with the message "arguments must be an object
33/// containing accountId" when `args` is not a JSON object, and the same error
34/// type with the message "accountId is required" when the field is missing or
35/// not a string.
36pub fn extract_account_id(args: Value) -> Result<(Id, Map<String, Value>), JmapError> {
37 let Value::Object(args) = args else {
38 return Err(JmapError::invalid_arguments(
39 "arguments must be an object containing accountId",
40 ));
41 };
42 match args.get("accountId").and_then(|v| v.as_str()) {
43 Some(s) => {
44 let id = Id::from(s);
45 Ok((id, args))
46 }
47 None => Err(JmapError::invalid_arguments("accountId is required")),
48 }
49}
50
51/// Return the current UTC instant formatted as an RFC 3339 string with
52/// millisecond precision (`YYYY-MM-DDTHH:MM:SS.mmmZ`).
53///
54/// Uses `std::time::SystemTime` so no external dependency is needed.
55///
56/// Pre-epoch handling: if `duration_since(UNIX_EPOCH)` fails (system clock
57/// drifted before the epoch), this function uses the absolute duration from
58/// `UNIX_EPOCH.duration_since(now)` but negates the seconds — producing a
59/// timestamp in the range 1969-12-31T… through 1970-01-01T00:00:00Z. This
60/// is still monotonically increasing for subsequent calls and never silently
61/// produces 1970-01-01T00:00:00.000Z for a clock that is merely slightly behind.
62pub fn now_utc_string() -> String {
63 use std::time::{SystemTime, UNIX_EPOCH};
64
65 let now = SystemTime::now();
66 let (secs, millis): (i64, u32) = match now.duration_since(UNIX_EPOCH) {
67 Ok(d) => (d.as_secs() as i64, d.subsec_millis()),
68 Err(e) => {
69 // Clock is before the Unix epoch — negate so we get a real (negative)
70 // epoch offset rather than silently returning 1970-01-01T00:00:00Z.
71 let d = e.duration();
72 (-(d.as_secs() as i64), d.subsec_millis())
73 }
74 };
75
76 let s = secs.rem_euclid(60);
77 let m = (secs / 60).rem_euclid(60);
78 let h = (secs / 3600).rem_euclid(24);
79 let days = secs.div_euclid(86400);
80 let (year, month, day) = civil_from_days(days);
81
82 format!("{year:04}-{month:02}-{day:02}T{h:02}:{m:02}:{s:02}.{millis:03}Z")
83}
84
85/// Convert a count of days since the Unix epoch (1970-01-01) to a proleptic
86/// Gregorian (year, month, day) triple.
87///
88/// Algorithm by Howard Hinnant (public domain):
89/// <https://howardhinnant.github.io/date_algorithms.html>
90fn civil_from_days(z: i64) -> (i32, u8, u8) {
91 let z = z + 719_468;
92 let era: i64 = if z >= 0 { z } else { z - 146_096 } / 146_097;
93 let doe = z - era * 146_097; // [0, 146096]
94 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
95 let y = yoe + era * 400;
96 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
97 let mp = (5 * doy + 2) / 153; // [0, 11]
98 let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
99 let mo = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12]
100 let yr = if mo <= 2 { y + 1 } else { y };
101 (yr as i32, mo as u8, d as u8)
102}
103
104/// Maximum recursion depth for [`json_merge_patch`] application.
105///
106/// Beyond this depth the patch is silently ignored at the affected sub-tree:
107/// the target value at that level is left unchanged. Mitigates stack DoS
108/// from adversarial `PatchObject` values (bd:JMAP-sc1b.97). 32 levels
109/// comfortably exceeds any legitimate JMAP `/set update` shape — the
110/// deepest standard JMAP `/set update` shape (Email with nested
111/// `bodyStructure`) tops out around 6 levels.
112pub const MAX_MERGE_PATCH_DEPTH: usize = 32;
113
114/// Apply a JSON Merge Patch (RFC 7396) to `target` in-place.
115///
116/// Used by every `*-server` backend's `update_object` implementation
117/// to merge a sparse `/set update` patch into the stored serialized
118/// object. Extracted from per-crate copies in bd:JMAP-sc1b.103 — keep
119/// edits here so all five reference backends stay byte-identical.
120///
121/// Patches deeper than [`MAX_MERGE_PATCH_DEPTH`] are silently truncated
122/// to bound stack use on adversarial input (bd:JMAP-sc1b.97). Below the
123/// cap the behaviour is exactly RFC 7396.
124pub fn json_merge_patch(target: &mut Value, patch: Value) {
125 json_merge_patch_inner(target, patch, 0);
126}
127
128fn json_merge_patch_inner(target: &mut Value, patch: Value, depth: usize) {
129 if depth > MAX_MERGE_PATCH_DEPTH {
130 return;
131 }
132 match patch {
133 Value::Object(patch_map) => {
134 // Per RFC 7396 §2: "If the target value is not a JSON object,
135 // the resulting value will be the merge patch." We therefore
136 // reset a non-Object target to an empty Object before merging
137 // — this is reachable when a Patch creates a nested field that
138 // is absent from the target (the parent recursion frame inserted
139 // Value::Null as a placeholder).
140 if !target.is_object() {
141 *target = Value::Object(Map::new());
142 }
143 let target_map = target
144 .as_object_mut()
145 .expect("target was just set to Value::Object above");
146 for (key, patch_val) in patch_map {
147 if patch_val.is_null() {
148 target_map.remove(&key);
149 } else {
150 let entry = target_map.entry(key).or_insert(Value::Null);
151 json_merge_patch_inner(entry, patch_val, depth + 1);
152 }
153 }
154 }
155 other => *target = other,
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::{civil_from_days, json_merge_patch, now_utc_string};
162
163 /// Test vectors derived independently with Python's `datetime.date` module.
164 /// `days` is the count of days since 1970-01-01.
165 #[test]
166 fn civil_from_days_known_dates() {
167 let cases: &[(i64, (i32, u8, u8))] = &[
168 (0, (1970, 1, 1)), // Unix epoch
169 (365, (1971, 1, 1)), // one year later (1970 is not a leap year)
170 (10957, (2000, 1, 1)), // Y2K
171 (11016, (2000, 2, 29)), // leap day in a century-divisible leap year
172 (11017, (2000, 3, 1)), // day after the leap day (era boundary in algorithm)
173 (19358, (2023, 1, 1)), // a recent non-leap year start
174 (19722, (2023, 12, 31)), // end of 2023
175 (19782, (2024, 2, 29)), // leap day in 2024
176 (19783, (2024, 3, 1)), // day after 2024 leap day
177 ];
178
179 for &(days, expected) in cases {
180 assert_eq!(
181 civil_from_days(days),
182 expected,
183 "civil_from_days({days}) mismatch"
184 );
185 }
186 }
187
188 #[test]
189 fn now_utc_string_format() {
190 let s = now_utc_string();
191 // Must match YYYY-MM-DDTHH:MM:SS.mmmZ (24 chars)
192 assert_eq!(s.len(), 24, "unexpected length: {s}");
193 assert!(s.ends_with('Z'), "must end with Z: {s}");
194 assert_eq!(&s[4..5], "-", "missing year-month separator: {s}");
195 assert_eq!(&s[7..8], "-", "missing month-day separator: {s}");
196 assert_eq!(&s[10..11], "T", "missing date-time separator: {s}");
197 assert_eq!(&s[13..14], ":", "missing hour-minute separator: {s}");
198 assert_eq!(&s[16..17], ":", "missing minute-second separator: {s}");
199 assert_eq!(&s[19..20], ".", "missing decimal point before millis: {s}");
200 // milliseconds are 3 decimal digits
201 assert!(
202 s[20..23].chars().all(|c| c.is_ascii_digit()),
203 "milliseconds must be 3 digits: {s}"
204 );
205 assert!(
206 s.starts_with("20"),
207 "year should start with 20 in 21st century: {s}"
208 );
209 }
210
211 // -----------------------------------------------------------------------
212 // json_merge_patch (RFC 7396)
213 //
214 // Test oracles are hand-built JSON values derived from RFC 7396 §2 and §3
215 // examples, plus the regression case from bd:JMAP-sc1b.87. No oracle is
216 // computed by the function under test (test-integrity rule from
217 // workspace AGENTS.md).
218 // -----------------------------------------------------------------------
219
220 /// Oracle: bd:JMAP-sc1b.97 — a 1000-deep merge patch must NOT crash
221 /// via stack overflow. The depth cap silently truncates beyond
222 /// [`MAX_MERGE_PATCH_DEPTH`], so the call returns; the topmost
223 /// levels are applied, and the deeper levels are ignored.
224 ///
225 /// The test does not use the function as its own oracle: the input
226 /// is hand-built (a 1000-deep `{ "a": { "a": ... { "a": {} } } }`
227 /// chain where every level is Object, matching the structural
228 /// shape of a real PatchObject — the documented latent panic from
229 /// bd:JMAP-sc1b.87 only fires on non-Object leaves, which a typed
230 /// PatchObject cannot produce). The assertion only checks that the
231 /// call completes without panicking and without overflowing the
232 /// stack. A pre-fix recursion-unlimited implementation would
233 /// overflow before returning.
234 #[test]
235 fn json_merge_patch_does_not_stack_overflow() {
236 const DEPTH: usize = 1000;
237 let mut target = serde_json::json!({});
238 for _ in 0..DEPTH {
239 target = serde_json::json!({ "a": target });
240 }
241 let mut patch = serde_json::json!({});
242 for _ in 0..DEPTH {
243 patch = serde_json::json!({ "a": patch });
244 }
245 json_merge_patch(&mut target, patch);
246 assert!(
247 target.is_object(),
248 "after a deeply-nested merge patch, target must remain a JSON object; got {target:?}"
249 );
250 }
251
252 /// Oracle: a shallow merge patch under the cap still applies
253 /// normally. Positive control paired with the stack-overflow test
254 /// above to prove the depth cap only fires at the boundary, not on
255 /// every call.
256 #[test]
257 fn json_merge_patch_shallow_applies_normally() {
258 let mut target = serde_json::json!({ "a": 1, "b": { "c": 2 } });
259 let patch = serde_json::json!({ "b": { "c": 99, "d": 7 }, "e": null });
260 json_merge_patch(&mut target, patch);
261 assert_eq!(
262 target,
263 serde_json::json!({ "a": 1, "b": { "c": 99, "d": 7 } }),
264 "RFC 7396 merge semantics broken at shallow depth"
265 );
266 }
267
268 /// Regression: a Patch that adds a nested Object to a previously-
269 /// absent field used to panic with `expect("merge patch target
270 /// must be an object")` because the parent recursion frame
271 /// inserted Value::Null as the placeholder, then recursed into
272 /// Null with an Object patch.
273 ///
274 /// Per RFC 7396 §2 the correct behaviour is to reset the non-Object
275 /// target to an empty Object and merge into it. Oracle is hand-
276 /// derived from RFC 7396 §2's pseudocode:
277 /// `Target[Name] = MergePatch(Target[Name], Value)` where
278 /// MergePatch resets a non-Object target to `{}`.
279 #[test]
280 fn json_merge_patch_adds_nested_object_to_absent_field() {
281 let mut target = serde_json::json!({ "a": 1 });
282 let patch = serde_json::json!({ "b": { "c": 2 } });
283 json_merge_patch(&mut target, patch);
284 assert_eq!(
285 target,
286 serde_json::json!({ "a": 1, "b": { "c": 2 } }),
287 "patch must add the nested object at the previously-absent field"
288 );
289 }
290}