diff --git a/ruma-identifiers-macros/src/lib.rs b/ruma-identifiers-macros/src/lib.rs index fc472ca5..9b9faac4 100644 --- a/ruma-identifiers-macros/src/lib.rs +++ b/ruma-identifiers-macros/src/lib.rs @@ -2,7 +2,8 @@ use proc_macro::TokenStream; use quote::quote; use ruma_identifiers_validation::{ - device_key_id, event_id, key_id, room_alias_id, room_id, room_version_id, server_name, user_id, + device_key_id, event_id, key_id, mxc_uri, room_alias_id, room_id, room_version_id, server_name, + user_id, }; use syn::{parse::Parse, parse_macro_input, LitStr, Path, Token}; @@ -107,6 +108,20 @@ pub fn server_name(input: TokenStream) -> TokenStream { output.into() } +#[proc_macro] +pub fn mxc_uri(input: TokenStream) -> TokenStream { + let Input { dollar_crate, id } = parse_macro_input!(input as Input); + assert!(mxc_uri::validate(&id.value()).is_ok(), "Invalid mxc://"); + + let output = quote! { + <#dollar_crate::MxcUri as ::std::convert::TryFrom<&str>>::try_from( + #id, + ).unwrap() + }; + + output.into() +} + #[proc_macro] pub fn user_id(input: TokenStream) -> TokenStream { let Input { dollar_crate, id } = parse_macro_input!(input as Input); diff --git a/ruma-identifiers-validation/src/error.rs b/ruma-identifiers-validation/src/error.rs index d3331da4..771f509c 100644 --- a/ruma-identifiers-validation/src/error.rs +++ b/ruma-identifiers-validation/src/error.rs @@ -19,6 +19,9 @@ pub enum Error { /// The key version contains outside of [a-zA-Z0-9_]. InvalidKeyVersion, + /// The mxc:// isn't a valid Matrix Content URI. + InvalidMxcUri, + /// The server name part of the the ID string is not a valid server name. InvalidServerName, @@ -40,6 +43,7 @@ impl Display for Error { Error::InvalidCharacters => "localpart contains invalid characters", Error::InvalidKeyAlgorithm => "invalid key algorithm specified", Error::InvalidKeyVersion => "key ID version contains invalid characters", + Error::InvalidMxcUri => "the mxc:// isn't a valid Matrix Content URI", Error::InvalidServerName => "server name is not a valid IP address or domain name", Error::MaximumLengthExceeded => "ID exceeds 255 bytes", Error::MissingDelimiter => "required colon is missing", diff --git a/ruma-identifiers-validation/src/lib.rs b/ruma-identifiers-validation/src/lib.rs index 30e8748b..f6755179 100644 --- a/ruma-identifiers-validation/src/lib.rs +++ b/ruma-identifiers-validation/src/lib.rs @@ -2,6 +2,7 @@ pub mod device_key_id; pub mod error; pub mod event_id; pub mod key_id; +pub mod mxc_uri; pub mod room_alias_id; pub mod room_id; pub mod room_id_or_alias_id; diff --git a/ruma-identifiers-validation/src/mxc_uri.rs b/ruma-identifiers-validation/src/mxc_uri.rs new file mode 100644 index 00000000..14d25451 --- /dev/null +++ b/ruma-identifiers-validation/src/mxc_uri.rs @@ -0,0 +1,27 @@ +use crate::{server_name, Error}; + +const PROTOCOL: &str = "mxc://"; + +pub fn validate(uri: &str) -> Result<(&str, &str), Error> { + let uri = match uri.strip_prefix(PROTOCOL) { + Some(uri) => uri, + None => return Err(Error::InvalidMxcUri), + }; + + let index = match uri.find('/') { + Some(index) => index, + None => return Err(Error::InvalidMxcUri), + }; + + let server_name = &uri[..index]; + let media_id = &uri[index + 1..]; + // See: https://matrix.org/docs/spec/client_server/r0.6.1#id69 + let media_id_is_valid = + media_id.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'-' )); + + if media_id_is_valid && server_name::validate(server_name).is_ok() { + Ok((media_id, server_name)) + } else { + Err(Error::InvalidMxcUri) + } +} diff --git a/ruma-identifiers/src/lib.rs b/ruma-identifiers/src/lib.rs index 7a2eb1c6..b9279be4 100644 --- a/ruma-identifiers/src/lib.rs +++ b/ruma-identifiers/src/lib.rs @@ -20,6 +20,7 @@ pub use crate::{ device_key_id::DeviceKeyId, event_id::EventId, key_id::{DeviceSigningKeyId, KeyId, ServerSigningKeyId, SigningKeyId}, + mxc_uri::MxcUri, opaque_ids::{DeviceId, DeviceIdBox, KeyName, KeyNameBox}, room_alias_id::RoomAliasId, room_id::RoomId, @@ -41,6 +42,7 @@ mod crypto_algorithms; mod device_key_id; mod event_id; mod key_id; +mod mxc_uri; mod opaque_ids; mod room_alias_id; mod room_id; @@ -159,6 +161,14 @@ macro_rules! server_name { }; } +/// Compile-time checked `MxcUri` construction. +#[macro_export] +macro_rules! mxc_uri { + ($s:literal) => { + $crate::_macros::mxc_uri!($crate, $s) + }; +} + /// Compile-time checked `UserId` construction. #[macro_export] macro_rules! user_id { diff --git a/ruma-identifiers/src/mxc_uri.rs b/ruma-identifiers/src/mxc_uri.rs new file mode 100644 index 00000000..485c4543 --- /dev/null +++ b/ruma-identifiers/src/mxc_uri.rs @@ -0,0 +1,123 @@ +//! Matrix-spec compliant mxc:// urls. + +use std::{ + convert::TryFrom, + fmt::{self, Display}, + str::FromStr, +}; + +use ruma_identifiers_validation::mxc_uri::validate; + +use crate::{Error, ServerName, ServerNameBox}; + +/// Matrix-spec compliant mxc:// urls. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct MxcUri { + server_name: ServerNameBox, + media_id: Box, +} + +impl MxcUri { + /// Returns the media ID of this mxc://. + pub fn media_id(&self) -> &str { + &self.media_id + } + + /// Returns the server name of this mxc://. + pub fn server_name(&self) -> &ServerName { + &self.server_name + } + + fn mxc_uri_fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("mxc://{}/{}", self.server_name, self.media_id)) + } +} + +impl fmt::Debug for MxcUri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.mxc_uri_fmt(f) + } +} + +impl Display for MxcUri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.mxc_uri_fmt(f) + } +} + +fn try_from(uri: S) -> Result +where + S: AsRef + Into>, +{ + let (media_id, server_name) = validate(uri.as_ref())?; + Ok(MxcUri { media_id: media_id.into(), server_name: ::try_from(server_name)? }) +} + +impl FromStr for MxcUri { + type Err = crate::Error; + + fn from_str(uri: &str) -> Result { + try_from(uri) + } +} + +impl TryFrom<&str> for MxcUri { + type Error = crate::Error; + + fn try_from(s: &str) -> Result { + try_from(s) + } +} + +impl TryFrom for MxcUri { + type Error = crate::Error; + + fn try_from(s: String) -> Result { + try_from(s) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for MxcUri { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + crate::deserialize_id( + deserializer, + "Content location represented as a Matrix Content (MXC) URI", + ) + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for MxcUri { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use super::MxcUri; + + #[test] + fn parse_mxc_uri() { + assert!(::try_from("mxc://127.0.0.1/asd32asdfasdsd").is_ok()); + } + + #[test] + fn parse_mxc_uri_without_media_id() { + assert!(!::try_from("mxc://127.0.0.1").is_ok()); + } + + #[test] + fn parse_mxc_uri_without_protocol() { + assert!(!::try_from("127.0.0.1/asd32asdfasdsd").is_ok()); + } +} diff --git a/ruma-identifiers/tests/ui/01-valid-id-macros.rs b/ruma-identifiers/tests/ui/01-valid-id-macros.rs index eb8b2e11..14897cb7 100644 --- a/ruma-identifiers/tests/ui/01-valid-id-macros.rs +++ b/ruma-identifiers/tests/ui/01-valid-id-macros.rs @@ -2,6 +2,7 @@ fn main() { let _ = ruma_identifiers::device_key_id!("ed25519:JLAFKJWSCS"); let _ = ruma_identifiers::event_id!("$39hvsi03hlne:example.com"); let _ = ruma_identifiers::event_id!("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"); + let _ = ruma_identifiers::mxc_uri!("mxc://myserver.fish/sdfdsfsdfsdfgsdfsd"); let _ = ruma_identifiers::room_alias_id!("#alias:server.tld"); let _ = ruma_identifiers::room_id!("!1234567890:matrix.org"); let _ = ruma_identifiers::room_version_id!("1"); diff --git a/ruma-identifiers/tests/ui/02-invalid-id-macros.rs b/ruma-identifiers/tests/ui/02-invalid-id-macros.rs index c82b7009..0bfedee8 100644 --- a/ruma-identifiers/tests/ui/02-invalid-id-macros.rs +++ b/ruma-identifiers/tests/ui/02-invalid-id-macros.rs @@ -1,6 +1,7 @@ fn main() { let _ = ruma_identifiers::event_id!("39hvsi03hlne:example.com"); let _ = ruma_identifiers::event_id!("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"); + let _ = ruma_identifiers::mxc_uri!(""); let _ = ruma_identifiers::room_alias_id!("alias:server.tld"); let _ = ruma_identifiers::room_id!("1234567890:matrix.org"); let _ = ruma_identifiers::room_version_id!(""); diff --git a/ruma-identifiers/tests/ui/02-invalid-id-macros.stderr b/ruma-identifiers/tests/ui/02-invalid-id-macros.stderr index c62c28bd..03cbdc99 100644 --- a/ruma-identifiers/tests/ui/02-invalid-id-macros.stderr +++ b/ruma-identifiers/tests/ui/02-invalid-id-macros.stderr @@ -19,43 +19,52 @@ error: proc macro panicked error: proc macro panicked --> $DIR/02-invalid-id-macros.rs:4:13 | -4 | let _ = ruma_identifiers::room_alias_id!("alias:server.tld"); +4 | let _ = ruma_identifiers::mxc_uri!(""); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: message: Invalid mxc:// + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: proc macro panicked + --> $DIR/02-invalid-id-macros.rs:5:13 + | +5 | let _ = ruma_identifiers::room_alias_id!("alias:server.tld"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: message: Invalid room_alias_id = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) error: proc macro panicked - --> $DIR/02-invalid-id-macros.rs:5:13 + --> $DIR/02-invalid-id-macros.rs:6:13 | -5 | let _ = ruma_identifiers::room_id!("1234567890:matrix.org"); +6 | let _ = ruma_identifiers::room_id!("1234567890:matrix.org"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: message: Invalid room_id = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) error: proc macro panicked - --> $DIR/02-invalid-id-macros.rs:6:13 + --> $DIR/02-invalid-id-macros.rs:7:13 | -6 | let _ = ruma_identifiers::room_version_id!(""); +7 | let _ = ruma_identifiers::room_version_id!(""); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: message: Invalid room_version_id = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) error: proc macro panicked - --> $DIR/02-invalid-id-macros.rs:7:13 + --> $DIR/02-invalid-id-macros.rs:8:13 | -7 | let _ = ruma_identifiers::server_name!(""); +8 | let _ = ruma_identifiers::server_name!(""); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: message: Invalid server_name = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) error: proc macro panicked - --> $DIR/02-invalid-id-macros.rs:8:13 + --> $DIR/02-invalid-id-macros.rs:9:13 | -8 | let _ = ruma_identifiers::user_id!("user:ruma.io"); +9 | let _ = ruma_identifiers::user_id!("user:ruma.io"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: message: Invalid user_id