1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use super::Guid;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value as JsonValue};

/// Represents the decrypted payload in a Bso. Provides a minimal layer of type
/// safety to avoid double-encrypting.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Payload {
    pub id: Guid,

    #[serde(default)]
    #[serde(skip_serializing_if = "crate::skip_if_default")]
    pub deleted: bool,

    #[serde(flatten)]
    pub data: Map<String, JsonValue>,
}

impl Payload {
    pub fn new_tombstone(id: impl Into<Guid>) -> Payload {
        Payload {
            id: id.into(),
            deleted: true,
            data: Map::new(),
        }
    }

    pub fn new_tombstone_with_ttl(id: impl Into<Guid>, ttl: u32) -> Payload {
        let mut result = Payload::new_tombstone(id);
        result.data.insert("ttl".into(), ttl.into());
        result
    }

    #[inline]
    pub fn with_sortindex(mut self, index: i32) -> Payload {
        self.data.insert("sortindex".into(), index.into());
        self
    }

    /// "Auto" fields are fields like 'sortindex' and 'ttl', which are:
    ///
    /// - Added to the payload automatically when deserializing if present on
    ///   the incoming BSO or envelope.
    /// - Removed from the payload automatically and attached to the BSO or
    ///   envelope if present on the outgoing payload.
    pub fn with_auto_field<T: Into<JsonValue>>(mut self, name: &str, v: Option<T>) -> Payload {
        let old_value: Option<JsonValue> = if let Some(value) = v {
            self.data.insert(name.into(), value.into())
        } else {
            self.data.remove(name)
        };

        // This is a little dubious, but it seems like if we have a e.g. `sortindex` field on the payload
        // it's going to be a bug if we use it instead of the "real" sort index.
        if let Some(old_value) = old_value {
            log::warn!(
                "Payload for record {} already contains 'automatic' field \"{}\"? \
                 Overwriting auto value: {} with 'real' value",
                self.id,
                name,
                old_value,
            );
        }
        self
    }

    pub fn take_auto_field<V>(&mut self, name: &str) -> Option<V>
    where
        for<'a> V: Deserialize<'a>,
    {
        let v = self.data.remove(name)?;
        match serde_json::from_value(v) {
            Ok(v) => Some(v),
            Err(e) => {
                log::error!(
                    "Automatic field {} exists on payload, but cannot be deserialized: {}",
                    name,
                    e
                );
                None
            }
        }
    }

    #[inline]
    pub fn id(&self) -> &str {
        &self.id[..]
    }

    #[inline]
    pub fn is_tombstone(&self) -> bool {
        self.deleted
    }

    pub fn from_json(value: JsonValue) -> Result<Payload, serde_json::Error> {
        serde_json::from_value(value)
    }

    /// Deserializes the BSO payload into a specific record type `T`.
    ///
    /// BSO payloads are unstructured JSON objects, with string keys and
    /// dynamically-typed values. `into_record` makes it more convenient to
    /// work with payloads by converting them into data type-specific structs.
    /// Your record type only needs to derive or implement `serde::Deserialize`;
    /// Serde will take care of the rest.
    ///
    /// # Errors
    ///
    /// `into_record` returns errors for type mismatches. As an example, trying
    /// to deserialize a string value from the payload into an integer field in
    /// `T` will fail.
    ///
    /// If there's a chance that a field contains invalid or mistyped data,
    /// you'll want to extract it from `payload.data` manually, instead of using
    /// `into_record`. This has been seen in the wild: for example, `dateAdded`
    /// for bookmarks can be either an integer or a string.
    pub fn into_record<T>(self) -> Result<T, serde_json::Error>
    where
        for<'a> T: Deserialize<'a>,
    {
        serde_json::from_value(JsonValue::from(self))
    }

    pub fn from_record<T: Serialize>(v: T) -> Result<Payload, serde_json::Error> {
        // TODO(issue #2588): This is kind of dumb, we do to_value and then
        // from_value. In general a more strongly typed API would help us avoid
        // this sort of thing... But also concretely this could probably be
        // avoided? At least in some cases.
        Ok(Payload::from_json(serde_json::to_value(v)?)?)
    }

    pub fn into_json_string(self) -> String {
        serde_json::to_string(&JsonValue::from(self))
            .expect("JSON.stringify failed, which shouldn't be possible")
    }
}

impl From<Payload> for JsonValue {
    fn from(cleartext: Payload) -> Self {
        let Payload {
            mut data,
            id,
            deleted,
        } = cleartext;
        data.insert("id".to_string(), JsonValue::String(id.into_string()));
        if deleted {
            data.insert("deleted".to_string(), JsonValue::Bool(true));
        }
        JsonValue::Object(data)
    }
}