#![cfg_attr(not(feature = "try_from"), doc = " ```rust,ignore")]
#![cfg_attr(feature = "try_from", doc = " ```rust")]
#![cfg_attr(not(feature = "try_from"), doc = " ```rust,ignore")]
#![cfg_attr(feature = "try_from", doc = " ```rust")]
#![deny(missing_docs)]
#![cfg_attr(feature = "try_from", feature(try_from))]
#![doc(html_root_url = "https://docs.rs/xmpp-addr/0.13.1")]
use unicode_normalization::UnicodeNormalization;
use std::borrow;
use std::cmp;
use std::convert;
use std::fmt;
use std::net;
use std::result;
use std::str;
use std::str::FromStr;
#[derive(Debug)]
pub enum Error {
EmptyJid,
EmptyLocal,
LongLocal,
ShortDomain,
LongDomain,
EmptyResource,
LongResource,
ForbiddenChars,
Addr(net::AddrParseError),
IDNA(idna::uts46::Errors),
}
pub type Result<T> = result::Result<T, Error>;
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub struct Jid<'a> {
local: Option<borrow::Cow<'a, str>>,
domain: borrow::Cow<'a, str>,
resource: Option<borrow::Cow<'a, str>>,
}
impl<'a> Jid<'a> {
pub fn split(s: &'a str) -> Result<(Option<&'a str>, &'a str, Option<&'a str>)> {
if s == "" {
return Err(Error::EmptyJid);
}
let mut chars = s.char_indices();
let sep = chars.find(|&c| match c {
(_, '@') | (_, '/') => true,
_ => false,
});
let (lpart, dpart, rpart) = match sep {
None => (None, s, None),
Some((i, '/')) => (None, &s[0..i], Some(&s[i + 1..])),
Some((i, '@')) if i + 1 == s.len() => return Err(Error::ShortDomain),
Some((i, '@')) => {
let slash = chars.find(|&c| match c {
(_, '/') => true,
_ => false,
});
if s[0..i].contains(&['"', '&', '\'', '/', ':', '<', '>', '@', '`'][..]) {
return Err(Error::ForbiddenChars);
}
match slash {
None => (Some(&s[0..i]), &s[i + 1..], None),
Some((j, _)) if j - i < 3 => return Err(Error::ShortDomain),
Some((j, _)) => (Some(&s[0..i]), &s[i + 1..j], Some(&s[j + 1..])),
}
}
_ => unreachable!(),
};
Ok((lpart, dpart.trim_end_matches('.'), rpart))
}
pub fn new<L, R>(local: L, domain: &'a str, resource: R) -> Result<Jid<'a>>
where
L: Into<Option<&'a str>>,
R: Into<Option<&'a str>>,
{
Ok(Jid {
local: match local.into() {
None => None,
Some(l) => Some(Jid::process_local(l)?),
},
domain: match Jid::process_domain(domain) {
Err(err) => return Err(err),
Ok(d) => d,
},
resource: match resource.into() {
None => None,
Some(r) => Some(Jid::process_resource(r)?),
},
})
}
fn process_local(local: &'a str) -> Result<borrow::Cow<'a, str>> {
let local: borrow::Cow<'a, str> = if local.is_ascii() {
if local.bytes().all(|c| c.is_ascii_lowercase()) {
local.into()
} else {
local.to_ascii_lowercase().into()
}
} else {
local.chars().flat_map(|c| c.to_lowercase()).nfc().collect()
};
match local.len() {
0 => Err(Error::EmptyLocal),
l if l > 1023 => Err(Error::LongLocal),
_ => Ok(local),
}
}
fn process_domain(domain: &'a str) -> Result<borrow::Cow<'a, str>> {
let is_v6 = if domain.starts_with('[') && domain.ends_with(']') {
let inner = unsafe { domain.get_unchecked(1..domain.len() - 1) };
match net::Ipv6Addr::from_str(inner) {
Ok(_) => true,
Err(v) => return Err(Error::Addr(v)),
}
} else {
false
};
let dlabel: borrow::Cow<'a, str> = if !is_v6 {
let (dlabel, result) = idna::domain_to_unicode(domain);
match result {
Ok(_) => dlabel.into(),
Err(e) => return Err(Error::IDNA(e)),
}
} else {
domain.into()
};
if dlabel.len() > 1023 {
return Err(Error::LongDomain);
}
if dlabel.len() < 1 {
return Err(Error::ShortDomain);
}
Ok(dlabel)
}
fn process_resource(res: &'a str) -> Result<borrow::Cow<'a, str>> {
let res: borrow::Cow<'a, str> = if res.is_ascii() {
res.into()
} else {
res.chars()
.map(|c| if c.is_whitespace() { '\u{0020}' } else { c })
.nfc()
.collect()
};
match res.len() {
0 => Err(Error::EmptyResource),
r if r > 1023 => Err(Error::LongResource),
_ => Ok(res),
}
}
pub fn from_domain(domain: &'a str) -> Result<Jid<'a>> {
Jid::new(None, domain, None)
}
pub fn bare(self) -> Jid<'a> {
Jid {
local: self.local,
domain: self.domain,
resource: None,
}
}
pub fn domain(self) -> Jid<'a> {
Jid {
local: None,
domain: self.domain,
resource: None,
}
}
pub fn with_local<T: Into<Option<&'a str>>>(self, local: T) -> Result<Jid<'a>> {
Ok(Jid {
local: match local.into() {
Some(l) => Some(Jid::process_local(l)?),
None => None,
},
domain: self.domain,
resource: self.resource,
})
}
pub fn with_domain(self, domain: &'a str) -> Result<Jid<'a>> {
Ok(Jid {
local: self.local,
domain: match Jid::process_domain(domain) {
Err(err) => return Err(err),
Ok(d) => d,
},
resource: self.resource,
})
}
pub fn with_resource<T: Into<Option<&'a str>>>(self, resource: T) -> Result<Jid<'a>> {
Ok(Jid {
local: self.local,
domain: self.domain,
resource: match resource.into() {
Some(r) => Some(Jid::process_resource(r)?),
None => None,
},
})
}
pub fn from_str(s: &'a str) -> Result<Jid<'a>> {
let (lpart, dpart, rpart) = Jid::split(s)?;
Jid::new(lpart, dpart, rpart)
}
pub fn localpart(&self) -> Option<&str> {
match self.local {
None => None,
Some(ref l) => Some(&l[..]),
}
}
pub fn domainpart(&self) -> &str {
&(self.domain)
}
pub fn resourcepart(&self) -> Option<&str> {
match self.resource {
None => None,
Some(ref r) => Some(&r[..]),
}
}
pub unsafe fn new_unchecked<L, R>(local: L, domain: &'a str, resource: R) -> Jid<'a>
where
L: Into<Option<&'a str>>,
R: Into<Option<&'a str>>,
{
Jid {
local: match local.into() {
None => None,
Some(s) => Some(s.into()),
},
domain: domain.into(),
resource: match resource.into() {
None => None,
Some(s) => Some(s.into()),
},
}
}
}
impl fmt::Display for Jid<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.local {
None => {}
Some(ref l) => write!(f, "{}@", l)?,
}
write!(f, "{}", self.domain)?;
match self.resource {
None => {}
Some(ref r) => write!(f, "/{}", r)?,
}
Ok(())
}
}
#[cfg_attr(not(feature = "try_from"), doc = " ```rust,ignore")]
#[cfg_attr(feature = "try_from", doc = " ```rust")]
#[cfg(feature = "try_from")]
impl<'a> convert::TryFrom<(&'a str, &'a str)> for Jid<'a> {
type Error = Error;
fn try_from(parts: (&'a str, &'a str)) -> result::Result<Self, Self::Error> {
Jid::new(Some(parts.0), parts.1, None)
}
}
#[cfg_attr(not(feature = "try_from"), doc = " ```rust,ignore")]
#[cfg_attr(feature = "try_from", doc = " ```rust")]
#[cfg(feature = "try_from")]
impl<'a> convert::TryFrom<(Option<&'a str>, &'a str)> for Jid<'a> {
type Error = Error;
fn try_from(parts: (Option<&'a str>, &'a str)) -> result::Result<Self, Self::Error> {
Jid::new(parts.0, parts.1, None)
}
}
#[cfg_attr(not(feature = "try_from"), doc = " ```rust,ignore")]
#[cfg_attr(feature = "try_from", doc = " ```rust")]
#[cfg(feature = "try_from")]
impl<'a> convert::TryFrom<(&'a str, Option<&'a str>)> for Jid<'a> {
type Error = Error;
fn try_from(parts: (&'a str, Option<&'a str>)) -> result::Result<Self, Self::Error> {
Jid::new(None, parts.0, parts.1)
}
}
#[cfg_attr(not(feature = "try_from"), doc = " ```rust,ignore")]
#[cfg_attr(feature = "try_from", doc = " ```rust")]
#[cfg(feature = "try_from")]
impl<'a> convert::TryFrom<(&'a str, &'a str, &'a str)> for Jid<'a> {
type Error = Error;
fn try_from(parts: (&'a str, &'a str, &'a str)) -> result::Result<Self, Self::Error> {
Jid::new(Some(parts.0), parts.1, Some(parts.2))
}
}
#[cfg_attr(not(feature = "try_from"), doc = " ```rust,ignore")]
#[cfg_attr(feature = "try_from", doc = " ```rust")]
#[cfg(feature = "try_from")]
impl<'a> convert::TryFrom<(Option<&'a str>, &'a str, Option<&'a str>)> for Jid<'a> {
type Error = Error;
fn try_from(
parts: (Option<&'a str>, &'a str, Option<&'a str>),
) -> result::Result<Self, Self::Error> {
Jid::new(parts.0, parts.1, parts.2)
}
}
#[cfg_attr(not(feature = "try_from"), doc = " ```rust,ignore")]
#[cfg_attr(feature = "try_from", doc = " ```rust")]
#[cfg(feature = "try_from")]
impl<'a> convert::TryFrom<&'a str> for Jid<'a> {
type Error = Error;
fn try_from(s: &'a str) -> result::Result<Self, Self::Error> {
Jid::from_str(s)
}
}
impl<'a> convert::From<net::Ipv4Addr> for Jid<'a> {
fn from(addr: net::Ipv4Addr) -> Jid<'a> {
Jid {
local: None,
domain: format!("{}", addr).into(),
resource: None,
}
}
}
impl<'a> convert::From<net::Ipv6Addr> for Jid<'a> {
fn from(addr: net::Ipv6Addr) -> Jid<'a> {
Jid {
local: None,
domain: format!("[{}]", addr).into(),
resource: None,
}
}
}
impl<'a> convert::From<net::IpAddr> for Jid<'a> {
fn from(addr: net::IpAddr) -> Jid<'a> {
match addr {
net::IpAddr::V6(v6) => v6.into(),
net::IpAddr::V4(v4) => v4.into(),
}
}
}
impl cmp::PartialEq<str> for Jid<'_> {
fn eq(&self, other: &str) -> bool {
if match Jid::split(other) {
Err(_) => false,
Ok(p) => {
let local_match = match p.0 {
None => self.local.is_none(),
Some(s) => match self.local {
None => false,
Some(ref l) => s == l,
},
};
let res_match = match p.2 {
None => self.resource.is_none(),
Some(s) => match self.resource {
None => false,
Some(ref r) => s == r,
},
};
local_match && p.1 == self.domain && res_match
}
} {
return true;
}
match Jid::from_str(other) {
Ok(j) => j.eq(self),
Err(_) => false,
}
}
}
impl cmp::PartialEq<Jid<'_>> for str {
fn eq(&self, other: &Jid<'_>) -> bool {
PartialEq::eq(other, self)
}
}
macro_rules! impl_eq {
($lhs:ty, $rhs:ty) => {
impl<'a, 'b> PartialEq<$lhs> for $rhs {
#[inline]
fn eq(&self, other: &$lhs) -> bool {
PartialEq::eq(self, &other[..])
}
}
impl<'a, 'b> PartialEq<$rhs> for $lhs {
#[inline]
fn eq(&self, other: &$rhs) -> bool {
PartialEq::eq(&self[..], other)
}
}
};
}
impl_eq! { borrow::Cow<'b, str>, Jid<'a> }
impl_eq! { &'b str, Jid<'a> }
impl_eq! { String, Jid<'a> }