//! A URI that should be a Matrix-spec compliant [MXC URI]. //! //! [MXC URI]: https://spec.matrix.org/latest/client-server-api/#matrix-content-mxc-uris use std::{fmt, num::NonZeroU8}; use ruma_identifiers_validation::{error::MxcUriError, mxc_uri::validate}; use ruma_macros::IdZst; use serde::{Serialize, Serializer}; use super::ServerName; type Result = std::result::Result; /// A URI that should be a Matrix-spec compliant [MXC URI]. /// /// [MXC URI]: https://spec.matrix.org/latest/client-server-api/#matrix-content-mxc-uris #[repr(transparent)] #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] pub struct MxcUri(str); /// Structured MXC URI which may reference strings from separate sources without serialization #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[allow(clippy::exhaustive_structs)] pub struct Mxc<'a> { /// ServerName part of the MXC URI pub server_name: &'a ServerName, /// MediaId part of the MXC URI pub media_id: &'a str, } impl MxcUri { /// If this is a valid MXC URI, returns the media ID. pub fn media_id(&self) -> Result<&str> { self.parts().map(|mxc| mxc.media_id) } /// If this is a valid MXC URI, returns the server name. pub fn server_name(&self) -> Result<&ServerName> { self.parts().map(|mxc| mxc.server_name) } /// If this is a valid MXC URI, returns a `(server_name, media_id)` tuple, else it returns the /// error. pub fn parts(&self) -> Result> { self.extract_slash_idx().map(|idx| Mxc::<'_> { server_name: ServerName::from_borrowed(&self.as_str()[6..idx.get() as usize]), media_id: &self.as_str()[idx.get() as usize + 1..], }) } /// Validates the URI and returns an error if it failed. pub fn validate(&self) -> Result<()> { self.extract_slash_idx().map(|_| ()) } /// Convenience method for `.validate().is_ok()`. #[inline(always)] pub fn is_valid(&self) -> bool { self.validate().is_ok() } // convenience method for calling validate(self) #[inline(always)] fn extract_slash_idx(&self) -> Result { validate(self.as_str()) } } impl fmt::Display for Mxc<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "mxc://{}/{}", self.server_name, self.media_id) } } impl<'a> TryFrom<&'a MxcUri> for Mxc<'a> { type Error = MxcUriError; fn try_from(s: &'a MxcUri) -> Result { s.parts() } } impl<'a> TryFrom<&'a str> for Mxc<'a> { type Error = MxcUriError; fn try_from(s: &'a str) -> Result { let s: &MxcUri = s.into(); s.try_into() } } impl<'a> TryFrom<&'a OwnedMxcUri> for Mxc<'a> { type Error = MxcUriError; fn try_from(s: &'a OwnedMxcUri) -> Result { let s: &MxcUri = s.as_ref(); s.try_into() } } impl Serialize for Mxc<'_> { fn serialize(&self, s: S) -> Result { s.serialize_str(self.to_string().as_str()) } } #[cfg(test)] mod tests { use ruma_identifiers_validation::error::MxcUriError; use super::{Mxc, MxcUri, OwnedMxcUri}; #[test] fn parse_mxc_uri() { let mxc = Box::::from("mxc://127.0.0.1/asd32asdfasdsd"); assert!(mxc.is_valid()); assert_eq!( mxc.parts(), Ok(Mxc { server_name: "127.0.0.1".try_into().expect("Failed to create ServerName"), media_id: "asd32asdfasdsd", }) ); } #[test] fn parse_mxc_uri_without_media_id() { let mxc = Box::::from("mxc://127.0.0.1"); assert!(!mxc.is_valid()); assert_eq!(mxc.parts(), Err(MxcUriError::MissingSlash)); } #[test] fn parse_mxc_uri_without_protocol() { assert!(!Box::::from("127.0.0.1/asd32asdfasdsd").is_valid()); } #[test] fn serialize_mxc_uri() { assert_eq!( serde_json::to_string(&Box::::from("mxc://server/1234id")) .expect("Failed to convert MxcUri to JSON."), r#""mxc://server/1234id""# ); } #[test] fn deserialize_mxc_uri() { let mxc = serde_json::from_str::(r#""mxc://server/1234id""#) .expect("Failed to convert JSON to MxcUri"); assert_eq!(mxc.as_str(), "mxc://server/1234id"); assert!(mxc.is_valid()); assert_eq!( mxc.parts(), Ok(Mxc { server_name: "server".try_into().expect("Failed to create ServerName"), media_id: "1234id" }) ); } }