diff --git a/src/diesel_integration.rs b/src/diesel_integration.rs new file mode 100644 index 00000000..8ae777e6 --- /dev/null +++ b/src/diesel_integration.rs @@ -0,0 +1,41 @@ +//! Implements traits from Diesel, allowing identifiers to be used as database fields. + +use std::{convert::TryFrom, error::Error as StdError, io::Write}; + +use diesel::{ + backend::Backend, + deserialize::{FromSql, Result as DeserializeResult}, + serialize::{Output, Result as SerializeResult, ToSql}, + sql_types::Text, +}; + +macro_rules! diesel_impl { + ($name:ident) => { + impl ToSql for $crate::$name + where + DB: Backend, + { + fn to_sql(&self, out: &mut Output) -> SerializeResult { + ToSql::::to_sql(&self.to_string(), out) + } + } + + impl FromSql for $crate::$name + where + String: FromSql, + DB: Backend, + { + fn from_sql(value: Option<&::RawValue>) -> DeserializeResult { + let string = >::from_sql(value)?; + Self::try_from(string.as_str()) + .map_err(|error| Box::new(error) as Box) + } + } + }; +} + +diesel_impl!(EventId); +diesel_impl!(RoomAliasId); +diesel_impl!(RoomId); +diesel_impl!(RoomIdOrAliasId); +diesel_impl!(UserId); diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..a23ece09 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,50 @@ +//! Error conditions. + +use std::{ + error::Error as StdError, + fmt::{Display, Formatter, Result as FmtResult}, +}; + +use url::ParseError; + +/// An error encountered when trying to parse an invalid ID string. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +pub enum Error { + /// The ID's localpart contains invalid characters. + /// + /// Only relevant for user IDs. + InvalidCharacters, + /// The domain part of the the ID string is not a valid IP address or DNS name. + InvalidHost, + /// The ID exceeds 255 bytes. + MaximumLengthExceeded, + /// The ID is less than 4 characters. + MinimumLengthNotSatisfied, + /// The ID is missing the colon delimiter between localpart and server name. + MissingDelimiter, + /// The ID is missing the leading sigil. + MissingSigil, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let message = match *self { + Error::InvalidCharacters => "localpart contains invalid characters", + Error::InvalidHost => "server name is not a valid IP address or domain name", + Error::MaximumLengthExceeded => "ID exceeds 255 bytes", + Error::MinimumLengthNotSatisfied => "ID must be at least 4 characters", + Error::MissingDelimiter => "colon is required between localpart and server name", + Error::MissingSigil => "leading sigil is missing", + }; + + write!(f, "{}", message) + } +} + +impl StdError for Error {} + +impl From for Error { + fn from(_: ParseError) -> Self { + Error::InvalidHost + } +} diff --git a/src/event_id.rs b/src/event_id.rs new file mode 100644 index 00000000..2729871f --- /dev/null +++ b/src/event_id.rs @@ -0,0 +1,248 @@ +//! Matrix event identifiers. + +use std::{ + convert::TryFrom, + fmt::{Display, Formatter, Result as FmtResult}, +}; + +#[cfg(feature = "diesel")] +use diesel::sql_types::Text; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use url::Host; + +use crate::{display, error::Error, generate_localpart, parse_id}; + +/// A Matrix event ID. +/// +/// An `EventId` is generated randomly or converted from a string slice, and can be converted back +/// into a string as needed. +/// +/// ``` +/// # use std::convert::TryFrom; +/// # use ruma_identifiers::EventId; +/// assert_eq!( +/// EventId::try_from("$h29iv0s8:example.com").unwrap().to_string(), +/// "$h29iv0s8:example.com" +/// ); +/// ``` +#[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 { + /// The hostname of the homeserver. + hostname: Host, + /// The event's unique ID. + opaque_id: String, + /// The network port of the homeserver. + port: u16, +} + +/// A serde visitor for `EventId`. +struct EventIdVisitor; + +impl EventId { + /// Attempts to generate an `EventId` for the given origin server with a localpart consisting + /// of 18 random ASCII characters. + /// + /// 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)?; + + Ok(Self { + hostname: host, + opaque_id: opaque_id.to_string(), + port, + }) + } + + /// Returns a `Host` for the event ID, containing the server name (minus the port) of the + /// originating homeserver. + /// + /// The host can be either a domain name, an IPv4 address, or an IPv6 address. + pub fn hostname(&self) -> &Host { + &self.hostname + } + + /// Returns the event's opaque ID. + pub fn opaque_id(&self) -> &str { + &self.opaque_id + } + + /// Returns the port the originating homeserver can be accessed on. + pub fn port(&self) -> u16 { + self.port + } +} + +impl Display for EventId { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + display(f, '$', &self.opaque_id, &self.hostname, self.port) + } +} + +impl Serialize for EventId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for EventId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(EventIdVisitor) + } +} + +impl<'a> TryFrom<&'a str> for EventId { + type Error = Error; + + /// 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. + fn try_from(event_id: &'a str) -> Result { + let (opaque_id, host, port) = parse_id('$', event_id)?; + + Ok(Self { + hostname: host, + opaque_id: opaque_id.to_owned(), + port, + }) + } +} + +impl<'de> Visitor<'de> for EventIdVisitor { + type Value = EventId; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "a Matrix event ID as a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { + match EventId::try_from(v) { + Ok(event_id) => Ok(event_id), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), + } + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use serde_json::{from_str, to_string}; + + use super::EventId; + use crate::error::Error; + + #[test] + fn valid_event_id() { + assert_eq!( + EventId::try_from("$39hvsi03hlne:example.com") + .expect("Failed to create EventId.") + .to_string(), + "$39hvsi03hlne:example.com" + ); + } + + #[test] + fn generate_random_valid_event_id() { + let event_id = EventId::new("example.com") + .expect("Failed to generate EventId.") + .to_string(); + + assert!(event_id.to_string().starts_with('$')); + assert_eq!(event_id.len(), 31); + } + + #[test] + fn generate_random_invalid_event_id() { + assert!(EventId::new("").is_err()); + } + + #[test] + fn serialize_valid_event_id() { + assert_eq!( + to_string( + &EventId::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.") + ) + .expect("Failed to convert EventId to JSON."), + r#""$39hvsi03hlne:example.com""# + ); + } + + #[test] + fn deserialize_valid_event_id() { + assert_eq!( + from_str::(r#""$39hvsi03hlne:example.com""#) + .expect("Failed to convert JSON to EventId"), + EventId::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.") + ); + } + + #[test] + fn valid_event_id_with_explicit_standard_port() { + assert_eq!( + EventId::try_from("$39hvsi03hlne:example.com:443") + .expect("Failed to create EventId.") + .to_string(), + "$39hvsi03hlne:example.com" + ); + } + + #[test] + fn valid_event_id_with_non_standard_port() { + assert_eq!( + EventId::try_from("$39hvsi03hlne:example.com:5000") + .expect("Failed to create EventId.") + .to_string(), + "$39hvsi03hlne:example.com:5000" + ); + } + + #[test] + fn missing_event_id_sigil() { + assert_eq!( + EventId::try_from("39hvsi03hlne:example.com").err().unwrap(), + Error::MissingSigil + ); + } + + #[test] + fn missing_event_id_delimiter() { + assert_eq!( + EventId::try_from("$39hvsi03hlne").err().unwrap(), + Error::MissingDelimiter + ); + } + + #[test] + fn invalid_event_id_host() { + assert_eq!( + EventId::try_from("$39hvsi03hlne:-").err().unwrap(), + Error::InvalidHost + ); + } + + #[test] + fn invalid_event_id_port() { + assert_eq!( + EventId::try_from("$39hvsi03hlne:example.com:notaport") + .err() + .unwrap(), + Error::InvalidHost + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index afd216b4..4672f296 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,25 +31,26 @@ #[cfg_attr(feature = "diesel", macro_use)] extern crate diesel; -use std::{ - convert::TryFrom, - error::Error as StdError, - fmt::{Display, Formatter, Result as FmtResult}, +use std::fmt::{Formatter, Result as FmtResult}; + +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use url::Url; + +pub use url::Host; + +pub use crate::{ + error::Error, event_id::EventId, room_alias_id::RoomAliasId, room_id::RoomId, + room_id_or_room_alias_id::RoomIdOrAliasId, user_id::UserId, }; #[cfg(feature = "diesel")] -use diesel::sql_types::Text; - -use lazy_static::lazy_static; -use rand::{distributions::Alphanumeric, thread_rng, Rng}; -use regex::Regex; -use serde::{ - de::{Error as SerdeError, Unexpected, Visitor}, - Deserialize, Deserializer, Serialize, Serializer, -}; -use url::{ParseError, Url}; - -pub use url::Host; +mod diesel_integration; +mod error; +mod event_id; +mod room_alias_id; +mod room_id; +mod room_id_or_room_alias_id; +mod user_id; /// All events must be 255 bytes or less. const MAX_BYTES: usize = 255; @@ -61,170 +62,6 @@ const MIN_CHARS: usize = 4; /// The number of bytes in a valid sigil. const SIGIL_BYTES: usize = 1; -lazy_static! { - static ref USER_LOCALPART_PATTERN: Regex = - Regex::new(r"\A[a-z0-9._=-]+\z").expect("Failed to create user localpart regex."); -} - -/// An error encountered when trying to parse an invalid ID string. -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] -pub enum Error { - /// The ID's localpart contains invalid characters. - /// - /// Only relevant for user IDs. - InvalidCharacters, - /// The domain part of the the ID string is not a valid IP address or DNS name. - InvalidHost, - /// The ID exceeds 255 bytes. - MaximumLengthExceeded, - /// The ID is less than 4 characters. - MinimumLengthNotSatisfied, - /// The ID is missing the colon delimiter between localpart and server name. - MissingDelimiter, - /// The ID is missing the leading sigil. - MissingSigil, -} - -/// A Matrix event ID. -/// -/// An `EventId` is generated randomly or converted from a string slice, and can be converted back -/// into a string as needed. -/// -/// ``` -/// # use std::convert::TryFrom; -/// # use ruma_identifiers::EventId; -/// assert_eq!( -/// EventId::try_from("$h29iv0s8:example.com").unwrap().to_string(), -/// "$h29iv0s8:example.com" -/// ); -/// ``` -#[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 { - /// The hostname of the homeserver. - hostname: Host, - /// The event's unique ID. - opaque_id: String, - /// The network port of the homeserver. - port: u16, -} - -/// A Matrix room alias ID. -/// -/// A `RoomAliasId` is converted from a string slice, and can be converted back into a string as -/// needed. -/// -/// ``` -/// # use std::convert::TryFrom; -/// # use ruma_identifiers::RoomAliasId; -/// assert_eq!( -/// RoomAliasId::try_from("#ruma:example.com").unwrap().to_string(), -/// "#ruma:example.com" -/// ); -/// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] -#[cfg_attr(feature = "diesel", sql_type = "Text")] -pub struct RoomAliasId { - /// The alias for the room. - alias: String, - /// The hostname of the homeserver. - hostname: Host, - /// The network port of the homeserver. - port: u16, -} - -/// A Matrix room ID. -/// -/// A `RoomId` is generated randomly or converted from a string slice, and can be converted back -/// into a string as needed. -/// -/// ``` -/// # use std::convert::TryFrom; -/// # use ruma_identifiers::RoomId; -/// assert_eq!( -/// RoomId::try_from("!n8f893n9:example.com").unwrap().to_string(), -/// "!n8f893n9:example.com" -/// ); -/// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] -#[cfg_attr(feature = "diesel", sql_type = "Text")] -pub struct RoomId { - /// The hostname of the homeserver. - hostname: Host, - /// The room's unique ID. - opaque_id: String, - /// The network port of the homeserver. - port: u16, -} - -/// A Matrix room ID or a Matrix room alias ID. -/// -/// `RoomIdOrAliasId` is useful for APIs that accept either kind of room identifier. It is converted -/// from a string slice, and can be converted back into a string as needed. When converted from a -/// string slice, the variant is determined by the leading sigil character. -/// -/// ``` -/// # use std::convert::TryFrom; -/// # use ruma_identifiers::RoomIdOrAliasId; -/// assert_eq!( -/// RoomIdOrAliasId::try_from("#ruma:example.com").unwrap().to_string(), -/// "#ruma:example.com" -/// ); -/// -/// assert_eq!( -/// RoomIdOrAliasId::try_from("!n8f893n9:example.com").unwrap().to_string(), -/// "!n8f893n9:example.com" -/// ); -/// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] -#[cfg_attr(feature = "diesel", sql_type = "Text")] -pub enum RoomIdOrAliasId { - /// A Matrix room alias ID. - RoomAliasId(RoomAliasId), - /// A Matrix room ID. - RoomId(RoomId), -} - -/// A Matrix user ID. -/// -/// A `UserId` is generated randomly or converted from a string slice, and can be converted back -/// into a string as needed. -/// -/// ``` -/// # use std::convert::TryFrom; -/// # use ruma_identifiers::UserId; -/// assert_eq!( -/// UserId::try_from("@carl:example.com").unwrap().to_string(), -/// "@carl:example.com" -/// ); -/// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] -#[cfg_attr(feature = "diesel", sql_type = "Text")] -pub struct UserId { - /// The hostname of the homeserver. - hostname: Host, - /// The user's unique ID. - localpart: String, - /// The network port of the homeserver. - port: u16, -} - -/// A serde visitor for `EventId`. -struct EventIdVisitor; -/// A serde visitor for `RoomAliasId`. -struct RoomAliasIdVisitor; -/// A serde visitor for `RoomId`. -struct RoomIdVisitor; -/// A serde visitor for `RoomIdOrAliasId`. -struct RoomIdOrAliasIdVisitor; -/// A serde visitor for `UserId`. -struct UserIdVisitor; - /// `Display` implementation shared by identifier types. fn display( f: &mut Formatter<'_>, @@ -288,1017 +125,3 @@ fn parse_id(required_sigil: char, id: &str) -> Result<(&str, Host, u16), Error> Ok((localpart, host, port)) } - -impl Display for Error { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "{}", self.description()) - } -} - -impl StdError for Error { - fn description(&self) -> &str { - match *self { - Error::InvalidCharacters => "localpart contains invalid characters", - Error::InvalidHost => "server name is not a valid IP address or domain name", - Error::MaximumLengthExceeded => "ID exceeds 255 bytes", - Error::MinimumLengthNotSatisfied => "ID must be at least 4 characters", - Error::MissingDelimiter => "colon is required between localpart and server name", - Error::MissingSigil => "leading sigil is missing", - } - } -} - -impl EventId { - /// Attempts to generate an `EventId` for the given origin server with a localpart consisting - /// of 18 random ASCII characters. - /// - /// 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)?; - - Ok(Self { - hostname: host, - opaque_id: opaque_id.to_string(), - port, - }) - } - - /// Returns a `Host` for the event ID, containing the server name (minus the port) of the - /// originating homeserver. - /// - /// The host can be either a domain name, an IPv4 address, or an IPv6 address. - pub fn hostname(&self) -> &Host { - &self.hostname - } - - /// Returns the event's opaque ID. - pub fn opaque_id(&self) -> &str { - &self.opaque_id - } - - /// Returns the port the originating homeserver can be accessed on. - pub fn port(&self) -> u16 { - self.port - } -} - -impl RoomId { - /// Attempts to generate a `RoomId` for the given origin server with a localpart consisting of - /// 18 random ASCII characters. - /// - /// Fails if the given origin server name cannot be parsed as a valid host. - pub fn new(server_name: &str) -> Result { - let room_id = format!("!{}:{}", generate_localpart(18), server_name); - let (opaque_id, host, port) = parse_id('!', &room_id)?; - - Ok(Self { - hostname: host, - opaque_id: opaque_id.to_string(), - port, - }) - } - - /// Returns a `Host` for the room ID, containing the server name (minus the port) of the - /// originating homeserver. - /// - /// The host can be either a domain name, an IPv4 address, or an IPv6 address. - pub fn hostname(&self) -> &Host { - &self.hostname - } - - /// Returns the event's opaque ID. - pub fn opaque_id(&self) -> &str { - &self.opaque_id - } - - /// Returns the port the originating homeserver can be accessed on. - pub fn port(&self) -> u16 { - self.port - } -} - -impl RoomAliasId { - /// Returns a `Host` for the room alias ID, containing the server name (minus the port) of - /// the originating homeserver. - /// - /// The host can be either a domain name, an IPv4 address, or an IPv6 address. - pub fn hostname(&self) -> &Host { - &self.hostname - } - - /// Returns the room's alias. - pub fn alias(&self) -> &str { - &self.alias - } - - /// Returns the port the originating homeserver can be accessed on. - pub fn port(&self) -> u16 { - self.port - } -} - -impl UserId { - /// Attempts to generate a `UserId` for the given origin server with a localpart consisting of - /// 12 random ASCII characters. - /// - /// Fails if the given origin server name cannot be parsed as a valid host. - pub fn new(server_name: &str) -> Result { - let user_id = format!("@{}:{}", generate_localpart(12).to_lowercase(), server_name); - let (localpart, host, port) = parse_id('@', &user_id)?; - - Ok(Self { - hostname: host, - localpart: localpart.to_string(), - port, - }) - } - - /// Returns a `Host` for the user ID, containing the server name (minus the port) of the - /// originating homeserver. - /// - /// The host can be either a domain name, an IPv4 address, or an IPv6 address. - pub fn hostname(&self) -> &Host { - &self.hostname - } - - /// Returns the user's localpart. - pub fn localpart(&self) -> &str { - &self.localpart - } - - /// Returns the port the originating homeserver can be accessed on. - pub fn port(&self) -> u16 { - self.port - } -} - -impl From for Error { - fn from(_: ParseError) -> Self { - Error::InvalidHost - } -} - -impl Display for EventId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - display(f, '$', &self.opaque_id, &self.hostname, self.port) - } -} - -impl Display for RoomAliasId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - display(f, '#', &self.alias, &self.hostname, self.port) - } -} - -impl Display for RoomId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - display(f, '!', &self.opaque_id, &self.hostname, self.port) - } -} - -impl Display for RoomIdOrAliasId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - match *self { - RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => display( - f, - '#', - &room_alias_id.alias, - &room_alias_id.hostname, - room_alias_id.port, - ), - RoomIdOrAliasId::RoomId(ref room_id) => { - display(f, '!', &room_id.opaque_id, &room_id.hostname, room_id.port) - } - } - } -} - -impl Display for UserId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - display(f, '@', &self.localpart, &self.hostname, self.port) - } -} - -impl Serialize for EventId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl Serialize for RoomAliasId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl Serialize for RoomId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl Serialize for RoomIdOrAliasId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match *self { - RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => { - serializer.serialize_str(&room_alias_id.to_string()) - } - RoomIdOrAliasId::RoomId(ref room_id) => serializer.serialize_str(&room_id.to_string()), - } - } -} - -impl Serialize for UserId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for EventId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(EventIdVisitor) - } -} - -impl<'de> Deserialize<'de> for RoomAliasId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(RoomAliasIdVisitor) - } -} - -impl<'de> Deserialize<'de> for RoomId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(RoomIdVisitor) - } -} - -impl<'de> Deserialize<'de> for RoomIdOrAliasId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(RoomIdOrAliasIdVisitor) - } -} - -impl<'de> Deserialize<'de> for UserId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(UserIdVisitor) - } -} - -impl<'a> TryFrom<&'a str> for EventId { - type Error = Error; - - /// 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. - fn try_from(event_id: &'a str) -> Result { - let (opaque_id, host, port) = parse_id('$', event_id)?; - - Ok(Self { - hostname: host, - opaque_id: opaque_id.to_owned(), - port, - }) - } -} - -impl<'a> TryFrom<&'a str> for RoomAliasId { - type Error = Error; - - /// Attempts to create a new Matrix room alias ID from a string representation. - /// - /// The string must include the leading # sigil, the alias, a literal colon, and a valid - /// server name. - fn try_from(room_id: &'a str) -> Result { - let (alias, host, port) = parse_id('#', room_id)?; - - Ok(Self { - alias: alias.to_owned(), - hostname: host, - port, - }) - } -} - -impl<'a> TryFrom<&'a str> for RoomId { - type Error = Error; - - /// Attempts to create a new Matrix room ID from a string representation. - /// - /// The string must include the leading ! sigil, the opaque ID, a literal colon, and a valid - /// server name. - fn try_from(room_id: &'a str) -> Result { - let (opaque_id, host, port) = parse_id('!', room_id)?; - - Ok(Self { - hostname: host, - opaque_id: opaque_id.to_owned(), - port, - }) - } -} - -impl<'a> TryFrom<&'a str> for RoomIdOrAliasId { - type Error = Error; - - /// Attempts to create a new Matrix room ID or a room alias ID from a string representation. - /// - /// The string must either - /// include the leading ! sigil, the opaque ID, a literal colon, and a valid server name or - /// include the leading # sigil, the alias, a literal colon, and a valid server name. - fn try_from(room_id_or_alias_id: &'a str) -> Result { - validate_id(room_id_or_alias_id)?; - - let mut chars = room_id_or_alias_id.chars(); - - let sigil = chars.nth(0).expect("ID missing first character."); - - match sigil { - '#' => { - let room_alias_id = RoomAliasId::try_from(room_id_or_alias_id)?; - Ok(RoomIdOrAliasId::RoomAliasId(room_alias_id)) - } - '!' => { - let room_id = RoomId::try_from(room_id_or_alias_id)?; - Ok(RoomIdOrAliasId::RoomId(room_id)) - } - _ => Err(Error::MissingSigil), - } - } -} - -impl<'a> TryFrom<&'a str> for UserId { - type Error = Error; - - /// Attempts to create a new Matrix user ID from a string representation. - /// - /// The string must include the leading @ sigil, the localpart, a literal colon, and a valid - /// server name. - fn try_from(user_id: &'a str) -> Result { - let (localpart, host, port) = parse_id('@', user_id)?; - let downcased_localpart = localpart.to_lowercase(); - - if !USER_LOCALPART_PATTERN.is_match(&downcased_localpart) { - return Err(Error::InvalidCharacters); - } - - Ok(Self { - hostname: host, - port, - localpart: downcased_localpart.to_owned(), - }) - } -} - -impl<'de> Visitor<'de> for EventIdVisitor { - type Value = EventId; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a Matrix event ID as a string") - } - - fn visit_str(self, v: &str) -> Result - where - E: SerdeError, - { - match EventId::try_from(v) { - Ok(event_id) => Ok(event_id), - Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), - } - } -} - -impl<'de> Visitor<'de> for RoomAliasIdVisitor { - type Value = RoomAliasId; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a Matrix room alias ID as a string") - } - - fn visit_str(self, v: &str) -> Result - where - E: SerdeError, - { - match RoomAliasId::try_from(v) { - Ok(room_alias_id) => Ok(room_alias_id), - Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), - } - } -} - -impl<'de> Visitor<'de> for RoomIdVisitor { - type Value = RoomId; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a Matrix room ID as a string") - } - - fn visit_str(self, v: &str) -> Result - where - E: SerdeError, - { - match RoomId::try_from(v) { - Ok(room_id) => Ok(room_id), - Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), - } - } -} - -impl<'de> Visitor<'de> for RoomIdOrAliasIdVisitor { - type Value = RoomIdOrAliasId; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a Matrix room ID or room alias ID as a string") - } - - fn visit_str(self, v: &str) -> Result - where - E: SerdeError, - { - match RoomIdOrAliasId::try_from(v) { - Ok(room_id_or_alias_id) => Ok(room_id_or_alias_id), - Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), - } - } -} - -impl<'de> Visitor<'de> for UserIdVisitor { - type Value = UserId; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a Matrix user ID as a string") - } - - fn visit_str(self, v: &str) -> Result - where - E: SerdeError, - { - match UserId::try_from(v) { - Ok(user_id) => Ok(user_id), - Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), - } - } -} - -/// Implements traits from Diesel, allowing identifiers to be used as database fields. -#[cfg(feature = "diesel")] -mod diesel_integration { - use std::{convert::TryFrom, error::Error as StdError, io::Write}; - - use diesel::{ - backend::Backend, - deserialize::{FromSql, Result as DeserializeResult}, - serialize::{Output, Result as SerializeResult, ToSql}, - sql_types::Text, - }; - - macro_rules! diesel_impl { - ($name:ident) => { - impl ToSql for $crate::$name - where - DB: Backend, - { - fn to_sql(&self, out: &mut Output) -> SerializeResult { - ToSql::::to_sql(&self.to_string(), out) - } - } - - impl FromSql for $crate::$name - where - String: FromSql, - DB: Backend, - { - fn from_sql(value: Option<&::RawValue>) -> DeserializeResult { - let string = >::from_sql(value)?; - Self::try_from(string.as_str()) - .map_err(|error| Box::new(error) as Box) - } - } - }; - } - - diesel_impl!(EventId); - diesel_impl!(RoomAliasId); - diesel_impl!(RoomId); - diesel_impl!(RoomIdOrAliasId); - diesel_impl!(UserId); -} - -#[cfg(test)] -mod tests { - use std::convert::TryFrom; - - use serde_json::{from_str, to_string}; - - use super::{Error, EventId, RoomAliasId, RoomId, RoomIdOrAliasId, UserId}; - - #[test] - fn valid_event_id() { - assert_eq!( - EventId::try_from("$39hvsi03hlne:example.com") - .expect("Failed to create EventId.") - .to_string(), - "$39hvsi03hlne:example.com" - ); - } - - #[test] - fn generate_random_valid_event_id() { - let event_id = EventId::new("example.com") - .expect("Failed to generate EventId.") - .to_string(); - - assert!(event_id.to_string().starts_with('$')); - assert_eq!(event_id.len(), 31); - } - - #[test] - fn generate_random_invalid_event_id() { - assert!(EventId::new("").is_err()); - } - - #[test] - fn serialize_valid_event_id() { - assert_eq!( - to_string( - &EventId::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.") - ) - .expect("Failed to convert EventId to JSON."), - r#""$39hvsi03hlne:example.com""# - ); - } - - #[test] - fn deserialize_valid_event_id() { - assert_eq!( - from_str::(r#""$39hvsi03hlne:example.com""#) - .expect("Failed to convert JSON to EventId"), - EventId::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.") - ); - } - - #[test] - fn valid_event_id_with_explicit_standard_port() { - assert_eq!( - EventId::try_from("$39hvsi03hlne:example.com:443") - .expect("Failed to create EventId.") - .to_string(), - "$39hvsi03hlne:example.com" - ); - } - - #[test] - fn valid_event_id_with_non_standard_port() { - assert_eq!( - EventId::try_from("$39hvsi03hlne:example.com:5000") - .expect("Failed to create EventId.") - .to_string(), - "$39hvsi03hlne:example.com:5000" - ); - } - - #[test] - fn missing_event_id_sigil() { - assert_eq!( - EventId::try_from("39hvsi03hlne:example.com").err().unwrap(), - Error::MissingSigil - ); - } - - #[test] - fn missing_event_id_delimiter() { - assert_eq!( - EventId::try_from("$39hvsi03hlne").err().unwrap(), - Error::MissingDelimiter - ); - } - - #[test] - fn invalid_event_id_host() { - assert_eq!( - EventId::try_from("$39hvsi03hlne:-").err().unwrap(), - Error::InvalidHost - ); - } - - #[test] - fn invalid_event_id_port() { - assert_eq!( - EventId::try_from("$39hvsi03hlne:example.com:notaport") - .err() - .unwrap(), - Error::InvalidHost - ); - } - - #[test] - fn valid_room_alias_id() { - assert_eq!( - RoomAliasId::try_from("#ruma:example.com") - .expect("Failed to create RoomAliasId.") - .to_string(), - "#ruma:example.com" - ); - } - - #[test] - fn serialize_valid_room_alias_id() { - assert_eq!( - to_string( - &RoomAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") - ) - .expect("Failed to convert RoomAliasId to JSON."), - r##""#ruma:example.com""## - ); - } - - #[test] - fn deserialize_valid_room_alias_id() { - assert_eq!( - from_str::(r##""#ruma:example.com""##) - .expect("Failed to convert JSON to RoomAliasId"), - RoomAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") - ); - } - - #[test] - fn valid_room_alias_id_with_explicit_standard_port() { - assert_eq!( - RoomAliasId::try_from("#ruma:example.com:443") - .expect("Failed to create RoomAliasId.") - .to_string(), - "#ruma:example.com" - ); - } - - #[test] - fn valid_room_alias_id_with_non_standard_port() { - assert_eq!( - RoomAliasId::try_from("#ruma:example.com:5000") - .expect("Failed to create RoomAliasId.") - .to_string(), - "#ruma:example.com:5000" - ); - } - - #[test] - fn valid_room_alias_id_unicode() { - assert_eq!( - RoomAliasId::try_from("#老虎£я:example.com") - .expect("Failed to create RoomAliasId.") - .to_string(), - "#老虎£я:example.com" - ); - } - - #[test] - fn missing_room_alias_id_sigil() { - assert_eq!( - RoomAliasId::try_from("39hvsi03hlne:example.com") - .err() - .unwrap(), - Error::MissingSigil - ); - } - - #[test] - fn missing_room_alias_id_delimiter() { - assert_eq!( - RoomAliasId::try_from("#ruma").err().unwrap(), - Error::MissingDelimiter - ); - } - - #[test] - fn invalid_room_alias_id_host() { - assert_eq!( - RoomAliasId::try_from("#ruma:-").err().unwrap(), - Error::InvalidHost - ); - } - - #[test] - fn invalid_room_alias_id_port() { - assert_eq!( - RoomAliasId::try_from("#ruma:example.com:notaport") - .err() - .unwrap(), - Error::InvalidHost - ); - } - - #[test] - fn valid_room_id() { - assert_eq!( - RoomId::try_from("!29fhd83h92h0:example.com") - .expect("Failed to create RoomId.") - .to_string(), - "!29fhd83h92h0:example.com" - ); - } - - #[test] - fn generate_random_valid_room_id() { - let room_id = RoomId::new("example.com") - .expect("Failed to generate RoomId.") - .to_string(); - - assert!(room_id.to_string().starts_with('!')); - assert_eq!(room_id.len(), 31); - } - - #[test] - fn generate_random_invalid_room_id() { - assert!(RoomId::new("").is_err()); - } - - #[test] - fn serialize_valid_room_id() { - assert_eq!( - to_string( - &RoomId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") - ) - .expect("Failed to convert RoomId to JSON."), - r#""!29fhd83h92h0:example.com""# - ); - } - - #[test] - fn deserialize_valid_room_id() { - assert_eq!( - from_str::(r#""!29fhd83h92h0:example.com""#) - .expect("Failed to convert JSON to RoomId"), - RoomId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") - ); - } - - #[test] - fn valid_room_id_with_explicit_standard_port() { - assert_eq!( - RoomId::try_from("!29fhd83h92h0:example.com:443") - .expect("Failed to create RoomId.") - .to_string(), - "!29fhd83h92h0:example.com" - ); - } - - #[test] - fn valid_room_id_with_non_standard_port() { - assert_eq!( - RoomId::try_from("!29fhd83h92h0:example.com:5000") - .expect("Failed to create RoomId.") - .to_string(), - "!29fhd83h92h0:example.com:5000" - ); - } - - #[test] - fn missing_room_id_sigil() { - assert_eq!( - RoomId::try_from("carl:example.com").err().unwrap(), - Error::MissingSigil - ); - } - - #[test] - fn missing_room_id_delimiter() { - assert_eq!( - RoomId::try_from("!29fhd83h92h0").err().unwrap(), - Error::MissingDelimiter - ); - } - - #[test] - fn invalid_room_id_host() { - assert_eq!( - RoomId::try_from("!29fhd83h92h0:-").err().unwrap(), - Error::InvalidHost - ); - } - - #[test] - fn invalid_room_id_port() { - assert_eq!( - RoomId::try_from("!29fhd83h92h0:example.com:notaport") - .err() - .unwrap(), - Error::InvalidHost - ); - } - - #[test] - fn valid_room_id_or_alias_id_with_a_room_alias_id() { - assert_eq!( - RoomIdOrAliasId::try_from("#ruma:example.com") - .expect("Failed to create RoomAliasId.") - .to_string(), - "#ruma:example.com" - ); - } - - #[test] - fn valid_room_id_or_alias_id_with_a_room_id() { - assert_eq!( - RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") - .expect("Failed to create RoomId.") - .to_string(), - "!29fhd83h92h0:example.com" - ); - } - - #[test] - fn missing_sigil_for_room_id_or_alias_id() { - assert_eq!( - RoomIdOrAliasId::try_from("ruma:example.com").err().unwrap(), - Error::MissingSigil - ); - } - - #[test] - fn serialize_valid_room_id_or_alias_id_with_a_room_alias_id() { - assert_eq!( - to_string( - &RoomIdOrAliasId::try_from("#ruma:example.com") - .expect("Failed to create RoomAliasId.") - ) - .expect("Failed to convert RoomAliasId to JSON."), - r##""#ruma:example.com""## - ); - } - - #[test] - fn serialize_valid_room_id_or_alias_id_with_a_room_id() { - assert_eq!( - to_string( - &RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") - .expect("Failed to create RoomId.") - ) - .expect("Failed to convert RoomId to JSON."), - r#""!29fhd83h92h0:example.com""# - ); - } - - #[test] - fn deserialize_valid_room_id_or_alias_id_with_a_room_alias_id() { - assert_eq!( - from_str::(r##""#ruma:example.com""##) - .expect("Failed to convert JSON to RoomAliasId"), - RoomIdOrAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") - ); - } - - #[test] - fn deserialize_valid_room_id_or_alias_id_with_a_room_id() { - assert_eq!( - from_str::(r##""!29fhd83h92h0:example.com""##) - .expect("Failed to convert JSON to RoomId"), - RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") - .expect("Failed to create RoomAliasId.") - ); - } - - #[test] - fn valid_user_id() { - assert_eq!( - UserId::try_from("@carl:example.com") - .expect("Failed to create UserId.") - .to_string(), - "@carl:example.com" - ); - } - - #[test] - fn downcase_user_id() { - assert_eq!( - UserId::try_from("@CARL:example.com") - .expect("Failed to create UserId.") - .to_string(), - "@carl:example.com" - ); - } - - #[test] - fn generate_random_valid_user_id() { - let user_id = UserId::new("example.com") - .expect("Failed to generate UserId.") - .to_string(); - - assert!(user_id.to_string().starts_with('@')); - assert_eq!(user_id.len(), 25); - } - - #[test] - fn generate_random_invalid_user_id() { - assert!(UserId::new("").is_err()); - } - - #[test] - fn serialize_valid_user_id() { - assert_eq!( - to_string(&UserId::try_from("@carl:example.com").expect("Failed to create UserId.")) - .expect("Failed to convert UserId to JSON."), - r#""@carl:example.com""# - ); - } - - #[test] - fn deserialize_valid_user_id() { - assert_eq!( - from_str::(r#""@carl:example.com""#).expect("Failed to convert JSON to UserId"), - UserId::try_from("@carl:example.com").expect("Failed to create UserId.") - ); - } - - #[test] - fn valid_user_id_with_explicit_standard_port() { - assert_eq!( - UserId::try_from("@carl:example.com:443") - .expect("Failed to create UserId.") - .to_string(), - "@carl:example.com" - ); - } - - #[test] - fn valid_user_id_with_non_standard_port() { - assert_eq!( - UserId::try_from("@carl:example.com:5000") - .expect("Failed to create UserId.") - .to_string(), - "@carl:example.com:5000" - ); - } - - #[test] - fn invalid_characters_in_user_id_localpart() { - assert_eq!( - UserId::try_from("@%%%:example.com").err().unwrap(), - Error::InvalidCharacters - ); - } - - #[test] - fn missing_user_id_sigil() { - assert_eq!( - UserId::try_from("carl:example.com").err().unwrap(), - Error::MissingSigil - ); - } - - #[test] - fn missing_user_id_delimiter() { - assert_eq!( - UserId::try_from("@carl").err().unwrap(), - Error::MissingDelimiter - ); - } - - #[test] - fn invalid_user_id_host() { - assert_eq!( - UserId::try_from("@carl:-").err().unwrap(), - Error::InvalidHost - ); - } - - #[test] - fn invalid_user_id_port() { - assert_eq!( - UserId::try_from("@carl:example.com:notaport") - .err() - .unwrap(), - Error::InvalidHost - ); - } -} diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs new file mode 100644 index 00000000..1d6c1de4 --- /dev/null +++ b/src/room_alias_id.rs @@ -0,0 +1,230 @@ +//! Matrix room alias identifiers. + +use std::{ + convert::TryFrom, + fmt::{Display, Formatter, Result as FmtResult}, +}; + +#[cfg(feature = "diesel")] +use diesel::sql_types::Text; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use url::Host; + +use crate::{display, error::Error, parse_id}; + +/// A Matrix room alias ID. +/// +/// A `RoomAliasId` is converted from a string slice, and can be converted back into a string as +/// needed. +/// +/// ``` +/// # use std::convert::TryFrom; +/// # use ruma_identifiers::RoomAliasId; +/// assert_eq!( +/// RoomAliasId::try_from("#ruma:example.com").unwrap().to_string(), +/// "#ruma:example.com" +/// ); +/// ``` +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] +#[cfg_attr(feature = "diesel", sql_type = "Text")] +pub struct RoomAliasId { + /// The alias for the room. + alias: String, + /// The hostname of the homeserver. + hostname: Host, + /// The network port of the homeserver. + port: u16, +} + +/// A serde visitor for `RoomAliasId`. +struct RoomAliasIdVisitor; + +impl RoomAliasId { + /// Returns a `Host` for the room alias ID, containing the server name (minus the port) of + /// the originating homeserver. + /// + /// The host can be either a domain name, an IPv4 address, or an IPv6 address. + pub fn hostname(&self) -> &Host { + &self.hostname + } + + /// Returns the room's alias. + pub fn alias(&self) -> &str { + &self.alias + } + + /// Returns the port the originating homeserver can be accessed on. + pub fn port(&self) -> u16 { + self.port + } +} + +impl Display for RoomAliasId { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + display(f, '#', &self.alias, &self.hostname, self.port) + } +} + +impl Serialize for RoomAliasId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for RoomAliasId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(RoomAliasIdVisitor) + } +} + +impl<'a> TryFrom<&'a str> for RoomAliasId { + type Error = Error; + + /// Attempts to create a new Matrix room alias ID from a string representation. + /// + /// The string must include the leading # sigil, the alias, a literal colon, and a valid + /// server name. + fn try_from(room_id: &'a str) -> Result { + let (alias, host, port) = parse_id('#', room_id)?; + + Ok(Self { + alias: alias.to_owned(), + hostname: host, + port, + }) + } +} + +impl<'de> Visitor<'de> for RoomAliasIdVisitor { + type Value = RoomAliasId; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "a Matrix room alias ID as a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { + match RoomAliasId::try_from(v) { + Ok(room_alias_id) => Ok(room_alias_id), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), + } + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use serde_json::{from_str, to_string}; + + use super::RoomAliasId; + use crate::error::Error; + + #[test] + fn valid_room_alias_id() { + assert_eq!( + RoomAliasId::try_from("#ruma:example.com") + .expect("Failed to create RoomAliasId.") + .to_string(), + "#ruma:example.com" + ); + } + + #[test] + fn serialize_valid_room_alias_id() { + assert_eq!( + to_string( + &RoomAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") + ) + .expect("Failed to convert RoomAliasId to JSON."), + r##""#ruma:example.com""## + ); + } + + #[test] + fn deserialize_valid_room_alias_id() { + assert_eq!( + from_str::(r##""#ruma:example.com""##) + .expect("Failed to convert JSON to RoomAliasId"), + RoomAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") + ); + } + + #[test] + fn valid_room_alias_id_with_explicit_standard_port() { + assert_eq!( + RoomAliasId::try_from("#ruma:example.com:443") + .expect("Failed to create RoomAliasId.") + .to_string(), + "#ruma:example.com" + ); + } + + #[test] + fn valid_room_alias_id_with_non_standard_port() { + assert_eq!( + RoomAliasId::try_from("#ruma:example.com:5000") + .expect("Failed to create RoomAliasId.") + .to_string(), + "#ruma:example.com:5000" + ); + } + + #[test] + fn valid_room_alias_id_unicode() { + assert_eq!( + RoomAliasId::try_from("#老虎£я:example.com") + .expect("Failed to create RoomAliasId.") + .to_string(), + "#老虎£я:example.com" + ); + } + + #[test] + fn missing_room_alias_id_sigil() { + assert_eq!( + RoomAliasId::try_from("39hvsi03hlne:example.com") + .err() + .unwrap(), + Error::MissingSigil + ); + } + + #[test] + fn missing_room_alias_id_delimiter() { + assert_eq!( + RoomAliasId::try_from("#ruma").err().unwrap(), + Error::MissingDelimiter + ); + } + + #[test] + fn invalid_room_alias_id_host() { + assert_eq!( + RoomAliasId::try_from("#ruma:-").err().unwrap(), + Error::InvalidHost + ); + } + + #[test] + fn invalid_room_alias_id_port() { + assert_eq!( + RoomAliasId::try_from("#ruma:example.com:notaport") + .err() + .unwrap(), + Error::InvalidHost + ); + } +} diff --git a/src/room_id.rs b/src/room_id.rs new file mode 100644 index 00000000..6c66c883 --- /dev/null +++ b/src/room_id.rs @@ -0,0 +1,248 @@ +//! Matrix room identifiers. + +use std::{ + convert::TryFrom, + fmt::{Display, Formatter, Result as FmtResult}, +}; + +#[cfg(feature = "diesel")] +use diesel::sql_types::Text; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use url::Host; + +use crate::{display, error::Error, generate_localpart, parse_id}; + +/// A Matrix room ID. +/// +/// A `RoomId` is generated randomly or converted from a string slice, and can be converted back +/// into a string as needed. +/// +/// ``` +/// # use std::convert::TryFrom; +/// # use ruma_identifiers::RoomId; +/// assert_eq!( +/// RoomId::try_from("!n8f893n9:example.com").unwrap().to_string(), +/// "!n8f893n9:example.com" +/// ); +/// ``` +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] +#[cfg_attr(feature = "diesel", sql_type = "Text")] +pub struct RoomId { + /// The hostname of the homeserver. + hostname: Host, + /// The room's unique ID. + opaque_id: String, + /// The network port of the homeserver. + port: u16, +} + +/// A serde visitor for `RoomId`. +struct RoomIdVisitor; + +impl RoomId { + /// Attempts to generate a `RoomId` for the given origin server with a localpart consisting of + /// 18 random ASCII characters. + /// + /// Fails if the given origin server name cannot be parsed as a valid host. + pub fn new(server_name: &str) -> Result { + let room_id = format!("!{}:{}", generate_localpart(18), server_name); + let (opaque_id, host, port) = parse_id('!', &room_id)?; + + Ok(Self { + hostname: host, + opaque_id: opaque_id.to_string(), + port, + }) + } + + /// Returns a `Host` for the room ID, containing the server name (minus the port) of the + /// originating homeserver. + /// + /// The host can be either a domain name, an IPv4 address, or an IPv6 address. + pub fn hostname(&self) -> &Host { + &self.hostname + } + + /// Returns the event's opaque ID. + pub fn opaque_id(&self) -> &str { + &self.opaque_id + } + + /// Returns the port the originating homeserver can be accessed on. + pub fn port(&self) -> u16 { + self.port + } +} + +impl Display for RoomId { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + display(f, '!', &self.opaque_id, &self.hostname, self.port) + } +} + +impl Serialize for RoomId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for RoomId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(RoomIdVisitor) + } +} + +impl<'a> TryFrom<&'a str> for RoomId { + type Error = Error; + + /// Attempts to create a new Matrix room ID from a string representation. + /// + /// The string must include the leading ! sigil, the opaque ID, a literal colon, and a valid + /// server name. + fn try_from(room_id: &'a str) -> Result { + let (opaque_id, host, port) = parse_id('!', room_id)?; + + Ok(Self { + hostname: host, + opaque_id: opaque_id.to_owned(), + port, + }) + } +} + +impl<'de> Visitor<'de> for RoomIdVisitor { + type Value = RoomId; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "a Matrix room ID as a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { + match RoomId::try_from(v) { + Ok(room_id) => Ok(room_id), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), + } + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use serde_json::{from_str, to_string}; + + use super::RoomId; + use crate::error::Error; + + #[test] + fn valid_room_id() { + assert_eq!( + RoomId::try_from("!29fhd83h92h0:example.com") + .expect("Failed to create RoomId.") + .to_string(), + "!29fhd83h92h0:example.com" + ); + } + + #[test] + fn generate_random_valid_room_id() { + let room_id = RoomId::new("example.com") + .expect("Failed to generate RoomId.") + .to_string(); + + assert!(room_id.to_string().starts_with('!')); + assert_eq!(room_id.len(), 31); + } + + #[test] + fn generate_random_invalid_room_id() { + assert!(RoomId::new("").is_err()); + } + + #[test] + fn serialize_valid_room_id() { + assert_eq!( + to_string( + &RoomId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") + ) + .expect("Failed to convert RoomId to JSON."), + r#""!29fhd83h92h0:example.com""# + ); + } + + #[test] + fn deserialize_valid_room_id() { + assert_eq!( + from_str::(r#""!29fhd83h92h0:example.com""#) + .expect("Failed to convert JSON to RoomId"), + RoomId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") + ); + } + + #[test] + fn valid_room_id_with_explicit_standard_port() { + assert_eq!( + RoomId::try_from("!29fhd83h92h0:example.com:443") + .expect("Failed to create RoomId.") + .to_string(), + "!29fhd83h92h0:example.com" + ); + } + + #[test] + fn valid_room_id_with_non_standard_port() { + assert_eq!( + RoomId::try_from("!29fhd83h92h0:example.com:5000") + .expect("Failed to create RoomId.") + .to_string(), + "!29fhd83h92h0:example.com:5000" + ); + } + + #[test] + fn missing_room_id_sigil() { + assert_eq!( + RoomId::try_from("carl:example.com").err().unwrap(), + Error::MissingSigil + ); + } + + #[test] + fn missing_room_id_delimiter() { + assert_eq!( + RoomId::try_from("!29fhd83h92h0").err().unwrap(), + Error::MissingDelimiter + ); + } + + #[test] + fn invalid_room_id_host() { + assert_eq!( + RoomId::try_from("!29fhd83h92h0:-").err().unwrap(), + Error::InvalidHost + ); + } + + #[test] + fn invalid_room_id_port() { + assert_eq!( + RoomId::try_from("!29fhd83h92h0:example.com:notaport") + .err() + .unwrap(), + Error::InvalidHost + ); + } +} diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs new file mode 100644 index 00000000..8ddb85c0 --- /dev/null +++ b/src/room_id_or_room_alias_id.rs @@ -0,0 +1,219 @@ +//! Matrix identifiers for places where a room ID or room alias ID are used interchangeably. + +use std::{ + convert::TryFrom, + fmt::{Display, Formatter, Result as FmtResult}, +}; + +#[cfg(feature = "diesel")] +use diesel::sql_types::Text; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; + +use crate::{display, error::Error, room_alias_id::RoomAliasId, room_id::RoomId, validate_id}; + +/// A Matrix room ID or a Matrix room alias ID. +/// +/// `RoomIdOrAliasId` is useful for APIs that accept either kind of room identifier. It is converted +/// from a string slice, and can be converted back into a string as needed. When converted from a +/// string slice, the variant is determined by the leading sigil character. +/// +/// ``` +/// # use std::convert::TryFrom; +/// # use ruma_identifiers::RoomIdOrAliasId; +/// assert_eq!( +/// RoomIdOrAliasId::try_from("#ruma:example.com").unwrap().to_string(), +/// "#ruma:example.com" +/// ); +/// +/// assert_eq!( +/// RoomIdOrAliasId::try_from("!n8f893n9:example.com").unwrap().to_string(), +/// "!n8f893n9:example.com" +/// ); +/// ``` +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] +#[cfg_attr(feature = "diesel", sql_type = "Text")] +pub enum RoomIdOrAliasId { + /// A Matrix room alias ID. + RoomAliasId(RoomAliasId), + /// A Matrix room ID. + RoomId(RoomId), +} + +/// A serde visitor for `RoomIdOrAliasId`. +struct RoomIdOrAliasIdVisitor; + +impl Display for RoomIdOrAliasId { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match *self { + RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => display( + f, + '#', + room_alias_id.alias(), + room_alias_id.hostname(), + room_alias_id.port(), + ), + RoomIdOrAliasId::RoomId(ref room_id) => display( + f, + '!', + room_id.opaque_id(), + room_id.hostname(), + room_id.port(), + ), + } + } +} + +impl Serialize for RoomIdOrAliasId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => { + serializer.serialize_str(&room_alias_id.to_string()) + } + RoomIdOrAliasId::RoomId(ref room_id) => serializer.serialize_str(&room_id.to_string()), + } + } +} + +impl<'de> Deserialize<'de> for RoomIdOrAliasId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(RoomIdOrAliasIdVisitor) + } +} + +impl<'a> TryFrom<&'a str> for RoomIdOrAliasId { + type Error = Error; + + /// Attempts to create a new Matrix room ID or a room alias ID from a string representation. + /// + /// The string must either + /// include the leading ! sigil, the opaque ID, a literal colon, and a valid server name or + /// include the leading # sigil, the alias, a literal colon, and a valid server name. + fn try_from(room_id_or_alias_id: &'a str) -> Result { + validate_id(room_id_or_alias_id)?; + + let mut chars = room_id_or_alias_id.chars(); + + let sigil = chars.nth(0).expect("ID missing first character."); + + match sigil { + '#' => { + let room_alias_id = RoomAliasId::try_from(room_id_or_alias_id)?; + Ok(RoomIdOrAliasId::RoomAliasId(room_alias_id)) + } + '!' => { + let room_id = RoomId::try_from(room_id_or_alias_id)?; + Ok(RoomIdOrAliasId::RoomId(room_id)) + } + _ => Err(Error::MissingSigil), + } + } +} + +impl<'de> Visitor<'de> for RoomIdOrAliasIdVisitor { + type Value = RoomIdOrAliasId; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "a Matrix room ID or room alias ID as a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { + match RoomIdOrAliasId::try_from(v) { + Ok(room_id_or_alias_id) => Ok(room_id_or_alias_id), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), + } + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use serde_json::{from_str, to_string}; + + use super::RoomIdOrAliasId; + use crate::error::Error; + + #[test] + fn valid_room_id_or_alias_id_with_a_room_alias_id() { + assert_eq!( + RoomIdOrAliasId::try_from("#ruma:example.com") + .expect("Failed to create RoomAliasId.") + .to_string(), + "#ruma:example.com" + ); + } + + #[test] + fn valid_room_id_or_alias_id_with_a_room_id() { + assert_eq!( + RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") + .expect("Failed to create RoomId.") + .to_string(), + "!29fhd83h92h0:example.com" + ); + } + + #[test] + fn missing_sigil_for_room_id_or_alias_id() { + assert_eq!( + RoomIdOrAliasId::try_from("ruma:example.com").err().unwrap(), + Error::MissingSigil + ); + } + + #[test] + fn serialize_valid_room_id_or_alias_id_with_a_room_alias_id() { + assert_eq!( + to_string( + &RoomIdOrAliasId::try_from("#ruma:example.com") + .expect("Failed to create RoomAliasId.") + ) + .expect("Failed to convert RoomAliasId to JSON."), + r##""#ruma:example.com""## + ); + } + + #[test] + fn serialize_valid_room_id_or_alias_id_with_a_room_id() { + assert_eq!( + to_string( + &RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") + .expect("Failed to create RoomId.") + ) + .expect("Failed to convert RoomId to JSON."), + r#""!29fhd83h92h0:example.com""# + ); + } + + #[test] + fn deserialize_valid_room_id_or_alias_id_with_a_room_alias_id() { + assert_eq!( + from_str::(r##""#ruma:example.com""##) + .expect("Failed to convert JSON to RoomAliasId"), + RoomIdOrAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") + ); + } + + #[test] + fn deserialize_valid_room_id_or_alias_id_with_a_room_id() { + assert_eq!( + from_str::(r##""!29fhd83h92h0:example.com""##) + .expect("Failed to convert JSON to RoomId"), + RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") + .expect("Failed to create RoomAliasId.") + ); + } +} diff --git a/src/user_id.rs b/src/user_id.rs new file mode 100644 index 00000000..81b1ef15 --- /dev/null +++ b/src/user_id.rs @@ -0,0 +1,275 @@ +//! Matrix user identifiers. + +use std::{ + convert::TryFrom, + fmt::{Display, Formatter, Result as FmtResult}, +}; + +#[cfg(feature = "diesel")] +use diesel::sql_types::Text; +use lazy_static::lazy_static; +use regex::Regex; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use url::Host; + +use crate::{display, error::Error, generate_localpart, parse_id}; + +lazy_static! { + static ref USER_LOCALPART_PATTERN: Regex = + Regex::new(r"\A[a-z0-9._=-]+\z").expect("Failed to create user localpart regex."); +} + +/// A Matrix user ID. +/// +/// A `UserId` is generated randomly or converted from a string slice, and can be converted back +/// into a string as needed. +/// +/// ``` +/// # use std::convert::TryFrom; +/// # use ruma_identifiers::UserId; +/// assert_eq!( +/// UserId::try_from("@carl:example.com").unwrap().to_string(), +/// "@carl:example.com" +/// ); +/// ``` +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] +#[cfg_attr(feature = "diesel", sql_type = "Text")] +pub struct UserId { + /// The hostname of the homeserver. + hostname: Host, + /// The user's unique ID. + localpart: String, + /// The network port of the homeserver. + port: u16, +} + +/// A serde visitor for `UserId`. +struct UserIdVisitor; + +impl UserId { + /// Attempts to generate a `UserId` for the given origin server with a localpart consisting of + /// 12 random ASCII characters. + /// + /// Fails if the given origin server name cannot be parsed as a valid host. + pub fn new(server_name: &str) -> Result { + let user_id = format!("@{}:{}", generate_localpart(12).to_lowercase(), server_name); + let (localpart, host, port) = parse_id('@', &user_id)?; + + Ok(Self { + hostname: host, + localpart: localpart.to_string(), + port, + }) + } + + /// Returns a `Host` for the user ID, containing the server name (minus the port) of the + /// originating homeserver. + /// + /// The host can be either a domain name, an IPv4 address, or an IPv6 address. + pub fn hostname(&self) -> &Host { + &self.hostname + } + + /// Returns the user's localpart. + pub fn localpart(&self) -> &str { + &self.localpart + } + + /// Returns the port the originating homeserver can be accessed on. + pub fn port(&self) -> u16 { + self.port + } +} + +impl Display for UserId { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + display(f, '@', &self.localpart, &self.hostname, self.port) + } +} + +impl Serialize for UserId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for UserId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(UserIdVisitor) + } +} + +impl<'a> TryFrom<&'a str> for UserId { + type Error = Error; + + /// Attempts to create a new Matrix user ID from a string representation. + /// + /// The string must include the leading @ sigil, the localpart, a literal colon, and a valid + /// server name. + fn try_from(user_id: &'a str) -> Result { + let (localpart, host, port) = parse_id('@', user_id)?; + let downcased_localpart = localpart.to_lowercase(); + + if !USER_LOCALPART_PATTERN.is_match(&downcased_localpart) { + return Err(Error::InvalidCharacters); + } + + Ok(Self { + hostname: host, + port, + localpart: downcased_localpart.to_owned(), + }) + } +} + +impl<'de> Visitor<'de> for UserIdVisitor { + type Value = UserId; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "a Matrix user ID as a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { + match UserId::try_from(v) { + Ok(user_id) => Ok(user_id), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), + } + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use serde_json::{from_str, to_string}; + + use super::UserId; + use crate::error::Error; + + #[test] + fn valid_user_id() { + assert_eq!( + UserId::try_from("@carl:example.com") + .expect("Failed to create UserId.") + .to_string(), + "@carl:example.com" + ); + } + + #[test] + fn downcase_user_id() { + assert_eq!( + UserId::try_from("@CARL:example.com") + .expect("Failed to create UserId.") + .to_string(), + "@carl:example.com" + ); + } + + #[test] + fn generate_random_valid_user_id() { + let user_id = UserId::new("example.com") + .expect("Failed to generate UserId.") + .to_string(); + + assert!(user_id.to_string().starts_with('@')); + assert_eq!(user_id.len(), 25); + } + + #[test] + fn generate_random_invalid_user_id() { + assert!(UserId::new("").is_err()); + } + + #[test] + fn serialize_valid_user_id() { + assert_eq!( + to_string(&UserId::try_from("@carl:example.com").expect("Failed to create UserId.")) + .expect("Failed to convert UserId to JSON."), + r#""@carl:example.com""# + ); + } + + #[test] + fn deserialize_valid_user_id() { + assert_eq!( + from_str::(r#""@carl:example.com""#).expect("Failed to convert JSON to UserId"), + UserId::try_from("@carl:example.com").expect("Failed to create UserId.") + ); + } + + #[test] + fn valid_user_id_with_explicit_standard_port() { + assert_eq!( + UserId::try_from("@carl:example.com:443") + .expect("Failed to create UserId.") + .to_string(), + "@carl:example.com" + ); + } + + #[test] + fn valid_user_id_with_non_standard_port() { + assert_eq!( + UserId::try_from("@carl:example.com:5000") + .expect("Failed to create UserId.") + .to_string(), + "@carl:example.com:5000" + ); + } + + #[test] + fn invalid_characters_in_user_id_localpart() { + assert_eq!( + UserId::try_from("@%%%:example.com").err().unwrap(), + Error::InvalidCharacters + ); + } + + #[test] + fn missing_user_id_sigil() { + assert_eq!( + UserId::try_from("carl:example.com").err().unwrap(), + Error::MissingSigil + ); + } + + #[test] + fn missing_user_id_delimiter() { + assert_eq!( + UserId::try_from("@carl").err().unwrap(), + Error::MissingDelimiter + ); + } + + #[test] + fn invalid_user_id_host() { + assert_eq!( + UserId::try_from("@carl:-").err().unwrap(), + Error::InvalidHost + ); + } + + #[test] + fn invalid_user_id_port() { + assert_eq!( + UserId::try_from("@carl:example.com:notaport") + .err() + .unwrap(), + Error::InvalidHost + ); + } +}