identifiers: Add MatrixToUri parsing
This commit is contained in:
parent
1c23cd25b5
commit
f9b4958654
@ -15,3 +15,4 @@ compat = []
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1.0.26"
|
||||
url = "2.2.2"
|
||||
|
@ -1,5 +1,7 @@
|
||||
//! Error conditions.
|
||||
|
||||
use std::str::Utf8Error;
|
||||
|
||||
/// An error encountered when trying to parse an invalid ID string.
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
@ -30,6 +32,14 @@ pub enum Error {
|
||||
#[error("key ID version contains invalid characters")]
|
||||
InvalidKeyVersion,
|
||||
|
||||
/// The string isn't a valid Matrix ID.
|
||||
#[error("invalid matrix ID: {0}")]
|
||||
InvalidMatrixId(#[from] MatrixIdError),
|
||||
|
||||
/// The string isn't a valid Matrix.to URI.
|
||||
#[error("invalid matrix.to URI: {0}")]
|
||||
InvalidMatrixToRef(#[from] MatrixToError),
|
||||
|
||||
/// The mxc:// isn't a valid Matrix Content URI.
|
||||
#[error("invalid Matrix Content URI: {0}")]
|
||||
InvalidMxcUri(#[from] MxcUriError),
|
||||
@ -38,6 +48,14 @@ pub enum Error {
|
||||
#[error("server name is not a valid IP address or domain name")]
|
||||
InvalidServerName,
|
||||
|
||||
/// The string isn't a valid URI.
|
||||
#[error("invalid URI")]
|
||||
InvalidUri,
|
||||
|
||||
/// The string isn't valid UTF-8.
|
||||
#[error("invalid UTF-8")]
|
||||
InvalidUtf8,
|
||||
|
||||
/// The ID exceeds 255 bytes (or 32 codepoints for a room version ID).
|
||||
#[error("ID exceeds 255 bytes")]
|
||||
MaximumLengthExceeded,
|
||||
@ -52,6 +70,18 @@ pub enum Error {
|
||||
MissingLeadingSigil,
|
||||
}
|
||||
|
||||
impl From<Utf8Error> for Error {
|
||||
fn from(_: Utf8Error) -> Self {
|
||||
Self::InvalidUtf8
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for Error {
|
||||
fn from(_: url::ParseError) -> Self {
|
||||
Self::InvalidUri
|
||||
}
|
||||
}
|
||||
|
||||
/// An error occurred while validating an MXC URI.
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
@ -75,3 +105,41 @@ pub enum MxcUriError {
|
||||
#[error("invalid Server Name")]
|
||||
ServerNameMalformed,
|
||||
}
|
||||
|
||||
/// An error occurred while validating a `MatrixId`.
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum MatrixIdError {
|
||||
/// The string is missing a room ID or alias.
|
||||
#[error("missing room ID or alias")]
|
||||
MissingRoom,
|
||||
|
||||
/// The string contains no identifier.
|
||||
#[error("no identifier")]
|
||||
NoIdentifier,
|
||||
|
||||
/// The string contains too many identifiers.
|
||||
#[error("too many identifiers")]
|
||||
TooManyIdentifiers,
|
||||
|
||||
/// The string contains an unknown identifier.
|
||||
#[error("unknown identifier")]
|
||||
UnknownIdentifier,
|
||||
|
||||
/// The string contains two identifiers that cannot be paired.
|
||||
#[error("unknown identifier pair")]
|
||||
UnknownIdentifierPair,
|
||||
}
|
||||
|
||||
/// An error occurred while validating an MXC URI.
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum MatrixToError {
|
||||
/// String did not start with `https://matrix.to/#/`.
|
||||
#[error("base URL is not https://matrix.to/#/")]
|
||||
WrongBaseUrl,
|
||||
|
||||
/// String has an unknown additional argument.
|
||||
#[error("unknown additional argument")]
|
||||
UnknownArgument,
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ Breaking changes:
|
||||
Improvements:
|
||||
|
||||
* Add `host`, `port` and `is_ip_literal` methods to `ServerName`
|
||||
* Add `matrix_uri::MatrixToUri` to build `matrix.to` URIs
|
||||
|
||||
Bug fixes:
|
||||
|
||||
|
@ -30,6 +30,7 @@ ruma-serde = { version = "0.5.0", path = "../ruma-serde", optional = true }
|
||||
ruma-serde-macros = { version = "0.5.0", path = "../ruma-serde-macros" }
|
||||
# Renamed so we can have a serde feature.
|
||||
serde1 = { package = "serde", version = "1.0.126", optional = true, features = ["derive"] }
|
||||
url = "2.2.2"
|
||||
uuid = { version = "0.8.2", optional = true, features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -1,8 +1,13 @@
|
||||
//! Matrix URIs.
|
||||
|
||||
use std::fmt;
|
||||
use std::{convert::TryFrom, fmt};
|
||||
|
||||
use percent_encoding::{percent_encode, AsciiSet, CONTROLS};
|
||||
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};
|
||||
|
||||
@ -48,6 +53,54 @@ pub enum MatrixId {
|
||||
}
|
||||
|
||||
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<Self, Error> {
|
||||
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.
|
||||
@ -130,6 +183,30 @@ impl MatrixToUri {
|
||||
pub fn via(&self) -> &[Box<ServerName>] {
|
||||
&self.via
|
||||
}
|
||||
|
||||
/// Try parsing a `&str` into a `MatrixToUri`.
|
||||
pub fn parse(s: &str) -> Result<Self, Error> {
|
||||
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 {
|
||||
@ -151,7 +228,14 @@ impl fmt::Display for MatrixToUri {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{event_id, room_alias_id, room_id, server_name, user_id};
|
||||
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() {
|
||||
@ -182,4 +266,204 @@ mod tests {
|
||||
"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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user