/* 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/. */ //! This module enhances the IncomingBso and OutgoingBso records to deal with //! arbitrary types, which we call "content" //! It can: //! * Parse JSON into some while handling tombstones and invalid json. //! * Turn arbitrary objects with an `id` field into an OutgoingBso. use super::{IncomingBso, IncomingContent, IncomingKind, OutgoingBso, OutgoingEnvelope}; use crate::Guid; use error_support::report_error; use serde::Serialize; // The only errors we return here are serde errors. type Result = std::result::Result; impl IncomingContent { /// Returns Some(content) if [self.kind] is [IncomingKind::Content], None otherwise. pub fn content(self) -> Option { match self.kind { IncomingKind::Content(t) => Some(t), _ => None, } } } // We don't want to force our T to be Debug, but we can be Debug if T is. impl std::fmt::Debug for IncomingKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { IncomingKind::Content(r) => { write!(f, "IncomingKind::Content<{:?}>", r) } IncomingKind::Tombstone => write!(f, "IncomingKind::Tombstone"), IncomingKind::Malformed => write!(f, "IncomingKind::Malformed"), } } } impl IncomingBso { /// Convert an [IncomingBso] to an [IncomingContent] possibly holding a T. pub fn into_content serde::Deserialize<'de>>(self) -> IncomingContent { match serde_json::from_str(&self.payload) { Ok(json) => { // We got a good serde_json::Value, see if it's a . let kind = json_to_kind(json, &self.envelope.id); IncomingContent { envelope: self.envelope, kind, } } Err(e) => { // payload isn't valid json. log::warn!("Invalid incoming cleartext {}: {}", self.envelope.id, e); IncomingContent { envelope: self.envelope, kind: IncomingKind::Malformed, } } } } } impl OutgoingBso { /// Creates a new tombstone record. /// Not all collections expect tombstones. pub fn new_tombstone(envelope: OutgoingEnvelope) -> Self { Self { envelope, payload: serde_json::json!({"deleted": true}).to_string(), } } /// Creates a outgoing record from some , which can be made into a JSON object /// with a valid `id`. This is the most convenient way to create an outgoing /// item from a when the default envelope is suitable. /// Will panic if there's no good `id` in the json. pub fn from_content_with_id(record: T) -> Result where T: Serialize, { let (json, id) = content_with_id_to_json(record)?; Ok(Self { envelope: id.into(), payload: serde_json::to_string(&json)?, }) } /// Create an Outgoing record with an explicit envelope. Will panic if the /// payload has an ID but it doesn't match the envelope. pub fn from_content(envelope: OutgoingEnvelope, record: T) -> Result where T: Serialize, { let json = content_to_json(record, &envelope.id)?; Ok(Self { envelope, payload: serde_json::to_string(&json)?, }) } } // Helpers for packing and unpacking serde objects to and from a . In particular: // * Helping deal complications around raw json payload not having 'id' (the envelope is // canonical) but needing it to exist when dealing with serde locally. // For example, a record on the server after being decrypted looks like: // `{"id": "a-guid", payload: {"field": "value"}}` // But the `T` for this typically looks like `struct T { id: Guid, field: String}` // So before we try and deserialize this record into a T, we copy the `id` field // from the envelope into the payload, and when serializing from a T we do the // reverse (ie, ensure the `id` in the payload is removed and placed in the envelope) // * Tombstones. // Deserializing json into a T fn json_to_kind(mut json: serde_json::Value, id: &Guid) -> IncomingKind where T: for<'de> serde::Deserialize<'de>, { // It's possible that the payload does not carry 'id', but always does - so grab it from the // envelope and put it into the json before deserializing the record. if let serde_json::Value::Object(ref mut map) = json { if map.contains_key("deleted") { return IncomingKind::Tombstone; } match map.get("id") { Some(serde_json::Value::String(content_id)) => { // It exists in the payload! We treat a mismatch as malformed. if content_id != id { log::trace!( "malformed incoming record: envelope id: {} payload id: {}", content_id, id ); report_error!( "incoming-invalid-mismatched-ids", "Envelope and payload don't agree on the ID" ); return IncomingKind::Malformed; } if !id.is_valid_for_sync_server() { log::trace!("malformed incoming record: id is not valid: {}", id); report_error!( "incoming-invalid-bad-payload-id", "ID in the payload is invalid" ); return IncomingKind::Malformed; } } Some(v) => { // It exists in the payload but is not a string - they can't possibly be // the same as the envelope uses a String, so must be malformed. log::trace!("malformed incoming record: id is not a string: {}", v); report_error!("incoming-invalid-wrong_type", "ID is not a string"); return IncomingKind::Malformed; } None => { // Doesn't exist in the payload - add it before trying to deser a T. if !id.is_valid_for_sync_server() { log::trace!("malformed incoming record: id is not valid: {}", id); report_error!( "incoming-invalid-bad-envelope-id", "ID in envelope is not valid" ); return IncomingKind::Malformed; } map.insert("id".to_string(), id.to_string().into()); } } }; match serde_json::from_value(json) { Ok(v) => IncomingKind::Content(v), Err(e) => { report_error!("invalid-incoming-content", "Invalid incoming T: {}", e); IncomingKind::Malformed } } } // Serializing into json with special handling of `id` (the `id` from the payload // is used as the envelope ID) fn content_with_id_to_json(record: T) -> Result<(serde_json::Value, Guid)> where T: Serialize, { let mut json = serde_json::to_value(record)?; let id = match json.as_object_mut() { Some(ref mut map) => { match map.get("id").as_ref().and_then(|v| v.as_str()) { Some(id) => { let id: Guid = id.into(); assert!(id.is_valid_for_sync_server(), "record's ID is invalid"); id } // In practice, this is a "static" error and not influenced by runtime behavior None => panic!("record does not have an ID in the payload"), } } None => panic!("record is not a json object"), }; Ok((json, id)) } // Serializing into json with special handling of `id` (if `id` in serialized // JSON already exists, we panic if it doesn't match the envelope. If the serialized // content does not have an `id`, it is added from the envelope) // is used as the envelope ID) fn content_to_json(record: T, id: &Guid) -> Result where T: Serialize, { let mut payload = serde_json::to_value(record)?; if let Some(ref mut map) = payload.as_object_mut() { if let Some(content_id) = map.get("id").as_ref().and_then(|v| v.as_str()) { assert_eq!(content_id, id); assert!(id.is_valid_for_sync_server(), "record's ID is invalid"); } else { map.insert("id".to_string(), serde_json::Value::String(id.to_string())); } }; Ok(payload) } #[cfg(test)] mod tests { use super::*; use crate::bso::IncomingBso; use serde::{Deserialize, Serialize}; use serde_json::json; #[derive(Default, Debug, PartialEq, Serialize, Deserialize)] struct TestStruct { id: Guid, data: u32, } #[test] fn test_content_deser() { env_logger::try_init().ok(); let json = json!({ "id": "test", "payload": json!({"data": 1}).to_string(), }); let incoming: IncomingBso = serde_json::from_value(json).unwrap(); assert_eq!(incoming.envelope.id, "test"); let record = incoming.into_content::().content().unwrap(); let expected = TestStruct { id: Guid::new("test"), data: 1, }; assert_eq!(record, expected); } #[test] fn test_content_deser_empty_id() { env_logger::try_init().ok(); let json = json!({ "id": "", "payload": json!({"data": 1}).to_string(), }); let incoming: IncomingBso = serde_json::from_value(json).unwrap(); // The envelope has an invalid ID, but it's not handled until we try and deserialize // it into a T assert_eq!(incoming.envelope.id, ""); let content = incoming.into_content::(); assert!(matches!(content.kind, IncomingKind::Malformed)); } #[test] fn test_content_deser_invalid() { env_logger::try_init().ok(); // And a non-empty but still invalid guid. let json = json!({ "id": "X".repeat(65), "payload": json!({"data": 1}).to_string(), }); let incoming: IncomingBso = serde_json::from_value(json).unwrap(); let content = incoming.into_content::(); assert!(matches!(content.kind, IncomingKind::Malformed)); } #[test] fn test_content_deser_not_string() { env_logger::try_init().ok(); // A non-string id. let json = json!({ "id": "0", "payload": json!({"id": 0, "data": 1}).to_string(), }); let incoming: IncomingBso = serde_json::from_value(json).unwrap(); let content = incoming.into_content::(); assert!(matches!(content.kind, IncomingKind::Malformed)); } #[test] fn test_content_ser_with_id() { env_logger::try_init().ok(); // When serializing, expect the ID to be in the top-level payload (ie, // in the envelope) but should not appear in the cleartext `payload` part of // the payload. let val = TestStruct { id: Guid::new("test"), data: 1, }; let outgoing = OutgoingBso::from_content_with_id(val).unwrap(); // The envelope should have our ID. assert_eq!(outgoing.envelope.id, Guid::new("test")); // and make sure `cleartext` part of the payload the data and the id. let ct_value = serde_json::from_str::(&outgoing.payload).unwrap(); assert_eq!(ct_value, json!({"data": 1, "id": "test"})); } #[test] fn test_content_ser_with_envelope() { env_logger::try_init().ok(); // When serializing, expect the ID to be in the top-level payload (ie, // in the envelope) but should not appear in the cleartext `payload` let val = TestStruct { id: Guid::new("test"), data: 1, }; let envelope: OutgoingEnvelope = Guid::new("test").into(); let outgoing = OutgoingBso::from_content(envelope, val).unwrap(); // The envelope should have our ID. assert_eq!(outgoing.envelope.id, Guid::new("test")); // and make sure `cleartext` part of the payload has data and the id. let ct_value = serde_json::from_str::(&outgoing.payload).unwrap(); assert_eq!(ct_value, json!({"data": 1, "id": "test"})); } #[test] #[should_panic] fn test_content_ser_no_ids() { env_logger::try_init().ok(); #[derive(Serialize)] struct StructWithNoId { data: u32, } let val = StructWithNoId { data: 1 }; let _ = OutgoingBso::from_content_with_id(val); } #[test] #[should_panic] fn test_content_ser_not_object() { env_logger::try_init().ok(); let _ = OutgoingBso::from_content_with_id(json!("string")); } #[test] #[should_panic] fn test_content_ser_mismatched_ids() { env_logger::try_init().ok(); let val = TestStruct { id: Guid::new("test"), data: 1, }; let envelope: OutgoingEnvelope = Guid::new("different").into(); let _ = OutgoingBso::from_content(envelope, val); } #[test] #[should_panic] fn test_content_empty_id() { env_logger::try_init().ok(); let val = TestStruct { id: Guid::new(""), data: 1, }; let _ = OutgoingBso::from_content_with_id(val); } #[test] #[should_panic] fn test_content_invalid_id() { env_logger::try_init().ok(); let val = TestStruct { id: Guid::new(&"X".repeat(65)), data: 1, }; let _ = OutgoingBso::from_content_with_id(val); } }