//! Matrix URIs. use std::{convert::TryFrom, fmt}; use percent_encoding::{percent_decode_str, percent_encode, AsciiSet, CONTROLS}; use ruma_identifiers_validation::{ error::{MatrixIdError, MatrixToError}, Error, }; use url::Url; use crate::{EventId, RoomAliasId, RoomId, RoomOrAliasId, ServerName, UserId}; const MATRIX_TO_BASE_URL: &str = "https://matrix.to/#/"; // Controls + Space + reserved characters from RFC 3986. In practice only the // reserved characters will be encountered most likely, but better be safe. // https://datatracker.ietf.org/doc/html/rfc3986/#page-13 const TO_ENCODE: &AsciiSet = &CONTROLS .add(b':') .add(b'/') .add(b'?') .add(b'#') .add(b'[') .add(b']') .add(b'@') .add(b'!') .add(b'$') .add(b'&') .add(b'\'') .add(b'(') .add(b')') .add(b'*') .add(b'+') .add(b',') .add(b';') .add(b'='); /// All Matrix Identifiers that can be represented as a Matrix URI. #[derive(Debug, PartialEq, Eq)] #[non_exhaustive] pub enum MatrixId { /// A room ID. Room(Box), /// A room alias. RoomAlias(Box), /// A user ID. User(Box), /// An event ID. Event(Box, Box), } impl MatrixId { /// Try parsing a `&str` with sigils into a `MatrixId`. /// /// The identifiers are expected to start with a sigil and to be percent /// encoded. Slashes at the beginning and the end are stripped. /// /// For events, the room ID or alias and the event ID should be separated by /// a slash and they can be in any order. pub(crate) fn parse_with_sigil(s: &str) -> Result { let s = if let Some(stripped) = s.strip_prefix('/') { stripped } else { s }; let s = if let Some(stripped) = s.strip_suffix('/') { stripped } else { s }; if s.is_empty() { return Err(MatrixIdError::NoIdentifier.into()); } if s.matches('/').count() > 1 { return Err(MatrixIdError::TooManyIdentifiers.into()); } if let Some((first_raw, second_raw)) = s.split_once('/') { let first = percent_decode_str(first_raw).decode_utf8()?; let second = percent_decode_str(second_raw).decode_utf8()?; match first.as_bytes()[0] { b'!' | b'#' if second.as_bytes()[0] == b'$' => { let room_id = <&RoomOrAliasId>::try_from(first.as_ref())?; let event_id = <&EventId>::try_from(second.as_ref())?; Ok((room_id, event_id).into()) } b'$' if matches!(second.as_bytes()[0], b'!' | b'#') => { let room_id = <&RoomOrAliasId>::try_from(second.as_ref())?; let event_id = <&EventId>::try_from(first.as_ref())?; Ok((room_id, event_id).into()) } _ => Err(MatrixIdError::UnknownIdentifierPair.into()), } } else { let id = percent_decode_str(s).decode_utf8()?; match id.as_bytes()[0] { b'@' => Ok(<&UserId>::try_from(id.as_ref())?.into()), b'!' => Ok(<&RoomId>::try_from(id.as_ref())?.into()), b'#' => Ok(<&RoomAliasId>::try_from(id.as_ref())?.into()), b'$' => Err(MatrixIdError::MissingRoom.into()), _ => Err(MatrixIdError::UnknownIdentifier.into()), } } } /// Construct a string with sigils from `self`. /// /// The identifiers will start with a sigil and be percent encoded. /// /// For events, the room ID or alias and the event ID will be separated by /// a slash. pub(crate) fn to_string_with_sigil(&self) -> String { match self { Self::Room(room_id) => percent_encode(room_id.as_bytes(), TO_ENCODE).to_string(), Self::RoomAlias(room_alias) => { percent_encode(room_alias.as_bytes(), TO_ENCODE).to_string() } Self::User(user_id) => percent_encode(user_id.as_bytes(), TO_ENCODE).to_string(), Self::Event(room_id, event_id) => format!( "{}/{}", percent_encode(room_id.as_bytes(), TO_ENCODE), percent_encode(event_id.as_bytes(), TO_ENCODE), ), } } } impl From<&RoomId> for MatrixId { fn from(room_id: &RoomId) -> Self { Self::Room(room_id.into()) } } impl From<&RoomAliasId> for MatrixId { fn from(room_alias: &RoomAliasId) -> Self { Self::RoomAlias(room_alias.into()) } } impl From<&UserId> for MatrixId { fn from(user_id: &UserId) -> Self { Self::User(user_id.into()) } } impl From<(&RoomOrAliasId, &EventId)> for MatrixId { fn from(ids: (&RoomOrAliasId, &EventId)) -> Self { Self::Event(ids.0.into(), ids.1.into()) } } impl From<(&RoomId, &EventId)> for MatrixId { fn from(ids: (&RoomId, &EventId)) -> Self { Self::Event(<&RoomOrAliasId>::from(ids.0).into(), ids.1.into()) } } impl From<(&RoomAliasId, &EventId)> for MatrixId { fn from(ids: (&RoomAliasId, &EventId)) -> Self { Self::Event(<&RoomOrAliasId>::from(ids.0).into(), ids.1.into()) } } /// The `matrix.to` URI representation of a user, room or event. /// /// Get the URI through its `Display` implementation (i.e. by interpolating it /// in a formatting macro or via `.to_string()`). #[derive(Debug, PartialEq, Eq)] pub struct MatrixToUri { id: MatrixId, via: Vec>, } impl MatrixToUri { pub(crate) fn new(id: MatrixId, via: Vec<&ServerName>) -> Self { Self { id, via: via.into_iter().map(ToOwned::to_owned).collect() } } /// The identifier represented by this `matrix.to` URI. pub fn id(&self) -> &MatrixId { &self.id } /// Matrix servers usable to route a `RoomId`. pub fn via(&self) -> &[Box] { &self.via } /// Try parsing a `&str` into a `MatrixToUri`. pub fn parse(s: &str) -> Result { let without_base = if let Some(stripped) = s.strip_prefix(MATRIX_TO_BASE_URL) { stripped } else { return Err(MatrixToError::WrongBaseUrl.into()); }; let url = Url::parse(MATRIX_TO_BASE_URL.trim_end_matches("#/"))?.join(without_base)?; let id = MatrixId::parse_with_sigil(url.path())?; let mut via = vec![]; for (key, value) in url.query_pairs() { if key.as_ref() == "via" { via.push(ServerName::parse(value)?); } else { return Err(MatrixToError::UnknownArgument.into()); } } Ok(Self { id, via }) } } impl fmt::Display for MatrixToUri { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(MATRIX_TO_BASE_URL)?; write!(f, "{}", self.id().to_string_with_sigil())?; let mut first = true; for server_name in &self.via { f.write_str(if first { "?via=" } else { "&via=" })?; f.write_str(server_name.as_str())?; first = false; } Ok(()) } } #[cfg(test)] mod tests { use matches::assert_matches; use ruma_identifiers_validation::{ error::{MatrixIdError, MatrixToError}, Error, }; use super::{MatrixId, MatrixToUri}; use crate::{event_id, room_alias_id, room_id, server_name, user_id, RoomOrAliasId}; #[test] fn display_matrixtouri() { assert_eq!( user_id!("@jplatte:notareal.hs").matrix_to_uri().to_string(), "https://matrix.to/#/%40jplatte%3Anotareal.hs" ); assert_eq!( room_alias_id!("#ruma:notareal.hs").matrix_to_uri().to_string(), "https://matrix.to/#/%23ruma%3Anotareal.hs" ); assert_eq!( room_id!("!ruma:notareal.hs") .matrix_to_uri(vec![server_name!("notareal.hs")]) .to_string(), "https://matrix.to/#/%21ruma%3Anotareal.hs?via=notareal.hs" ); assert_eq!( room_alias_id!("#ruma:notareal.hs") .matrix_to_event_uri(event_id!("$event:notareal.hs")) .to_string(), "https://matrix.to/#/%23ruma%3Anotareal.hs/%24event%3Anotareal.hs" ); assert_eq!( room_id!("!ruma:notareal.hs") .matrix_to_event_uri(event_id!("$event:notareal.hs")) .to_string(), "https://matrix.to/#/%21ruma%3Anotareal.hs/%24event%3Anotareal.hs" ); } #[test] fn parse_valid_matrixid_with_sigil() { assert_eq!( MatrixId::parse_with_sigil("@user:imaginary.hs").expect("Failed to create MatrixId."), MatrixId::User(user_id!("@user:imaginary.hs").into()) ); assert_eq!( MatrixId::parse_with_sigil("!roomid:imaginary.hs").expect("Failed to create MatrixId."), MatrixId::Room(room_id!("!roomid:imaginary.hs").into()) ); assert_eq!( MatrixId::parse_with_sigil("#roomalias:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into()) ); assert_eq!( MatrixId::parse_with_sigil("!roomid:imaginary.hs/$event:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::Event( <&RoomOrAliasId>::from(room_id!("!roomid:imaginary.hs")).into(), event_id!("$event:imaginary.hs").into() ) ); assert_eq!( MatrixId::parse_with_sigil("#roomalias:imaginary.hs/$event:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::Event( <&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(), event_id!("$event:imaginary.hs").into() ) ); // Invert the order of the event and the room. assert_eq!( MatrixId::parse_with_sigil("$event:imaginary.hs/!roomid:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::Event( <&RoomOrAliasId>::from(room_id!("!roomid:imaginary.hs")).into(), event_id!("$event:imaginary.hs").into() ) ); assert_eq!( MatrixId::parse_with_sigil("$event:imaginary.hs/#roomalias:imaginary.hs") .expect("Failed to create MatrixId."), MatrixId::Event( <&RoomOrAliasId>::from(room_alias_id!("#roomalias:imaginary.hs")).into(), event_id!("$event:imaginary.hs").into() ) ); // Starting with a slash assert_eq!( MatrixId::parse_with_sigil("/@user:imaginary.hs").expect("Failed to create MatrixId."), MatrixId::User(user_id!("@user:imaginary.hs").into()) ); // Ending with a slash assert_eq!( MatrixId::parse_with_sigil("!roomid:imaginary.hs/") .expect("Failed to create MatrixId."), MatrixId::Room(room_id!("!roomid:imaginary.hs").into()) ); // Starting and ending with a slash assert_eq!( MatrixId::parse_with_sigil("/#roomalias:imaginary.hs/") .expect("Failed to create MatrixId."), MatrixId::RoomAlias(room_alias_id!("#roomalias:imaginary.hs").into()) ); } #[test] fn parse_matrixid_no_identifier() { assert_eq!(MatrixId::parse_with_sigil("").unwrap_err(), MatrixIdError::NoIdentifier.into()); assert_eq!( MatrixId::parse_with_sigil("/").unwrap_err(), MatrixIdError::NoIdentifier.into() ); } #[test] fn parse_matrixid_too_many_identifiers() { assert_eq!( MatrixId::parse_with_sigil( "@user:imaginary.hs/#room:imaginary.hs/$event1:imaginary.hs" ) .unwrap_err(), MatrixIdError::TooManyIdentifiers.into() ); } #[test] fn parse_matrixid_unknown_identifier_pair() { assert_eq!( MatrixId::parse_with_sigil("!roomid:imaginary.hs/@user:imaginary.hs").unwrap_err(), MatrixIdError::UnknownIdentifierPair.into() ); assert_eq!( MatrixId::parse_with_sigil("#roomalias:imaginary.hs/notanidentifier").unwrap_err(), MatrixIdError::UnknownIdentifierPair.into() ); assert_eq!( MatrixId::parse_with_sigil("$event:imaginary.hs/$otherevent:imaginary.hs").unwrap_err(), MatrixIdError::UnknownIdentifierPair.into() ); assert_eq!( MatrixId::parse_with_sigil("notanidentifier/neitheristhis").unwrap_err(), MatrixIdError::UnknownIdentifierPair.into() ); } #[test] fn parse_matrixid_missing_room() { assert_eq!( MatrixId::parse_with_sigil("$event:imaginary.hs").unwrap_err(), MatrixIdError::MissingRoom.into() ); } #[test] fn parse_matrixid_unknown_identifier() { assert_eq!( MatrixId::parse_with_sigil("event:imaginary.hs").unwrap_err(), MatrixIdError::UnknownIdentifier.into() ); assert_eq!( MatrixId::parse_with_sigil("notanidentifier").unwrap_err(), MatrixIdError::UnknownIdentifier.into() ); } #[test] fn parse_matrixtouri_valid_uris() { let matrix_to = MatrixToUri::parse("https://matrix.to/#/%40jplatte%3Anotareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!(matrix_to.id(), &user_id!("@jplatte:notareal.hs").into()); let matrix_to = MatrixToUri::parse("https://matrix.to/#/%23ruma%3Anotareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!(matrix_to.id(), &room_alias_id!("#ruma:notareal.hs").into()); let matrix_to = MatrixToUri::parse("https://matrix.to/#/%21ruma%3Anotareal.hs?via=notareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!(matrix_to.id(), &room_id!("!ruma:notareal.hs").into()); assert_eq!(matrix_to.via(), &vec![server_name!("notareal.hs").to_owned()]); let matrix_to = MatrixToUri::parse("https://matrix.to/#/%23ruma%3Anotareal.hs/%24event%3Anotareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!( matrix_to.id(), &(room_alias_id!("#ruma:notareal.hs"), event_id!("$event:notareal.hs")).into() ); let matrix_to = MatrixToUri::parse("https://matrix.to/#/%21ruma%3Anotareal.hs/%24event%3Anotareal.hs") .expect("Failed to create MatrixToUri."); assert_eq!( matrix_to.id(), &(room_id!("!ruma:notareal.hs"), event_id!("$event:notareal.hs")).into() ); assert!(matrix_to.via().is_empty()); } #[test] fn parse_matrixtouri_wrong_base_url() { assert_eq!(MatrixToUri::parse("").unwrap_err(), MatrixToError::WrongBaseUrl.into()); assert_eq!( MatrixToUri::parse("https://notreal.to/#/").unwrap_err(), MatrixToError::WrongBaseUrl.into() ); } #[test] fn parse_matrixtouri_wrong_identifier() { assert_matches!( MatrixToUri::parse("https://matrix.to/#/notanidentifier").unwrap_err(), Error::InvalidMatrixId(_) ); assert_matches!( MatrixToUri::parse("https://matrix.to/#/").unwrap_err(), Error::InvalidMatrixId(_) ); assert_matches!( MatrixToUri::parse( "https://matrix.to/#/%40jplatte%3Anotareal.hs/%24event%3Anotareal.hs" ) .unwrap_err(), Error::InvalidMatrixId(_) ); } #[test] fn parse_matrixtouri_unknown_arguments() { assert_eq!( MatrixToUri::parse( "https://matrix.to/#/%21ruma%3Anotareal.hs?via=notareal.hs&custom=data" ) .unwrap_err(), MatrixToError::UnknownArgument.into() ) } }