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 use super::Guid;
5 use serde::{Deserialize, Serialize};
6 use serde_json::{Map, Value as JsonValue};
7 
8 /// Represents the decrypted payload in a Bso. Provides a minimal layer of type
9 /// safety to avoid double-encrypting.
10 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11 pub struct Payload {
12     pub id: Guid,
13 
14     #[serde(default)]
15     #[serde(skip_serializing_if = "crate::skip_if_default")]
16     pub deleted: bool,
17 
18     #[serde(flatten)]
19     pub data: Map<String, JsonValue>,
20 }
21 
22 impl Payload {
new_tombstone(id: impl Into<Guid>) -> Payload23     pub fn new_tombstone(id: impl Into<Guid>) -> Payload {
24         Payload {
25             id: id.into(),
26             deleted: true,
27             data: Map::new(),
28         }
29     }
30 
new_tombstone_with_ttl(id: impl Into<Guid>, ttl: u32) -> Payload31     pub fn new_tombstone_with_ttl(id: impl Into<Guid>, ttl: u32) -> Payload {
32         let mut result = Payload::new_tombstone(id);
33         result.data.insert("ttl".into(), ttl.into());
34         result
35     }
36 
37     #[inline]
with_sortindex(mut self, index: i32) -> Payload38     pub fn with_sortindex(mut self, index: i32) -> Payload {
39         self.data.insert("sortindex".into(), index.into());
40         self
41     }
42 
43     /// "Auto" fields are fields like 'sortindex' and 'ttl', which are:
44     ///
45     /// - Added to the payload automatically when deserializing if present on
46     ///   the incoming BSO or envelope.
47     /// - Removed from the payload automatically and attached to the BSO or
48     ///   envelope if present on the outgoing payload.
with_auto_field<T: Into<JsonValue>>(mut self, name: &str, v: Option<T>) -> Payload49     pub fn with_auto_field<T: Into<JsonValue>>(mut self, name: &str, v: Option<T>) -> Payload {
50         let old_value: Option<JsonValue> = if let Some(value) = v {
51             self.data.insert(name.into(), value.into())
52         } else {
53             self.data.remove(name)
54         };
55 
56         // This is a little dubious, but it seems like if we have a e.g. `sortindex` field on the payload
57         // it's going to be a bug if we use it instead of the "real" sort index.
58         if let Some(old_value) = old_value {
59             log::warn!(
60                 "Payload for record {} already contains 'automatic' field \"{}\"? \
61                  Overwriting auto value: {} with 'real' value",
62                 self.id,
63                 name,
64                 old_value,
65             );
66         }
67         self
68     }
69 
take_auto_field<V>(&mut self, name: &str) -> Option<V> where for<'a> V: Deserialize<'a>,70     pub fn take_auto_field<V>(&mut self, name: &str) -> Option<V>
71     where
72         for<'a> V: Deserialize<'a>,
73     {
74         let v = self.data.remove(name)?;
75         match serde_json::from_value(v) {
76             Ok(v) => Some(v),
77             Err(e) => {
78                 log::error!(
79                     "Automatic field {} exists on payload, but cannot be deserialized: {}",
80                     name,
81                     e
82                 );
83                 None
84             }
85         }
86     }
87 
88     #[inline]
id(&self) -> &str89     pub fn id(&self) -> &str {
90         &self.id[..]
91     }
92 
93     #[inline]
is_tombstone(&self) -> bool94     pub fn is_tombstone(&self) -> bool {
95         self.deleted
96     }
97 
from_json(value: JsonValue) -> Result<Payload, serde_json::Error>98     pub fn from_json(value: JsonValue) -> Result<Payload, serde_json::Error> {
99         serde_json::from_value(value)
100     }
101 
102     /// Deserializes the BSO payload into a specific record type `T`.
103     ///
104     /// BSO payloads are unstructured JSON objects, with string keys and
105     /// dynamically-typed values. `into_record` makes it more convenient to
106     /// work with payloads by converting them into data type-specific structs.
107     /// Your record type only needs to derive or implement `serde::Deserialize`;
108     /// Serde will take care of the rest.
109     ///
110     /// # Errors
111     ///
112     /// `into_record` returns errors for type mismatches. As an example, trying
113     /// to deserialize a string value from the payload into an integer field in
114     /// `T` will fail.
115     ///
116     /// If there's a chance that a field contains invalid or mistyped data,
117     /// you'll want to extract it from `payload.data` manually, instead of using
118     /// `into_record`. This has been seen in the wild: for example, `dateAdded`
119     /// for bookmarks can be either an integer or a string.
into_record<T>(self) -> Result<T, serde_json::Error> where for<'a> T: Deserialize<'a>,120     pub fn into_record<T>(self) -> Result<T, serde_json::Error>
121     where
122         for<'a> T: Deserialize<'a>,
123     {
124         serde_json::from_value(JsonValue::from(self))
125     }
126 
from_record<T: Serialize>(v: T) -> Result<Payload, serde_json::Error>127     pub fn from_record<T: Serialize>(v: T) -> Result<Payload, serde_json::Error> {
128         // TODO(issue #2588): This is kind of dumb, we do to_value and then
129         // from_value. In general a more strongly typed API would help us avoid
130         // this sort of thing... But also concretely this could probably be
131         // avoided? At least in some cases.
132         Payload::from_json(serde_json::to_value(v)?)
133     }
134 
into_json_string(self) -> String135     pub fn into_json_string(self) -> String {
136         serde_json::to_string(&JsonValue::from(self))
137             .expect("JSON.stringify failed, which shouldn't be possible")
138     }
139 }
140 
141 impl From<Payload> for JsonValue {
from(cleartext: Payload) -> Self142     fn from(cleartext: Payload) -> Self {
143         let Payload {
144             mut data,
145             id,
146             deleted,
147         } = cleartext;
148         data.insert("id".to_string(), JsonValue::String(id.into_string()));
149         if deleted {
150             data.insert("deleted".to_string(), JsonValue::Bool(true));
151         }
152         JsonValue::Object(data)
153     }
154 }
155