xmpp-rs/jid/src/lib.rs
Maxime “pep” Buquet 4c2fac0ac3 jid: clippy run
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2025-04-08 12:47:56 +02:00

1465 lines
44 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) 2017, 2018 lumi <lumi@pew.im>
// Copyright (c) 2017, 2018, 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
// Copyright (c) 2017, 2018, 2019 Maxime “pep” Buquet <pep@bouah.net>
// Copyright (c) 2017, 2018 Astro <astro@spaceboyz.net>
// Copyright (c) 2017 Bastien Orivel <eijebong@bananium.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#![no_std]
#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
//! Represents XMPP addresses, also known as JabberIDs (JIDs) for the [XMPP](https://xmpp.org/)
//! protocol. A [`Jid`] can have between one and three parts in the form `node@domain/resource`:
//! - the (optional) node part designates a specific account/service on a server, for example
//! `username@server.com`
//! - the domain part designates a server, for example `irc.jabberfr.org`
//! - the (optional) resource part designates a more specific client, such as a participant in a
//! groupchat (`jabberfr@chat.jabberfr.org/user`) or a specific client device associated with an
//! account (`user@example.com/dino`)
//!
//! The [`Jid`] enum can be one of two variants, containing a more specific type:
//! - [`BareJid`] (`Jid::Bare` variant): a JID without a resource
//! - [`FullJid`] (`Jid::Full` variant): a JID with a resource
//!
//! Jids as per the XMPP protocol only ever contain valid UTF-8. However, creating any form of Jid
//! can fail in one of the following cases:
//! - wrong syntax: creating a Jid with an empty (yet declared) node or resource part, such as
//! `@example.com` or `user@example.com/`
//! - stringprep error: some characters were invalid according to the stringprep algorithm, such as
//! mixing left-to-write and right-to-left characters
extern crate alloc;
#[cfg(test)]
extern crate std;
use alloc::borrow::Cow;
use alloc::format;
use alloc::string::{String, ToString};
use core::borrow::Borrow;
use core::cmp::Ordering;
use core::fmt;
use core::hash::{Hash, Hasher};
use core::mem;
use core::net::{Ipv4Addr, Ipv6Addr};
use core::num::NonZeroU16;
use core::ops::Deref;
use core::str::FromStr;
use memchr::memchr2_iter;
use idna::uts46::{AsciiDenyList, DnsLength, Hyphens, Uts46};
use stringprep::{nameprep, nodeprep, resourceprep};
#[cfg(feature = "serde")]
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
#[cfg(feature = "quote")]
use proc_macro2::TokenStream;
#[cfg(feature = "quote")]
use quote::{quote, ToTokens};
#[cfg(feature = "minidom")]
use minidom::{IntoAttributeValue, Node};
mod error;
mod parts;
pub use crate::error::Error;
pub use parts::{DomainPart, DomainRef, NodePart, NodeRef, ResourcePart, ResourceRef};
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(())
}
}
fn node_check(node: &str) -> Result<Cow<'_, str>, Error> {
let node = nodeprep(node).map_err(|_| Error::NodePrep)?;
length_check(node.len(), Error::NodeEmpty, Error::NodeTooLong)?;
Ok(node)
}
fn resource_check(resource: &str) -> Result<Cow<'_, str>, Error> {
let resource = resourceprep(resource).map_err(|_| Error::ResourcePrep)?;
length_check(resource.len(), Error::ResourceEmpty, Error::ResourceTooLong)?;
Ok(resource)
}
fn domain_check(mut domain: &str) -> Result<Cow<'_, str>, Error> {
// First, check if this is an IPv4 address.
if Ipv4Addr::from_str(domain).is_ok() {
return Ok(Cow::Borrowed(domain));
}
// Then if this is an IPv6 address.
if domain.starts_with('[')
&& domain.ends_with(']')
&& Ipv6Addr::from_str(&domain[1..domain.len() - 1]).is_ok()
{
return Ok(Cow::Borrowed(domain));
}
// idna can handle the root dot for us, but we still want to remove it for normalization
// purposes.
if domain.ends_with('.') {
domain = &domain[..domain.len() - 1];
}
Uts46::new()
.to_ascii(
domain.as_bytes(),
AsciiDenyList::URL,
Hyphens::Check,
DnsLength::Verify,
)
.map_err(|_| Error::Idna)?;
let domain = nameprep(domain).map_err(|_| Error::NamePrep)?;
Ok(domain)
}
/// A struct representing a Jabber ID (JID).
///
/// This JID can either be "bare" (without a `/resource` suffix) or full (with
/// a resource suffix).
///
/// In many APIs, it is appropriate to use the more specific types
/// ([`BareJid`] or [`FullJid`]) instead, as these two JID types are generally
/// used in different contexts within XMPP.
///
/// This dynamic type on the other hand can be used in contexts where it is
/// not known, at compile-time, whether a JID is full or bare.
#[derive(Clone, Eq)]
pub struct Jid {
normalized: String,
at: Option<NonZeroU16>,
slash: Option<NonZeroU16>,
}
impl PartialEq for Jid {
fn eq(&self, other: &Jid) -> bool {
self.normalized == other.normalized
}
}
impl PartialOrd for Jid {
fn partial_cmp(&self, other: &Jid) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Jid {
fn cmp(&self, other: &Jid) -> Ordering {
self.normalized.cmp(&other.normalized)
}
}
impl Hash for Jid {
fn hash<H: Hasher>(&self, state: &mut H) {
self.normalized.hash(state)
}
}
impl FromStr for Jid {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl From<BareJid> for Jid {
fn from(other: BareJid) -> Self {
other.inner
}
}
impl From<FullJid> for Jid {
fn from(other: FullJid) -> Self {
other.inner
}
}
impl fmt::Display for Jid {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str(&self.normalized)
}
}
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
///
/// ```
/// use jid::Jid;
/// # use jid::Error;
///
/// # fn main() -> Result<(), Error> {
/// let jid = Jid::new("node@domain/resource")?;
///
/// assert_eq!(jid.node().map(|x| x.as_str()), Some("node"));
/// assert_eq!(jid.domain().as_str(), "domain");
/// assert_eq!(jid.resource().map(|x| x.as_str()), Some("resource"));
/// # Ok(())
/// # }
/// ```
pub fn new(unnormalized: &str) -> Result<Jid, Error> {
let bytes = unnormalized.as_bytes();
let orig_at;
let orig_slash;
let mut iter = memchr2_iter(b'@', b'/', bytes);
let normalized = if let Some(first_index) = iter.next() {
let byte = bytes[first_index];
if byte == b'@' {
if let Some(second_index) = iter.next() {
let byte = bytes[second_index];
if byte == b'/' {
let node = node_check(&unnormalized[..first_index])?;
let domain = domain_check(&unnormalized[first_index + 1..second_index])?;
let resource = resource_check(&unnormalized[second_index + 1..])?;
orig_at = Some(node.len());
orig_slash = Some(node.len() + domain.len() + 1);
match (node, domain, resource) {
(Cow::Borrowed(_), Cow::Borrowed(_), Cow::Borrowed(_)) => {
unnormalized.to_string()
}
(node, domain, resource) => format!("{node}@{domain}/{resource}"),
}
} else
/* This is another '@' character. */
{
return Err(Error::TooManyAts);
}
} else {
// That is a node@domain JID.
let node = node_check(&unnormalized[..first_index])?;
let domain = domain_check(&unnormalized[first_index + 1..])?;
orig_at = Some(node.len());
orig_slash = None;
match (node, domain) {
(Cow::Borrowed(_), Cow::Borrowed(_)) => unnormalized.to_string(),
(node, domain) => format!("{node}@{domain}"),
}
}
} else
/* This is a '/' character. */
{
// The JID is of the form domain/resource, we can stop looking for further
// characters.
let domain = domain_check(&unnormalized[..first_index])?;
let resource = resource_check(&unnormalized[first_index + 1..])?;
orig_at = None;
orig_slash = Some(domain.len());
match (domain, resource) {
(Cow::Borrowed(_), Cow::Borrowed(_)) => unnormalized.to_string(),
(domain, resource) => format!("{domain}/{resource}"),
}
}
} else {
// Last possible case, just a domain JID.
let domain = domain_check(unnormalized)?;
orig_at = None;
orig_slash = None;
domain.into_owned()
};
Ok(Self {
normalized,
at: orig_at.and_then(|x| NonZeroU16::new(x as u16)),
slash: orig_slash.and_then(|x| NonZeroU16::new(x as u16)),
})
}
/// Returns the inner String of this JID.
pub fn into_inner(self) -> String {
self.normalized
}
/// 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. To avoid
/// allocation if both `node` and `resource` are known to be `None` and
/// `domain` is owned, you can use `domain.into()`.
pub fn from_parts(
node: Option<&NodeRef>,
domain: &DomainRef,
resource: Option<&ResourceRef>,
) -> Self {
match resource {
Some(resource) => FullJid::from_parts(node, domain, resource).into(),
None => BareJid::from_parts(node, domain).into(),
}
}
/// The optional node part of the JID as reference.
pub fn node(&self) -> Option<&NodeRef> {
self.at.map(|at| {
let at = u16::from(at) as usize;
NodeRef::from_str_unchecked(&self.normalized[..at])
})
}
/// The domain part of the JID as reference
pub fn domain(&self) -> &DomainRef {
match (self.at, self.slash) {
(Some(at), Some(slash)) => {
let at = u16::from(at) as usize;
let slash = u16::from(slash) as usize;
DomainRef::from_str_unchecked(&self.normalized[at + 1..slash])
}
(Some(at), None) => {
let at = u16::from(at) as usize;
DomainRef::from_str_unchecked(&self.normalized[at + 1..])
}
(None, Some(slash)) => {
let slash = u16::from(slash) as usize;
DomainRef::from_str_unchecked(&self.normalized[..slash])
}
(None, None) => DomainRef::from_str_unchecked(&self.normalized),
}
}
/// The optional resource of the Jabber ID. It is guaranteed to be present when the JID is
/// a Full variant, which you can check with [`Jid::is_full`].
pub fn resource(&self) -> Option<&ResourceRef> {
self.slash.map(|slash| {
let slash = u16::from(slash) as usize;
ResourceRef::from_str_unchecked(&self.normalized[slash + 1..])
})
}
/// Allocate a new [`BareJid`] from this JID, discarding the resource.
pub fn to_bare(&self) -> BareJid {
BareJid::from_parts(self.node(), self.domain())
}
/// Transforms this JID into a [`BareJid`], throwing away the resource.
///
/// ```
/// # use jid::{BareJid, Jid};
/// let jid: Jid = "foo@bar/baz".parse().unwrap();
/// let bare = jid.into_bare();
/// assert_eq!(bare.to_string(), "foo@bar");
/// ```
pub fn into_bare(mut self) -> BareJid {
if let Some(slash) = self.slash {
// truncate the string
self.normalized.truncate(slash.get() as usize);
self.slash = None;
}
BareJid { inner: self }
}
/// Checks if the JID is a full JID.
pub fn is_full(&self) -> bool {
self.slash.is_some()
}
/// Checks if the JID is a bare JID.
pub fn is_bare(&self) -> bool {
self.slash.is_none()
}
/// Return a reference to the canonical string representation of the JID.
pub fn as_str(&self) -> &str {
&self.normalized
}
/// Try to convert this Jid to a [`FullJid`] if it contains a resource
/// and return a [`BareJid`] otherwise.
///
/// This is useful for match blocks:
///
/// ```
/// # use jid::Jid;
/// let jid: Jid = "foo@bar".parse().unwrap();
/// match jid.try_into_full() {
/// Ok(full) => println!("it is full: {:?}", full),
/// Err(bare) => println!("it is bare: {:?}", bare),
/// }
/// ```
pub fn try_into_full(self) -> Result<FullJid, BareJid> {
if self.slash.is_some() {
Ok(FullJid { inner: self })
} else {
Err(BareJid { inner: self })
}
}
/// Try to convert this Jid reference to a [`&FullJid`][`FullJid`] if it
/// contains a resource and return a [`&BareJid`][`BareJid`] otherwise.
///
/// This is useful for match blocks:
///
/// ```
/// # use jid::Jid;
/// let jid: Jid = "foo@bar".parse().unwrap();
/// match jid.try_as_full() {
/// Ok(full) => println!("it is full: {:?}", full),
/// Err(bare) => println!("it is bare: {:?}", bare),
/// }
/// ```
pub fn try_as_full(&self) -> Result<&FullJid, &BareJid> {
if self.slash.is_some() {
Ok(unsafe {
// SAFETY: FullJid is #[repr(transparent)] of Jid
// SOUNDNESS: we asserted that self.slash is set above
mem::transmute::<&Jid, &FullJid>(self)
})
} else {
Err(unsafe {
// SAFETY: BareJid is #[repr(transparent)] of Jid
// SOUNDNESS: we asserted that self.slash is unset above
mem::transmute::<&Jid, &BareJid>(self)
})
}
}
/// Try to convert this mutable Jid reference to a
/// [`&mut FullJid`][`FullJid`] if it contains a resource and return a
/// [`&mut BareJid`][`BareJid`] otherwise.
pub fn try_as_full_mut(&mut self) -> Result<&mut FullJid, &mut BareJid> {
if self.slash.is_some() {
Ok(unsafe {
// SAFETY: FullJid is #[repr(transparent)] of Jid
// SOUNDNESS: we asserted that self.slash is set above
mem::transmute::<&mut Jid, &mut FullJid>(self)
})
} else {
Err(unsafe {
// SAFETY: BareJid is #[repr(transparent)] of Jid
// SOUNDNESS: we asserted that self.slash is unset above
mem::transmute::<&mut Jid, &mut BareJid>(self)
})
}
}
#[doc(hidden)]
#[allow(non_snake_case)]
#[deprecated(
since = "0.11.0",
note = "use Jid::from (for construction of Jid values) or Jid::try_into_full/Jid::try_as_full (for match blocks) instead"
)]
pub fn Bare(other: BareJid) -> Self {
Self::from(other)
}
#[doc(hidden)]
#[allow(non_snake_case)]
#[deprecated(
since = "0.11.0",
note = "use Jid::from (for construction of Jid values) or Jid::try_into_full/Jid::try_as_full (for match blocks) instead"
)]
pub fn Full(other: FullJid) -> Self {
Self::from(other)
}
}
impl TryFrom<Jid> for FullJid {
type Error = Error;
fn try_from(inner: Jid) -> Result<Self, Self::Error> {
if inner.slash.is_none() {
return Err(Error::ResourceMissingInFullJid);
}
Ok(Self { inner })
}
}
impl TryFrom<Jid> for BareJid {
type Error = Error;
fn try_from(inner: Jid) -> Result<Self, Self::Error> {
if inner.slash.is_some() {
return Err(Error::ResourceInBareJid);
}
Ok(Self { inner })
}
}
impl PartialEq<Jid> for FullJid {
fn eq(&self, other: &Jid) -> bool {
&self.inner == other
}
}
impl PartialEq<Jid> for BareJid {
fn eq(&self, other: &Jid) -> bool {
&self.inner == other
}
}
impl PartialEq<FullJid> for Jid {
fn eq(&self, other: &FullJid) -> bool {
self == &other.inner
}
}
impl PartialEq<BareJid> for Jid {
fn eq(&self, other: &BareJid) -> bool {
self == &other.inner
}
}
/// A struct representing a full Jabber ID, with a resource part.
///
/// A full JID is composed of 3 components, of which only the node is optional:
///
/// - the (optional) node part is the part before the (optional) `@`.
/// - the domain part is the mandatory part between the (optional) `@` and before the `/`.
/// - the resource part after the `/`.
///
/// Unlike a [`BareJid`], it always contains a resource, and should only be used when you are
/// certain there is no case where a resource can be missing. Otherwise, use a [`Jid`] or
/// [`BareJid`].
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)] // WARNING: Jid::try_as_* relies on this for safety!
pub struct FullJid {
inner: Jid,
}
/// A struct representing a bare Jabber ID, without a resource part.
///
/// A bare JID is composed of 2 components, of which only the node is optional:
/// - the (optional) node part is the part before the (optional) `@`.
/// - the domain part is the mandatory part between the (optional) `@` and before the `/`.
///
/// Unlike a [`FullJid`], it cant contain a resource, and should only be used when you are certain
/// there is no case where a resource can be set. Otherwise, use a [`Jid`] or [`FullJid`].
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)] // WARNING: Jid::try_as_* relies on this for safety!
pub struct BareJid {
inner: Jid,
}
impl Deref for FullJid {
type Target = Jid;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl Deref for BareJid {
type Target = Jid;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl Borrow<Jid> for FullJid {
fn borrow(&self) -> &Jid {
&self.inner
}
}
impl Borrow<Jid> for BareJid {
fn borrow(&self) -> &Jid {
&self.inner
}
}
impl fmt::Debug for Jid {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.debug_tuple("Jid").field(&self.normalized).finish()
}
}
impl fmt::Debug for FullJid {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.debug_tuple("FullJid")
.field(&self.inner.normalized)
.finish()
}
}
impl fmt::Debug for BareJid {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.debug_tuple("BareJid")
.field(&self.inner.normalized)
.finish()
}
}
impl fmt::Display for FullJid {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.inner, fmt)
}
}
impl fmt::Display for BareJid {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.inner, fmt)
}
}
#[cfg(feature = "serde")]
impl Serialize for Jid {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.normalized)
}
}
#[cfg(feature = "serde")]
impl Serialize for FullJid {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.inner.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl Serialize for BareJid {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.inner.serialize(serializer)
}
}
impl FromStr for FullJid {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Jid {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Jid::new(&s).map_err(de::Error::custom)
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for FullJid {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let jid = Jid::deserialize(deserializer)?;
jid.try_into().map_err(de::Error::custom)
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for BareJid {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let jid = Jid::deserialize(deserializer)?;
jid.try_into().map_err(de::Error::custom)
}
}
#[cfg(feature = "quote")]
impl ToTokens for Jid {
fn to_tokens(&self, tokens: &mut TokenStream) {
let s = &self.normalized;
tokens.extend(quote! {
::jid::Jid::new(#s).unwrap()
});
}
}
#[cfg(feature = "quote")]
impl ToTokens for FullJid {
fn to_tokens(&self, tokens: &mut TokenStream) {
let s = &self.inner.normalized;
tokens.extend(quote! {
::jid::FullJid::new(#s).unwrap()
});
}
}
#[cfg(feature = "quote")]
impl ToTokens for BareJid {
fn to_tokens(&self, tokens: &mut TokenStream) {
let s = &self.inner.normalized;
tokens.extend(quote! {
::jid::BareJid::new(#s).unwrap()
});
}
}
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
///
/// ```
/// use jid::FullJid;
/// # use jid::Error;
///
/// # fn main() -> Result<(), Error> {
/// let jid = FullJid::new("node@domain/resource")?;
///
/// assert_eq!(jid.node().map(|x| x.as_str()), Some("node"));
/// assert_eq!(jid.domain().as_str(), "domain");
/// assert_eq!(jid.resource().as_str(), "resource");
/// # Ok(())
/// # }
/// ```
pub fn new(unnormalized: &str) -> Result<Self, Error> {
Jid::new(unnormalized)?.try_into()
}
/// 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<&NodeRef>,
domain: &DomainRef,
resource: &ResourceRef,
) -> FullJid {
let (at, slash, normalized);
if let Some(node) = node {
// Parts are never empty so len > 0 for NonZeroU16::new is always Some
at = NonZeroU16::new(node.len() as u16);
slash = NonZeroU16::new((node.len() + 1 + domain.len()) as u16);
normalized = format!("{node}@{domain}/{resource}");
} else {
at = None;
slash = NonZeroU16::new(domain.len() as u16);
normalized = format!("{domain}/{resource}");
}
let inner = Jid {
normalized,
at,
slash,
};
Self { inner }
}
/// The optional resource of the Jabber ID. Since this is a full JID it is always present.
pub fn resource(&self) -> &ResourceRef {
self.inner.resource().unwrap()
}
/// Return the inner String of this full JID.
pub fn into_inner(self) -> String {
self.inner.into_inner()
}
/// Transforms this full JID into a [`BareJid`], throwing away the
/// resource.
///
/// ```
/// # use jid::{BareJid, FullJid};
/// let jid: FullJid = "foo@bar/baz".parse().unwrap();
/// let bare = jid.into_bare();
/// assert_eq!(bare.to_string(), "foo@bar");
/// ```
pub fn into_bare(self) -> BareJid {
self.inner.into_bare()
}
}
impl FromStr for BareJid {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
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
///
/// ```
/// use jid::BareJid;
/// # use jid::Error;
///
/// # fn main() -> Result<(), Error> {
/// let jid = BareJid::new("node@domain")?;
///
/// assert_eq!(jid.node().map(|x| x.as_str()), Some("node"));
/// assert_eq!(jid.domain().as_str(), "domain");
/// # Ok(())
/// # }
/// ```
pub fn new(unnormalized: &str) -> Result<Self, Error> {
Jid::new(unnormalized)?.try_into()
}
/// 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. To avoid
/// allocation if `node` is known to be `None` and `domain` is owned, you
/// can use `domain.into()`.
pub fn from_parts(node: Option<&NodeRef>, domain: &DomainRef) -> Self {
let (at, normalized);
if let Some(node) = node {
// Parts are never empty so len > 0 for NonZeroU16::new is always Some
at = NonZeroU16::new(node.len() as u16);
normalized = format!("{node}@{domain}");
} else {
at = None;
normalized = domain.to_string();
}
let inner = Jid {
normalized,
at,
slash: None,
};
Self { inner }
}
/// Constructs a [`BareJid`] from the bare JID, by specifying a [`ResourcePart`].
/// If you'd like to specify a stringy resource, use [`BareJid::with_resource_str`] instead.
///
/// # Examples
///
/// ```
/// use jid::{BareJid, ResourcePart};
///
/// let resource = ResourcePart::new("resource").unwrap();
/// let bare = BareJid::new("node@domain").unwrap();
/// let full = bare.with_resource(&resource);
///
/// assert_eq!(full.node().map(|x| x.as_str()), Some("node"));
/// assert_eq!(full.domain().as_str(), "domain");
/// assert_eq!(full.resource().as_str(), "resource");
/// ```
pub fn with_resource(&self, resource: &ResourceRef) -> FullJid {
let slash = NonZeroU16::new(self.inner.normalized.len() as u16);
let normalized = format!("{}/{resource}", self.inner.normalized);
let inner = Jid {
normalized,
at: self.inner.at,
slash,
};
FullJid { inner }
}
/// Constructs a [`FullJid`] from the bare JID, by specifying a stringy `resource`.
/// If your resource has already been parsed into a [`ResourcePart`], use [`BareJid::with_resource`].
///
/// # Examples
///
/// ```
/// use jid::BareJid;
///
/// let bare = BareJid::new("node@domain").unwrap();
/// let full = bare.with_resource_str("resource").unwrap();
///
/// assert_eq!(full.node().map(|x| x.as_str()), Some("node"));
/// assert_eq!(full.domain().as_str(), "domain");
/// assert_eq!(full.resource().as_str(), "resource");
/// ```
pub fn with_resource_str(&self, resource: &str) -> Result<FullJid, Error> {
let resource = ResourcePart::new(resource)?;
Ok(self.with_resource(&resource))
}
/// Return the inner String of this bare JID.
pub fn into_inner(self) -> String {
self.inner.into_inner()
}
}
#[cfg(feature = "minidom")]
impl IntoAttributeValue for Jid {
fn into_attribute_value(self) -> Option<String> {
Some(self.to_string())
}
}
#[cfg(feature = "minidom")]
impl From<Jid> for Node {
fn from(jid: Jid) -> Self {
Node::Text(jid.to_string())
}
}
#[cfg(feature = "minidom")]
impl IntoAttributeValue for FullJid {
fn into_attribute_value(self) -> Option<String> {
self.inner.into_attribute_value()
}
}
#[cfg(feature = "minidom")]
impl From<FullJid> for Node {
fn from(jid: FullJid) -> Self {
jid.inner.into()
}
}
#[cfg(feature = "minidom")]
impl IntoAttributeValue for BareJid {
fn into_attribute_value(self) -> Option<String> {
self.inner.into_attribute_value()
}
}
#[cfg(feature = "minidom")]
impl From<BareJid> for Node {
fn from(other: BareJid) -> Self {
other.inner.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::{
collections::{BTreeMap, BTreeSet},
vec::Vec,
};
macro_rules! assert_size (
($t:ty, $sz:expr) => (
assert_eq!(::core::mem::size_of::<$t>(), $sz);
);
);
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(BareJid, 16);
assert_size!(FullJid, 16);
assert_size!(Jid, 16);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(BareJid, 32);
assert_size!(FullJid, 32);
assert_size!(Jid, 32);
}
#[test]
fn can_parse_full_jids() {
assert_eq!(
FullJid::from_str("a@b.c/d"),
Ok(FullJid::new("a@b.c/d").unwrap())
);
assert_eq!(
FullJid::from_str("b.c/d"),
Ok(FullJid::new("b.c/d").unwrap())
);
assert_eq!(
FullJid::from_str("a@b.c"),
Err(Error::ResourceMissingInFullJid)
);
assert_eq!(
FullJid::from_str("b.c"),
Err(Error::ResourceMissingInFullJid)
);
}
#[test]
fn can_parse_bare_jids() {
assert_eq!(
BareJid::from_str("a@b.c"),
Ok(BareJid::new("a@b.c").unwrap())
);
assert_eq!(BareJid::from_str("b.c"), Ok(BareJid::new("b.c").unwrap()));
}
#[test]
fn can_parse_jids() {
let full = FullJid::from_str("a@b.c/d").unwrap();
let bare = BareJid::from_str("e@f.g").unwrap();
assert_eq!(Jid::from_str("a@b.c/d").unwrap(), full);
assert_eq!(Jid::from_str("e@f.g").unwrap(), bare);
}
#[test]
fn full_to_bare_jid() {
let bare: BareJid = FullJid::new("a@b.c/d").unwrap().to_bare();
assert_eq!(bare, BareJid::new("a@b.c").unwrap());
}
#[test]
fn bare_to_full_jid_str() {
assert_eq!(
BareJid::new("a@b.c")
.unwrap()
.with_resource_str("d")
.unwrap(),
FullJid::new("a@b.c/d").unwrap()
);
}
#[test]
fn bare_to_full_jid() {
assert_eq!(
BareJid::new("a@b.c")
.unwrap()
.with_resource(&ResourcePart::new("d").unwrap()),
FullJid::new("a@b.c/d").unwrap()
)
}
#[test]
fn node_from_jid() {
let jid = Jid::new("a@b.c/d").unwrap();
assert_eq!(jid.node().map(|x| x.as_str()), Some("a"),);
}
#[test]
fn domain_from_jid() {
let jid = Jid::new("a@b.c").unwrap();
assert_eq!(jid.domain().as_str(), "b.c");
}
#[test]
fn resource_from_jid() {
let jid = Jid::new("a@b.c/d").unwrap();
assert_eq!(jid.resource().map(|x| x.as_str()), Some("d"),);
}
#[test]
fn jid_to_full_bare() {
let full = FullJid::new("a@b.c/d").unwrap();
let bare = BareJid::new("a@b.c").unwrap();
assert_eq!(FullJid::try_from(Jid::from(full.clone())), Ok(full.clone()));
assert_eq!(
FullJid::try_from(Jid::from(bare.clone())),
Err(Error::ResourceMissingInFullJid),
);
assert_eq!(Jid::from(full.clone().to_bare()), bare.clone());
assert_eq!(Jid::from(bare.clone()), bare);
}
#[test]
fn serialise() {
assert_eq!(FullJid::new("a@b/c").unwrap().to_string(), "a@b/c");
assert_eq!(BareJid::new("a@b").unwrap().to_string(), "a@b");
}
#[test]
fn hash() {
let _map: BTreeMap<Jid, String> = BTreeMap::new();
}
#[test]
fn invalid_jids() {
assert_eq!(BareJid::from_str(""), Err(Error::Idna));
assert_eq!(BareJid::from_str("/c"), Err(Error::Idna));
assert_eq!(BareJid::from_str("a@/c"), Err(Error::Idna));
assert_eq!(BareJid::from_str("@b"), Err(Error::NodeEmpty));
assert_eq!(BareJid::from_str("b/"), Err(Error::ResourceEmpty));
assert_eq!(FullJid::from_str(""), Err(Error::Idna));
assert_eq!(FullJid::from_str("/c"), Err(Error::Idna));
assert_eq!(FullJid::from_str("a@/c"), Err(Error::Idna));
assert_eq!(FullJid::from_str("@b"), Err(Error::NodeEmpty));
assert_eq!(FullJid::from_str("b/"), Err(Error::ResourceEmpty));
assert_eq!(
FullJid::from_str("a@b"),
Err(Error::ResourceMissingInFullJid)
);
assert_eq!(BareJid::from_str("a@b/c"), Err(Error::ResourceInBareJid));
assert_eq!(BareJid::from_str("a@b@c"), Err(Error::TooManyAts));
assert_eq!(FullJid::from_str("a@b@c/d"), Err(Error::TooManyAts));
}
#[test]
fn display_jids() {
assert_eq!(FullJid::new("a@b/c").unwrap().to_string(), "a@b/c");
assert_eq!(BareJid::new("a@b").unwrap().to_string(), "a@b");
assert_eq!(
Jid::from(FullJid::new("a@b/c").unwrap()).to_string(),
"a@b/c"
);
assert_eq!(Jid::from(BareJid::new("a@b").unwrap()).to_string(), "a@b");
}
#[cfg(feature = "minidom")]
#[test]
fn minidom() {
let elem: minidom::Element = "<message xmlns='ns1' from='a@b/c'/>".parse().unwrap();
let to: Jid = elem.attr("from").unwrap().parse().unwrap();
assert_eq!(to, Jid::from(FullJid::new("a@b/c").unwrap()));
let elem: minidom::Element = "<message xmlns='ns1' from='a@b'/>".parse().unwrap();
let to: Jid = elem.attr("from").unwrap().parse().unwrap();
assert_eq!(to, Jid::from(BareJid::new("a@b").unwrap()));
let elem: minidom::Element = "<message xmlns='ns1' from='a@b/c'/>".parse().unwrap();
let to: FullJid = elem.attr("from").unwrap().parse().unwrap();
assert_eq!(to, FullJid::new("a@b/c").unwrap());
let elem: minidom::Element = "<message xmlns='ns1' from='a@b'/>".parse().unwrap();
let to: BareJid = elem.attr("from").unwrap().parse().unwrap();
assert_eq!(to, BareJid::new("a@b").unwrap());
}
#[cfg(feature = "minidom")]
#[test]
fn minidom_into_attr() {
let full = FullJid::new("a@b/c").unwrap();
let elem = minidom::Element::builder("message", "jabber:client")
.attr("from", full.clone())
.build();
assert_eq!(elem.attr("from"), Some(full.to_string().as_str()));
let bare = BareJid::new("a@b").unwrap();
let elem = minidom::Element::builder("message", "jabber:client")
.attr("from", bare.clone())
.build();
assert_eq!(elem.attr("from"), Some(bare.to_string().as_str()));
let jid = Jid::from(bare.clone());
let _elem = minidom::Element::builder("message", "jabber:client")
.attr("from", jid)
.build();
assert_eq!(elem.attr("from"), Some(bare.to_string().as_str()));
}
#[test]
fn stringprep() {
let full = FullJid::from_str("Test@☃.coM/Test™").unwrap();
let equiv = FullJid::new("test@☃.com/TestTM").unwrap();
assert_eq!(full, equiv);
}
#[test]
fn invalid_stringprep() {
FullJid::from_str("a@b/🎉").unwrap_err();
}
#[test]
fn idna() {
let bare = BareJid::from_str("Weiß.com.").unwrap();
let equiv = BareJid::new("weiss.com").unwrap();
assert_eq!(bare, equiv);
BareJid::from_str("127.0.0.1").unwrap();
BareJid::from_str("[::1]").unwrap();
BareJid::from_str("domain.tld.").unwrap();
}
#[test]
fn invalid_idna() {
BareJid::from_str("a@b@c").unwrap_err();
FullJid::from_str("a@b@c/d").unwrap_err();
BareJid::from_str("[::1234").unwrap_err();
BareJid::from_str("1::1234]").unwrap_err();
BareJid::from_str("domain.tld:5222").unwrap_err();
BareJid::from_str("-domain.tld").unwrap_err();
BareJid::from_str("domain.tld-").unwrap_err();
BareJid::from_str("domain..tld").unwrap_err();
BareJid::from_str("domain.tld..").unwrap_err();
BareJid::from_str("1234567890123456789012345678901234567890123456789012345678901234.com")
.unwrap_err();
}
#[test]
fn jid_from_parts() {
let node = NodePart::new("node").unwrap();
let domain = DomainPart::new("domain").unwrap();
let resource = ResourcePart::new("resource").unwrap();
let jid = Jid::from_parts(Some(&node), &domain, Some(&resource));
assert_eq!(jid, Jid::new("node@domain/resource").unwrap());
let barejid = BareJid::from_parts(Some(&node), &domain);
assert_eq!(barejid, BareJid::new("node@domain").unwrap());
let fulljid = FullJid::from_parts(Some(&node), &domain, &resource);
assert_eq!(fulljid, FullJid::new("node@domain/resource").unwrap());
}
#[test]
#[cfg(feature = "serde")]
fn jid_ser_de() {
let jid: Jid = Jid::new("node@domain").unwrap();
serde_test::assert_tokens(&jid, &[serde_test::Token::Str("node@domain")]);
let jid: Jid = Jid::new("node@domain/resource").unwrap();
serde_test::assert_tokens(&jid, &[serde_test::Token::Str("node@domain/resource")]);
let jid: BareJid = BareJid::new("node@domain").unwrap();
serde_test::assert_tokens(&jid, &[serde_test::Token::Str("node@domain")]);
let jid: FullJid = FullJid::new("node@domain/resource").unwrap();
serde_test::assert_tokens(&jid, &[serde_test::Token::Str("node@domain/resource")]);
}
#[test]
fn jid_into_parts_and_from_parts() {
let node = NodePart::new("node").unwrap();
let domain = DomainPart::new("domain").unwrap();
let jid1 = domain.with_node(&node);
let jid2 = node.with_domain(&domain);
let jid3 = BareJid::new("node@domain").unwrap();
assert_eq!(jid1, jid2);
assert_eq!(jid2, jid3);
}
#[test]
fn jid_match_replacement_try_as() {
let jid1 = Jid::new("foo@bar").unwrap();
let jid2 = Jid::new("foo@bar/baz").unwrap();
match jid1.try_as_full() {
Err(_) => (),
other => panic!("unexpected result: {:?}", other),
};
match jid2.try_as_full() {
Ok(_) => (),
other => panic!("unexpected result: {:?}", other),
};
}
#[test]
fn jid_match_replacement_try_as_mut() {
let mut jid1 = Jid::new("foo@bar").unwrap();
let mut jid2 = Jid::new("foo@bar/baz").unwrap();
match jid1.try_as_full_mut() {
Err(_) => (),
other => panic!("unexpected result: {:?}", other),
};
match jid2.try_as_full_mut() {
Ok(_) => (),
other => panic!("unexpected result: {:?}", other),
};
}
#[test]
fn jid_match_replacement_try_into() {
let jid1 = Jid::new("foo@bar").unwrap();
let jid2 = Jid::new("foo@bar/baz").unwrap();
match jid1.try_as_full() {
Err(_) => (),
other => panic!("unexpected result: {:?}", other),
};
match jid2.try_as_full() {
Ok(_) => (),
other => panic!("unexpected result: {:?}", other),
};
}
#[test]
fn lookup_jid_by_full_jid() {
let mut map: BTreeSet<Jid> = BTreeSet::new();
let jid1 = Jid::new("foo@bar").unwrap();
let jid2 = Jid::new("foo@bar/baz").unwrap();
let jid3 = FullJid::new("foo@bar/baz").unwrap();
map.insert(jid1);
assert!(!map.contains(&jid2));
assert!(!map.contains(&jid3));
map.insert(jid2);
assert!(map.contains(&jid3));
}
#[test]
fn lookup_full_jid_by_jid() {
let mut map: BTreeSet<FullJid> = BTreeSet::new();
let jid1 = FullJid::new("foo@bar/baz").unwrap();
let jid2 = FullJid::new("foo@bar/fnord").unwrap();
let jid3 = Jid::new("foo@bar/fnord").unwrap();
map.insert(jid1);
assert!(!map.contains(&jid2));
assert!(!map.contains(&jid3));
map.insert(jid2);
assert!(map.contains(&jid3));
}
#[test]
fn lookup_bare_jid_by_jid() {
let mut map: BTreeSet<BareJid> = BTreeSet::new();
let jid1 = BareJid::new("foo@bar").unwrap();
let jid2 = BareJid::new("foo@baz").unwrap();
let jid3 = Jid::new("foo@baz").unwrap();
map.insert(jid1);
assert!(!map.contains(&jid2));
assert!(!map.contains(&jid3));
map.insert(jid2);
assert!(map.contains(&jid3));
}
#[test]
fn normalizes_all_parts() {
assert_eq!(
Jid::new("ßA@IX.test/\u{2168}").unwrap().as_str(),
"ssa@ix.test/IX"
);
}
#[test]
fn rejects_unassigned_codepoints() {
match Jid::new("\u{01f601}@example.com") {
Err(Error::NodePrep) => (),
other => panic!("unexpected result: {:?}", other),
};
match Jid::new("foo@\u{01f601}.example.com") {
Err(Error::NamePrep) => (),
other => panic!("unexpected result: {:?}", other),
};
match Jid::new("foo@example.com/\u{01f601}") {
Err(Error::ResourcePrep) => (),
other => panic!("unexpected result: {:?}", other),
};
}
#[test]
fn accepts_domain_only_jid() {
match Jid::new("example.com") {
Ok(_) => (),
other => panic!("unexpected result: {:?}", other),
};
match BareJid::new("example.com") {
Ok(_) => (),
other => panic!("unexpected result: {:?}", other),
};
match FullJid::new("example.com/x") {
Ok(_) => (),
other => panic!("unexpected result: {:?}", other),
};
}
#[test]
fn is_bare_returns_true_iff_bare() {
let bare = Jid::new("foo@bar").unwrap();
let full = Jid::new("foo@bar/baz").unwrap();
assert!(bare.is_bare());
assert!(!full.is_bare());
}
#[test]
fn is_full_returns_true_iff_full() {
let bare = Jid::new("foo@bar").unwrap();
let full = Jid::new("foo@bar/baz").unwrap();
assert!(!bare.is_full());
assert!(full.is_full());
}
#[test]
fn reject_long_localpart() {
let mut long = Vec::with_capacity(1028);
long.resize(1024, b'a');
let mut long = String::from_utf8(long).unwrap();
long.push_str("@foo");
match Jid::new(&long) {
Err(Error::NodeTooLong) => (),
other => panic!("unexpected result: {:?}", other),
}
match BareJid::new(&long) {
Err(Error::NodeTooLong) => (),
other => panic!("unexpected result: {:?}", other),
}
}
#[test]
fn reject_long_domainpart() {
let mut long = Vec::with_capacity(66);
long.push(b'x');
long.push(b'@');
long.resize(66, b'a');
let long = String::from_utf8(long).unwrap();
Jid::new(&long).unwrap_err();
BareJid::new(&long).unwrap_err();
// A domain can be up to 253 bytes.
let mut long = Vec::with_capacity(256);
long.push(b'x');
long.push(b'@');
long.resize(65, b'a');
long.push(b'.');
long.resize(129, b'b');
long.push(b'.');
long.resize(193, b'c');
long.push(b'.');
long.resize(256, b'd');
let long = String::from_utf8(long).unwrap();
Jid::new(&long).unwrap_err();
BareJid::new(&long).unwrap_err();
}
#[test]
fn reject_long_resourcepart() {
let mut long = Vec::with_capacity(1028);
long.push(b'x');
long.push(b'@');
long.push(b'y');
long.push(b'/');
long.resize(1028, b'a');
let long = String::from_utf8(long).unwrap();
match Jid::new(&long) {
Err(Error::ResourceTooLong) => (),
other => panic!("unexpected result: {:?}", other),
}
match FullJid::new(&long) {
Err(Error::ResourceTooLong) => (),
other => panic!("unexpected result: {:?}", other),
}
}
}