diff --git a/ruma-serde/src/canonical_json.rs b/ruma-serde/src/canonical_json.rs new file mode 100644 index 00000000..fc45e214 --- /dev/null +++ b/ruma-serde/src/canonical_json.rs @@ -0,0 +1 @@ +pub mod value; diff --git a/ruma-serde/src/canonical_json/value.rs b/ruma-serde/src/canonical_json/value.rs new file mode 100644 index 00000000..65e4d8ed --- /dev/null +++ b/ruma-serde/src/canonical_json/value.rs @@ -0,0 +1,267 @@ +use std::{ + collections::BTreeMap, + convert::{TryFrom, TryInto}, + fmt, +}; + +use js_int::Int; +use serde::{de::Deserializer, ser::Serializer, Deserialize, Serialize}; +use serde_json::{to_string as to_json_string, Error as JsonError, Value as JsonValue}; + +/// The set of possible errors when serializing to canonical JSON. +#[derive(Debug)] +pub enum Error { + /// The numeric value failed conversion to js_int::Int. + IntConvert, + /// The `CanonicalJsonValue` being serialized was larger than 65,535 bytes. + JsonSize, + /// An error occurred while serializing/deserializing. + SerDe(JsonError), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::IntConvert => f.write_str("number found is not a valid `js_int::Int`"), + Error::JsonSize => f.write_str("JSON is larger than 65,535 byte max"), + Error::SerDe(err) => write!(f, "serde Error: {}", err), + } + } +} + +impl std::error::Error for Error {} + +#[derive(Clone, Eq, PartialEq)] +pub enum CanonicalJsonValue { + /// Represents a JSON null value. + /// + /// ``` + /// # use serde_json::json; + /// # use std::convert::TryInto; + /// # use ruma_serde::CanonicalJsonValue; + /// let v: CanonicalJsonValue = json!(null).try_into().unwrap(); + /// ``` + Null, + + /// Represents a JSON boolean. + /// + /// ``` + /// # use serde_json::json; + /// # use std::convert::TryInto; + /// # use ruma_serde::CanonicalJsonValue; + /// let v: CanonicalJsonValue = json!(true).try_into().unwrap(); + /// ``` + Bool(bool), + + /// Represents a JSON integer. + /// + /// ``` + /// # use serde_json::json; + /// # use std::convert::TryInto; + /// # use ruma_serde::CanonicalJsonValue; + /// let v: CanonicalJsonValue = json!(12).try_into().unwrap(); + /// ``` + Integer(Int), + + /// Represents a JSON string. + /// + /// ``` + /// # use serde_json::json; + /// # use std::convert::TryInto; + /// # use ruma_serde::CanonicalJsonValue; + /// let v: CanonicalJsonValue = json!("a string").try_into().unwrap(); + /// ``` + String(String), + + /// Represents a JSON array. + /// + /// ``` + /// # use serde_json::json; + /// # use std::convert::TryInto; + /// # use ruma_serde::CanonicalJsonValue; + /// let v: CanonicalJsonValue = json!(["an", "array"]).try_into().unwrap(); + /// ``` + Array(Vec), + + /// Represents a JSON object. + /// + /// The map is backed by a BTreeMap to guarantee the sorting of keys. + /// + /// ``` + /// # use serde_json::json; + /// # use std::convert::TryInto; + /// # use ruma_serde::CanonicalJsonValue; + /// let v: CanonicalJsonValue = json!({ "an": "object" }).try_into().unwrap(); + /// ``` + Object(BTreeMap), +} + +impl CanonicalJsonValue { + /// Returns a canonical JSON string according to Matrix specification. + /// + /// The method should be preferred over `serde_json::to_string` since it + /// checks the size of the canonical string. Matrix canonical JSON enforces + /// a size limit of less than 65,535 when sending PDU's for the server-server protocol. + pub fn to_canonical_string(&self) -> Result { + Ok(to_json_string(self).map_err(Error::SerDe).and_then(|s| { + if s.as_bytes().len() > 65_535 { + Err(Error::JsonSize) + } else { + Ok(s) + } + })?) + } +} + +impl fmt::Debug for CanonicalJsonValue { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + match *self { + Self::Null => formatter.debug_tuple("Null").finish(), + Self::Bool(v) => formatter.debug_tuple("Bool").field(&v).finish(), + Self::Integer(ref v) => fmt::Debug::fmt(v, formatter), + Self::String(ref v) => formatter.debug_tuple("String").field(v).finish(), + Self::Array(ref v) => { + formatter.write_str("Array(")?; + fmt::Debug::fmt(v, formatter)?; + formatter.write_str(")") + } + Self::Object(ref v) => { + formatter.write_str("Object(")?; + fmt::Debug::fmt(v, formatter)?; + formatter.write_str(")") + } + } + } +} + +impl fmt::Display for CanonicalJsonValue { + /// Display a JSON value as a string. + /// + /// ``` + /// # use serde_json::json; + /// # + /// let json = json!({ "city": "London", "street": "10 Downing Street" }); + /// + /// // Canonical format: + /// // + /// // {"city":"London","street":"10 Downing Street"} + /// let compact = format!("{}", json); + /// assert_eq!(compact, + /// "{\"city\":\"London\",\"street\":\"10 Downing Street\"}"); + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", to_json_string(&self).map_err(|_| fmt::Error)?) + } +} + +impl TryFrom for CanonicalJsonValue { + type Error = Error; + + fn try_from(json: JsonValue) -> Result { + Ok(match json { + JsonValue::Bool(b) => Self::Bool(b), + JsonValue::Number(num) => Self::Integer( + Int::try_from(num.as_i64().ok_or(Error::IntConvert)?) + .map_err(|_| Error::IntConvert)?, + ), + JsonValue::Array(vec) => { + Self::Array(vec.into_iter().map(TryInto::try_into).collect::, _>>()?) + } + JsonValue::String(string) => Self::String(string), + JsonValue::Object(obj) => Self::Object( + obj.into_iter() + .map(|(k, v)| Ok((k, v.try_into()?))) + .collect::, _>>()?, + ), + JsonValue::Null => Self::Null, + }) + } +} + +impl Serialize for CanonicalJsonValue { + #[inline] + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Self::Null => serializer.serialize_unit(), + Self::Bool(b) => serializer.serialize_bool(*b), + Self::Integer(n) => n.serialize(serializer), + Self::String(s) => serializer.serialize_str(s), + Self::Array(v) => v.serialize(serializer), + Self::Object(m) => { + use serde::ser::SerializeMap; + let mut map = serializer.serialize_map(Some(m.len()))?; + for (k, v) in m { + map.serialize_entry(k, v)?; + } + map.end() + } + } + } +} + +impl<'de> Deserialize<'de> for CanonicalJsonValue { + #[inline] + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let val = JsonValue::deserialize(deserializer)?; + Ok(val.try_into().map_err(serde::de::Error::custom)?) + } +} + +#[cfg(test)] +mod test { + use std::convert::TryInto; + + use super::CanonicalJsonValue; + use serde_json::{from_str as from_json_str, json, to_string as to_json_string}; + + #[test] + fn serialize_canon() { + let json: CanonicalJsonValue = json!({ + "a": [1, 2, 3], + "other": { "stuff": "hello" }, + "string": "Thing" + }) + .try_into() + .unwrap(); + + let ser = json.to_canonical_string().unwrap(); + let back = from_json_str::(&ser).unwrap(); + + assert_eq!(json, back); + } + + #[test] + fn check_canonical_sorts_keys() { + let json: CanonicalJsonValue = json!({ + "auth": { + "success": true, + "mxid": "@john.doe:example.com", + "profile": { + "display_name": "John Doe", + "three_pids": [ + { + "medium": "email", + "address": "john.doe@example.org" + }, + { + "medium": "msisdn", + "address": "123456789" + } + ] + } + } + }) + .try_into() + .unwrap(); + + assert_eq!( + to_json_string(&json).unwrap(), + r#"{"auth":{"mxid":"@john.doe:example.com","profile":{"display_name":"John Doe","three_pids":[{"address":"john.doe@example.org","medium":"email"},{"address":"123456789","medium":"msisdn"}]},"success":true}}"# + ) + } +} diff --git a/ruma-serde/src/lib.rs b/ruma-serde/src/lib.rs index 165c33df..c0d06baf 100644 --- a/ruma-serde/src/lib.rs +++ b/ruma-serde/src/lib.rs @@ -3,6 +3,7 @@ use serde::de::{Deserialize, IntoDeserializer}; pub mod can_be_empty; +mod canonical_json; pub mod duration; pub mod empty; pub mod json_string; @@ -12,6 +13,7 @@ pub mod time; pub mod urlencoded; pub use can_be_empty::{is_empty, CanBeEmpty}; +pub use canonical_json::value::{CanonicalJsonValue, Error as CanonicalError}; pub use empty::vec_as_map_of_empty; /// Check whether a value is equal to its default value.