/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis *file,Youcanobtainoneathttp://mozilla.org/MPL/2.0/.
*/
//! This module enhances the IncomingBso and OutgoingBso records to deal with //! arbitrary <T> types, which we call "content" //! It can: //! * Parse JSON into some <T> while handling tombstones and invalid json. //! * Turn arbitrary <T> objects with an `id` field into an OutgoingBso.
usesuper::{IncomingBso, IncomingContent, IncomingKind, OutgoingBso, OutgoingEnvelope}; usecrate::Guid; use error_support::report_error; use serde::Serialize;
// The only errors we return here are serde errors. type Result<T> = std::result::Result<T, serde_json::Error>;
// We don't want to force our T to be Debug, but we can be Debug if T is. impl<T: std::fmt::Debug> std::fmt::Debug for IncomingKind<T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { matchself {
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. pubfn into_content<T: for<'de> serde::Deserialize<'de>>(self) -> IncomingContent<T> { self.into_content_with_fixup(|_| {})
}
/// Like into_content, but adds an additional fixup step where the caller can adjust the /// `serde_json::Value' pubfn into_content_with_fixup<T: for<'de> serde::Deserialize<'de>>( self,
fixup: impl FnOnce(&mut serde_json::Value),
) -> IncomingContent<T> { match serde_json::from_str(&self.payload) {
Ok(mut json) => { // We got a good serde_json::Value, run the fixup method
fixup(&mut json); // ...now see if it's a <T>. 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. pubfn new_tombstone(envelope: OutgoingEnvelope) -> Self { Self {
envelope,
payload: serde_json::json!({"deleted": true}).to_string(),
}
}
/// Creates a outgoing record from some <T>, 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 <T> when the default envelope is suitable. /// Will panic if there's no good `id` in the json. pubfn from_content_with_id<T>(record: T) -> Result<Self> 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. pubfn from_content<T>(envelope: OutgoingEnvelope, record: T) -> Result<Self> 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 <T>. 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<T>(mut json: serde_json::Value, id: &Guid) -> IncomingKind<T> where
T: for<'de> serde::Deserialize<'de>,
{ // It's possible that the payload does not carry 'id', but <T> always does - so grab it from the // envelope and put it into the json before deserializing the record. iflet serde_json::Value::Object(refmut 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_path_to_error::deserialize(json) {
Ok(v) => IncomingKind::Content(v),
Err(e) => {
report_error!( "invalid-incoming-content", "{}.{}: {}",
std::any::type_name::<T>(),
e.path(),
e.inner()
);
IncomingKind::Malformed
}
}
}
// Serializing <T> into json with special handling of `id` (the `id` from the payload // is used as the envelope ID) fn content_with_id_to_json<T>(record: T) -> Result<(serde_json::Value, Guid)> where
T: Serialize,
{ letmut json = serde_json::to_value(record)?; let id = match json.as_object_mut() {
Some(refmut 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 <T> 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<T>(record: T, id: &Guid) -> Result<serde_json::Value> where
T: Serialize,
{ letmut payload = serde_json::to_value(record)?; iflet Some(refmut map) = payload.as_object_mut() { iflet 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 { usesuper::*; usecrate::bso::IncomingBso; use serde::{Deserialize, Serialize}; use serde_json::json;
#[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::<TestStruct>();
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::<TestStruct>();
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::<serde_json::Value>();
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::<serde_json::Value>(&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::<serde_json::Value>(&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);
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.