From fb8039ac6d9f206d8bbd5c409dbd79bd40064e4a Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 3 Jun 2019 11:52:15 -0700 Subject: [PATCH] Support event ID formats for all room versions. --- src/event_id.rs | 217 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 181 insertions(+), 36 deletions(-) diff --git a/src/event_id.rs b/src/event_id.rs index 2729871f..551d502c 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -20,24 +20,59 @@ use crate::{display, error::Error, generate_localpart, parse_id}; /// An `EventId` is generated randomly or converted from a string slice, and can be converted back /// into a string as needed. /// +/// # Room versions +/// +/// Matrix specifies multiple [room versions](https://matrix.org/docs/spec/#room-versions) and the +/// format of event identifiers differ between them. The original format used by room versions 1 +/// and 2 uses a short pseudorandom "localpart" followed by the hostname and port of the +/// originating homeserver. Later room versions change event identifiers to be a hash of the event +/// encoded with Base64. Some of the methods provided by `EventId` are only relevant to the +/// original event format. +/// /// ``` /// # use std::convert::TryFrom; /// # use ruma_identifiers::EventId; +/// // Original format /// assert_eq!( /// EventId::try_from("$h29iv0s8:example.com").unwrap().to_string(), /// "$h29iv0s8:example.com" /// ); +/// // Room version 3 format +/// assert_eq!( +/// EventId::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap().to_string(), +/// "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk" +/// ); +/// // Room version 4 format +/// assert_eq!( +/// EventId::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap().to_string(), +/// "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg" +/// ); /// ``` #[derive(Clone, Debug, Eq, Hash, PartialEq)] #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] -pub struct EventId { +pub struct EventId(Format); + +/// Different event ID formats from the different Matrix room versions. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +enum Format { + /// The original format as used by Matrix room versions 1 and 2. + Original(Original), + /// The format used by Matrix room version 3. + Base64(String), + /// The format used by Matrix room version 4. + UrlSafeBase64(String), +} + +/// An event in the original format as used by Matrix room versions 1 and 2. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +struct Original { /// The hostname of the homeserver. - hostname: Host, + pub hostname: Host, /// The event's unique ID. - opaque_id: String, + pub localpart: String, /// The network port of the homeserver. - port: u16, + pub port: u16, } /// A serde visitor for `EventId`. @@ -45,42 +80,67 @@ struct EventIdVisitor; impl EventId { /// Attempts to generate an `EventId` for the given origin server with a localpart consisting - /// of 18 random ASCII characters. + /// of 18 random ASCII characters. This should only be used for events in the original format + /// as used by Matrix room versions 1 and 2. /// /// Fails if the given origin server name cannot be parsed as a valid host. pub fn new(server_name: &str) -> Result { let event_id = format!("${}:{}", generate_localpart(18), server_name); - let (opaque_id, host, port) = parse_id('$', &event_id)?; + let (localpart, host, port) = parse_id('$', &event_id)?; - Ok(Self { + Ok(Self(Format::Original(Original { hostname: host, - opaque_id: opaque_id.to_string(), + localpart: localpart.to_string(), port, - }) + }))) } /// Returns a `Host` for the event ID, containing the server name (minus the port) of the - /// originating homeserver. + /// originating homeserver. Only applicable to events in the original format as used by Matrix + /// room versions 1 and 2. /// /// The host can be either a domain name, an IPv4 address, or an IPv6 address. - pub fn hostname(&self) -> &Host { - &self.hostname + pub fn hostname(&self) -> Option<&Host> { + if let Format::Original(original) = &self.0 { + Some(&original.hostname) + } else { + None + } } - /// Returns the event's opaque ID. - pub fn opaque_id(&self) -> &str { - &self.opaque_id + /// Returns the event's unique ID. For the original event format as used by Matrix room + /// versions 1 and 2, this is the "localpart" that precedes the homeserver. For later formats, + /// this is the entire ID without the leading $ sigil. + pub fn localpart(&self) -> &str { + match &self.0 { + Format::Original(original) => &original.localpart, + Format::Base64(id) | Format::UrlSafeBase64(id) => id, + } } - /// Returns the port the originating homeserver can be accessed on. - pub fn port(&self) -> u16 { - self.port + /// Returns the port the originating homeserver can be accessed on. Only applicable to events + /// in the original format as used by Matrix room versions 1 and 2. + pub fn port(&self) -> Option { + if let Format::Original(original) = &self.0 { + Some(original.port) + } else { + None + } } } impl Display for EventId { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - display(f, '$', &self.opaque_id, &self.hostname, self.port) + match &self.0 { + Format::Original(original) => display( + f, + '$', + &original.localpart, + &original.hostname, + original.port, + ), + Format::Base64(id) | Format::UrlSafeBase64(id) => write!(f, "${}", id), + } } } @@ -107,16 +167,25 @@ impl<'a> TryFrom<&'a str> for EventId { /// Attempts to create a new Matrix event ID from a string representation. /// - /// The string must include the leading $ sigil, the opaque ID, a literal colon, and a valid - /// server name. + /// If using the original event format as used by Matrix room versions 1 and 2, the string must + /// include the leading $ sigil, the localpart, a literal colon, and a valid homeserver + /// hostname. fn try_from(event_id: &'a str) -> Result { - let (opaque_id, host, port) = parse_id('$', event_id)?; + if event_id.contains(':') { + let (localpart, host, port) = parse_id('$', event_id)?; - Ok(Self { - hostname: host, - opaque_id: opaque_id.to_owned(), - port, - }) + Ok(Self(Format::Original(Original { + hostname: host, + localpart: localpart.to_owned(), + port, + }))) + } else if !event_id.starts_with('$') { + Err(Error::MissingSigil) + } else if event_id.contains(|chr| chr == '+' || chr == '/') { + Ok(Self(Format::Base64(event_id[1..].to_string()))) + } else { + Ok(Self(Format::UrlSafeBase64(event_id[1..].to_string()))) + } } } @@ -148,7 +217,7 @@ mod tests { use crate::error::Error; #[test] - fn valid_event_id() { + fn valid_original_event_id() { assert_eq!( EventId::try_from("$39hvsi03hlne:example.com") .expect("Failed to create EventId.") @@ -157,6 +226,26 @@ mod tests { ); } + #[test] + fn valid_base64_event_id() { + assert_eq!( + EventId::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk") + .expect("Failed to create EventId.") + .to_string(), + "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk" + ) + } + + #[test] + fn valid_url_safe_base64_event_id() { + assert_eq!( + EventId::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg") + .expect("Failed to create EventId.") + .to_string(), + "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg" + ) + } + #[test] fn generate_random_valid_event_id() { let event_id = EventId::new("example.com") @@ -173,7 +262,7 @@ mod tests { } #[test] - fn serialize_valid_event_id() { + fn serialize_valid_original_event_id() { assert_eq!( to_string( &EventId::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.") @@ -184,7 +273,31 @@ mod tests { } #[test] - fn deserialize_valid_event_id() { + fn serialize_valid_base64_event_id() { + assert_eq!( + to_string( + &EventId::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk") + .expect("Failed to create EventId.") + ) + .expect("Failed to convert EventId to JSON."), + r#""$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk""# + ); + } + + #[test] + fn serialize_valid_url_safe_base64_event_id() { + assert_eq!( + to_string( + &EventId::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg") + .expect("Failed to create EventId.") + ) + .expect("Failed to convert EventId to JSON."), + r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""# + ); + } + + #[test] + fn deserialize_valid_original_event_id() { assert_eq!( from_str::(r#""$39hvsi03hlne:example.com""#) .expect("Failed to convert JSON to EventId"), @@ -193,7 +306,27 @@ mod tests { } #[test] - fn valid_event_id_with_explicit_standard_port() { + fn deserialize_valid_base64_event_id() { + assert_eq!( + from_str::(r#""$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk""#) + .expect("Failed to convert JSON to EventId"), + EventId::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk") + .expect("Failed to create EventId.") + ); + } + + #[test] + fn deserialize_valid_url_safe_base64_event_id() { + assert_eq!( + from_str::(r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""#) + .expect("Failed to convert JSON to EventId"), + EventId::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg") + .expect("Failed to create EventId.") + ); + } + + #[test] + fn valid_original_event_id_with_explicit_standard_port() { assert_eq!( EventId::try_from("$39hvsi03hlne:example.com:443") .expect("Failed to create EventId.") @@ -203,7 +336,7 @@ mod tests { } #[test] - fn valid_event_id_with_non_standard_port() { + fn valid_original_event_id_with_non_standard_port() { assert_eq!( EventId::try_from("$39hvsi03hlne:example.com:5000") .expect("Failed to create EventId.") @@ -213,7 +346,7 @@ mod tests { } #[test] - fn missing_event_id_sigil() { + fn missing_original_event_id_sigil() { assert_eq!( EventId::try_from("39hvsi03hlne:example.com").err().unwrap(), Error::MissingSigil @@ -221,10 +354,22 @@ mod tests { } #[test] - fn missing_event_id_delimiter() { + fn missing_base64_event_id_sigil() { assert_eq!( - EventId::try_from("$39hvsi03hlne").err().unwrap(), - Error::MissingDelimiter + EventId::try_from("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk") + .err() + .unwrap(), + Error::MissingSigil + ); + } + + #[test] + fn missing_url_safe_base64_event_id_sigil() { + assert_eq!( + EventId::try_from("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg") + .err() + .unwrap(), + Error::MissingSigil ); }