From 199b3ae7ae12f909b18fca188a121068b340f718 Mon Sep 17 00:00:00 2001 From: xmppftw Date: Wed, 21 Jun 2023 13:52:31 +0200 Subject: [PATCH] Introduce typed Parts for the JID to enable unfallible JID construction --- jid/src/lib.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++++++ jid/src/parts.rs | 52 +++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 jid/src/parts.rs diff --git a/jid/src/lib.rs b/jid/src/lib.rs index cb555b4b..8159b762 100644 --- a/jid/src/lib.rs +++ b/jid/src/lib.rs @@ -45,6 +45,9 @@ pub use crate::error::Error; mod inner; use inner::InnerJid; +mod parts; +pub use parts::{DomainPart, NodePart, ResourcePart}; + /// An enum representing a Jabber ID. It can be either a `FullJid` or a `BareJid`. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(untagged))] @@ -89,6 +92,7 @@ impl fmt::Display for Jid { impl Jid { /// Constructs a Jabber ID from a string. This is of the form /// `node`@`domain`/`resource`, where node and resource parts are optional. + /// If you want a non-fallible version, use [`Jid::from_parts`] instead. /// /// # Examples /// @@ -114,6 +118,21 @@ impl Jid { } } + /// Build a [`Jid`] from typed parts. This method cannot fail because it uses parts that have + /// already been parsed and stringprepped into [`NodePart`], [`DomainPart`], and [`ResourcePart`]. + /// This method allocates and does not consume the typed parts. + pub fn from_parts( + node: Option<&NodePart>, + domain: &DomainPart, + resource: Option<&ResourcePart>, + ) -> Jid { + if let Some(resource) = resource { + Jid::Full(FullJid::from_parts(node, domain, resource)) + } else { + Jid::Bare(BareJid::from_parts(node, domain)) + } + } + /// The optional node part of the JID. pub fn node(&self) -> Option<&str> { match self { @@ -305,6 +324,7 @@ impl<'de> Deserialize<'de> for BareJid { impl FullJid { /// Constructs a full Jabber ID containing all three components. This is of the form /// `node@domain/resource`, where node part is optional. + /// If you want a non-fallible version, use [`FullJid::from_parts`] instead. /// /// # Examples /// @@ -330,6 +350,38 @@ impl FullJid { } } + /// Build a [`FullJid`] from typed parts. This method cannot fail because it uses parts that have + /// already been parsed and stringprepped into [`NodePart`], [`DomainPart`], and [`ResourcePart`]. + /// This method allocates and does not consume the typed parts. + pub fn from_parts( + node: Option<&NodePart>, + domain: &DomainPart, + resource: &ResourcePart, + ) -> FullJid { + let (at, slash, normalized) = if let Some(node) = node { + // Parts are never empty so len > 0 for NonZeroU16::new is always Some + ( + NonZeroU16::new(node.0.len() as u16), + NonZeroU16::new((node.0.len() + 1 + domain.0.len()) as u16), + format!("{}@{}/{}", node.0, domain.0, resource.0), + ) + } else { + ( + None, + NonZeroU16::new(domain.0.len() as u16), + format!("{}/{}", domain.0, resource.0), + ) + }; + + let inner = InnerJid { + normalized, + at, + slash, + }; + + FullJid { inner } + } + /// The optional node part of the JID. pub fn node(&self) -> Option<&str> { self.inner.node() @@ -378,6 +430,7 @@ impl FromStr for BareJid { impl BareJid { /// Constructs a bare Jabber ID, containing two components. This is of the form /// `node`@`domain`, where node part is optional. + /// If you want a non-fallible version, use [`BareJid::from_parts`] instead. /// /// # Examples /// @@ -402,6 +455,29 @@ impl BareJid { } } + /// Build a [`BareJid`] from typed parts. This method cannot fail because it uses parts that have + /// already been parsed and stringprepped into [`NodePart`] and [`DomainPart`]. This method allocates + /// and does not consume the typed parts. + pub fn from_parts(node: Option<&NodePart>, domain: &DomainPart) -> BareJid { + let (at, normalized) = if let Some(node) = node { + // Parts are never empty so len > 0 for NonZeroU16::new is always Some + ( + NonZeroU16::new(node.0.len() as u16), + format!("{}@{}", node.0, domain.0), + ) + } else { + (None, domain.0.clone()) + }; + + let inner = InnerJid { + normalized, + at, + slash: None, + }; + + BareJid { inner } + } + /// The optional node part of the JID. pub fn node(&self) -> Option<&str> { self.inner.node() diff --git a/jid/src/parts.rs b/jid/src/parts.rs new file mode 100644 index 00000000..62b95566 --- /dev/null +++ b/jid/src/parts.rs @@ -0,0 +1,52 @@ +use stringprep::{nameprep, nodeprep, resourceprep}; + +use crate::Error; + +/// The [`NodePart`] is the optional part before the (optional) `@` in any [`Jid`], whether [`BareJid`] or [`FullJid`]. +#[derive(Clone, Debug, PartialEq, Hash, PartialOrd)] +pub struct NodePart(pub(crate) String); + +fn length_check(len: usize, error_empty: Error, error_too_long: Error) -> Result<(), Error> { + if len == 0 { + Err(error_empty) + } else if len > 1023 { + Err(error_too_long) + } else { + Ok(()) + } +} + +impl NodePart { + /// Build a new [`NodePart`] from a string slice. Will fail in case of stringprep validation error. + pub fn new(s: &str) -> Result { + let node = nodeprep(s).map_err(|_| Error::NodePrep)?; + length_check(node.len(), Error::NodeEmpty, Error::NodeTooLong)?; + Ok(NodePart(node.to_string())) + } +} + +/// The [`DomainPart`] is the part between the (optional) `@` and the (optional) `/` in any [`Jid`], whether [`BareJid`] or [`FullJid`]. +#[derive(Clone, Debug, PartialEq, Hash, PartialOrd)] +pub struct DomainPart(pub(crate) String); + +impl DomainPart { + /// Build a new [`DomainPart`] from a string slice. Will fail in case of stringprep validation error. + pub fn new(s: &str) -> Result { + let domain = nameprep(s).map_err(|_| Error::NamePrep)?; + length_check(domain.len(), Error::DomainEmpty, Error::DomainTooLong)?; + Ok(DomainPart(domain.to_string())) + } +} + +/// The [`ResourcePart`] is the optional part after the `/` in a [`Jid`]. It is mandatory in [`FullJid`]. +#[derive(Clone, Debug, PartialEq, Hash, PartialOrd)] +pub struct ResourcePart(pub(crate) String); + +impl ResourcePart { + /// Build a new [`ResourcePart`] from a string slice. Will fail in case of stringprep validation error. + pub fn new(s: &str) -> Result { + let resource = resourceprep(s).map_err(|_| Error::ResourcePrep)?; + length_check(resource.len(), Error::ResourceEmpty, Error::ResourceTooLong)?; + Ok(ResourcePart(resource.to_string())) + } +}