sync15/bso/
content.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 */
5
6//! This module enhances the IncomingBso and OutgoingBso records to deal with
7//! arbitrary <T> types, which we call "content"
8//! It can:
9//! * Parse JSON into some <T> while handling tombstones and invalid json.
10//! * Turn arbitrary <T> objects with an `id` field into an OutgoingBso.
11
12use super::{IncomingBso, IncomingContent, IncomingKind, OutgoingBso, OutgoingEnvelope};
13use crate::Guid;
14use crate::error::{trace, warn};
15use error_support::report_error;
16use serde::Serialize;
17
18// The only errors we return here are serde errors.
19type Result<T> = std::result::Result<T, serde_json::Error>;
20
21impl<T> IncomingContent<T> {
22    /// Returns Some(content) if [self.kind] is [IncomingKind::Content], None otherwise.
23    pub fn content(self) -> Option<T> {
24        match self.kind {
25            IncomingKind::Content(t) => Some(t),
26            _ => None,
27        }
28    }
29}
30
31// We don't want to force our T to be Debug, but we can be Debug if T is.
32impl<T: std::fmt::Debug> std::fmt::Debug for IncomingKind<T> {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            IncomingKind::Content(r) => {
36                write!(f, "IncomingKind::Content<{:?}>", r)
37            }
38            IncomingKind::Tombstone => write!(f, "IncomingKind::Tombstone"),
39            IncomingKind::Malformed => write!(f, "IncomingKind::Malformed"),
40        }
41    }
42}
43
44impl IncomingBso {
45    /// Convert an [IncomingBso] to an [IncomingContent] possibly holding a T.
46    pub fn into_content<T: for<'de> serde::Deserialize<'de>>(self) -> IncomingContent<T> {
47        self.into_content_with_fixup(|_| {})
48    }
49
50    /// Like into_content, but adds an additional fixup step where the caller can adjust the
51    /// `serde_json::Value'
52    pub fn into_content_with_fixup<T: for<'de> serde::Deserialize<'de>>(
53        self,
54        fixup: impl FnOnce(&mut serde_json::Value),
55    ) -> IncomingContent<T> {
56        match serde_json::from_str(&self.payload) {
57            Ok(mut json) => {
58                // We got a good serde_json::Value, run the fixup method
59                fixup(&mut json);
60                // ...now see if it's a <T>.
61                let kind = json_to_kind(json, &self.envelope.id);
62                IncomingContent {
63                    envelope: self.envelope,
64                    kind,
65                }
66            }
67            Err(e) => {
68                // payload isn't valid json.
69                warn!("Invalid incoming cleartext {}: {}", self.envelope.id, e);
70                IncomingContent {
71                    envelope: self.envelope,
72                    kind: IncomingKind::Malformed,
73                }
74            }
75        }
76    }
77}
78
79impl OutgoingBso {
80    /// Creates a new tombstone record.
81    /// Not all collections expect tombstones.
82    pub fn new_tombstone(envelope: OutgoingEnvelope) -> Self {
83        Self {
84            envelope,
85            payload: serde_json::json!({"deleted": true}).to_string(),
86        }
87    }
88
89    /// Creates a outgoing record from some <T>, which can be made into a JSON object
90    /// with a valid `id`. This is the most convenient way to create an outgoing
91    /// item from a <T> when the default envelope is suitable.
92    /// Will panic if there's no good `id` in the json.
93    pub fn from_content_with_id<T>(record: T) -> Result<Self>
94    where
95        T: Serialize,
96    {
97        let (json, id) = content_with_id_to_json(record)?;
98        Ok(Self {
99            envelope: id.into(),
100            payload: serde_json::to_string(&json)?,
101        })
102    }
103
104    /// Create an Outgoing record with an explicit envelope. Will panic if the
105    /// payload has an ID but it doesn't match the envelope.
106    pub fn from_content<T>(envelope: OutgoingEnvelope, record: T) -> Result<Self>
107    where
108        T: Serialize,
109    {
110        let json = content_to_json(record, &envelope.id)?;
111        Ok(Self {
112            envelope,
113            payload: serde_json::to_string(&json)?,
114        })
115    }
116}
117
118// Helpers for packing and unpacking serde objects to and from a <T>. In particular:
119// * Helping deal complications around raw json payload not having 'id' (the envelope is
120//   canonical) but needing it to exist when dealing with serde locally.
121//   For example, a record on the server after being decrypted looks like:
122//   `{"id": "a-guid", payload: {"field": "value"}}`
123//   But the `T` for this typically looks like `struct T { id: Guid, field: String}`
124//   So before we try and deserialize this record into a T, we copy the `id` field
125//   from the envelope into the payload, and when serializing from a T we do the
126//   reverse (ie, ensure the `id` in the payload is removed and placed in the envelope)
127// * Tombstones.
128
129// Deserializing json into a T
130fn json_to_kind<T>(mut json: serde_json::Value, id: &Guid) -> IncomingKind<T>
131where
132    T: for<'de> serde::Deserialize<'de>,
133{
134    // It's possible that the payload does not carry 'id', but <T> always does - so grab it from the
135    // envelope and put it into the json before deserializing the record.
136    if let serde_json::Value::Object(ref mut map) = json {
137        if map.contains_key("deleted") {
138            return IncomingKind::Tombstone;
139        }
140        match map.get("id") {
141            Some(serde_json::Value::String(content_id)) => {
142                // It exists in the payload! We treat a mismatch as malformed.
143                if content_id != id {
144                    trace!(
145                        "malformed incoming record: envelope id: {} payload id: {}",
146                        content_id, id
147                    );
148                    report_error!(
149                        "incoming-invalid-mismatched-ids",
150                        "Envelope and payload don't agree on the ID"
151                    );
152                    return IncomingKind::Malformed;
153                }
154                if !id.is_valid_for_sync_server() {
155                    trace!("malformed incoming record: id is not valid: {}", id);
156                    report_error!(
157                        "incoming-invalid-bad-payload-id",
158                        "ID in the payload is invalid"
159                    );
160                    return IncomingKind::Malformed;
161                }
162            }
163            Some(v) => {
164                // It exists in the payload but is not a string - they can't possibly be
165                // the same as the envelope uses a String, so must be malformed.
166                trace!("malformed incoming record: id is not a string: {}", v);
167                report_error!("incoming-invalid-wrong_type", "ID is not a string");
168                return IncomingKind::Malformed;
169            }
170            None => {
171                // Doesn't exist in the payload - add it before trying to deser a T.
172                if !id.is_valid_for_sync_server() {
173                    trace!("malformed incoming record: id is not valid: {}", id);
174                    report_error!(
175                        "incoming-invalid-bad-envelope-id",
176                        "ID in envelope is not valid"
177                    );
178                    return IncomingKind::Malformed;
179                }
180                map.insert("id".to_string(), id.to_string().into());
181            }
182        }
183    };
184    match serde_path_to_error::deserialize(json) {
185        Ok(v) => IncomingKind::Content(v),
186        Err(e) => {
187            report_error!(
188                "invalid-incoming-content",
189                "{}.{}: {}",
190                std::any::type_name::<T>(),
191                e.path(),
192                e.inner()
193            );
194            IncomingKind::Malformed
195        }
196    }
197}
198
199// Serializing <T> into json with special handling of `id` (the `id` from the payload
200// is used as the envelope ID)
201fn content_with_id_to_json<T>(record: T) -> Result<(serde_json::Value, Guid)>
202where
203    T: Serialize,
204{
205    let mut json = serde_json::to_value(record)?;
206    let id = match json.as_object_mut() {
207        Some(ref mut map) => {
208            match map.get("id").as_ref().and_then(|v| v.as_str()) {
209                Some(id) => {
210                    let id: Guid = id.into();
211                    assert!(id.is_valid_for_sync_server(), "record's ID is invalid");
212                    id
213                }
214                // In practice, this is a "static" error and not influenced by runtime behavior
215                None => panic!("record does not have an ID in the payload"),
216            }
217        }
218        None => panic!("record is not a json object"),
219    };
220    Ok((json, id))
221}
222
223// Serializing <T> into json with special handling of `id` (if `id` in serialized
224// JSON already exists, we panic if it doesn't match the envelope. If the serialized
225// content does not have an `id`, it is added from the envelope)
226// is used as the envelope ID)
227fn content_to_json<T>(record: T, id: &Guid) -> Result<serde_json::Value>
228where
229    T: Serialize,
230{
231    let mut payload = serde_json::to_value(record)?;
232    if let Some(ref mut map) = payload.as_object_mut() {
233        if let Some(content_id) = map.get("id").as_ref().and_then(|v| v.as_str()) {
234            assert_eq!(content_id, id);
235            assert!(id.is_valid_for_sync_server(), "record's ID is invalid");
236        } else {
237            map.insert("id".to_string(), serde_json::Value::String(id.to_string()));
238        }
239    };
240    Ok(payload)
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use crate::bso::IncomingBso;
247    use serde::{Deserialize, Serialize};
248    use serde_json::json;
249
250    #[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
251    struct TestStruct {
252        id: Guid,
253        data: u32,
254    }
255    #[test]
256    fn test_content_deser() {
257        error_support::init_for_tests();
258        let json = json!({
259            "id": "test",
260            "payload": json!({"data": 1}).to_string(),
261        });
262        let incoming: IncomingBso = serde_json::from_value(json).unwrap();
263        assert_eq!(incoming.envelope.id, "test");
264        let record = incoming.into_content::<TestStruct>().content().unwrap();
265        let expected = TestStruct {
266            id: Guid::new("test"),
267            data: 1,
268        };
269        assert_eq!(record, expected);
270    }
271
272    #[test]
273    fn test_content_deser_empty_id() {
274        error_support::init_for_tests();
275        let json = json!({
276            "id": "",
277            "payload": json!({"data": 1}).to_string(),
278        });
279        let incoming: IncomingBso = serde_json::from_value(json).unwrap();
280        // The envelope has an invalid ID, but it's not handled until we try and deserialize
281        // it into a T
282        assert_eq!(incoming.envelope.id, "");
283        let content = incoming.into_content::<TestStruct>();
284        assert!(matches!(content.kind, IncomingKind::Malformed));
285    }
286
287    #[test]
288    fn test_content_deser_invalid() {
289        error_support::init_for_tests();
290        // And a non-empty but still invalid guid.
291        let json = json!({
292            "id": "X".repeat(65),
293            "payload": json!({"data": 1}).to_string(),
294        });
295        let incoming: IncomingBso = serde_json::from_value(json).unwrap();
296        let content = incoming.into_content::<TestStruct>();
297        assert!(matches!(content.kind, IncomingKind::Malformed));
298    }
299
300    #[test]
301    fn test_content_deser_not_string() {
302        error_support::init_for_tests();
303        // A non-string id.
304        let json = json!({
305            "id": "0",
306            "payload": json!({"id": 0, "data": 1}).to_string(),
307        });
308        let incoming: IncomingBso = serde_json::from_value(json).unwrap();
309        let content = incoming.into_content::<serde_json::Value>();
310        assert!(matches!(content.kind, IncomingKind::Malformed));
311    }
312
313    #[test]
314    fn test_content_ser_with_id() {
315        error_support::init_for_tests();
316        // When serializing, expect the ID to be in the top-level payload (ie,
317        // in the envelope) but should not appear in the cleartext `payload` part of
318        // the payload.
319        let val = TestStruct {
320            id: Guid::new("test"),
321            data: 1,
322        };
323        let outgoing = OutgoingBso::from_content_with_id(val).unwrap();
324
325        // The envelope should have our ID.
326        assert_eq!(outgoing.envelope.id, Guid::new("test"));
327
328        // and make sure `cleartext` part of the payload the data and the id.
329        let ct_value = serde_json::from_str::<serde_json::Value>(&outgoing.payload).unwrap();
330        assert_eq!(ct_value, json!({"data": 1, "id": "test"}));
331    }
332
333    #[test]
334    fn test_content_ser_with_envelope() {
335        error_support::init_for_tests();
336        // When serializing, expect the ID to be in the top-level payload (ie,
337        // in the envelope) but should not appear in the cleartext `payload`
338        let val = TestStruct {
339            id: Guid::new("test"),
340            data: 1,
341        };
342        let envelope: OutgoingEnvelope = Guid::new("test").into();
343        let outgoing = OutgoingBso::from_content(envelope, val).unwrap();
344
345        // The envelope should have our ID.
346        assert_eq!(outgoing.envelope.id, Guid::new("test"));
347
348        // and make sure `cleartext` part of the payload has data and the id.
349        let ct_value = serde_json::from_str::<serde_json::Value>(&outgoing.payload).unwrap();
350        assert_eq!(ct_value, json!({"data": 1, "id": "test"}));
351    }
352
353    #[test]
354    #[should_panic]
355    fn test_content_ser_no_ids() {
356        error_support::init_for_tests();
357        #[derive(Serialize)]
358        struct StructWithNoId {
359            data: u32,
360        }
361        let val = StructWithNoId { data: 1 };
362        let _ = OutgoingBso::from_content_with_id(val);
363    }
364
365    #[test]
366    #[should_panic]
367    fn test_content_ser_not_object() {
368        error_support::init_for_tests();
369        let _ = OutgoingBso::from_content_with_id(json!("string"));
370    }
371
372    #[test]
373    #[should_panic]
374    fn test_content_ser_mismatched_ids() {
375        error_support::init_for_tests();
376        let val = TestStruct {
377            id: Guid::new("test"),
378            data: 1,
379        };
380        let envelope: OutgoingEnvelope = Guid::new("different").into();
381        let _ = OutgoingBso::from_content(envelope, val);
382    }
383
384    #[test]
385    #[should_panic]
386    fn test_content_empty_id() {
387        error_support::init_for_tests();
388        let val = TestStruct {
389            id: Guid::new(""),
390            data: 1,
391        };
392        let _ = OutgoingBso::from_content_with_id(val);
393    }
394
395    #[test]
396    #[should_panic]
397    fn test_content_invalid_id() {
398        error_support::init_for_tests();
399        let val = TestStruct {
400            id: Guid::new(&"X".repeat(65)),
401            data: 1,
402        };
403        let _ = OutgoingBso::from_content_with_id(val);
404    }
405}