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