use core::{cmp::Ordering, fmt};
use crate::std::{self as std, string::String, vec::Vec};
use super::Host;
use rama_core::bytes::Bytes;
mod label;
#[doc(inline)]
pub use label::{Label, LabelError};
mod labels;
#[doc(inline)]
pub use labels::{DomainLabelIter, DomainLabels, SuffixIter};
mod builder;
#[doc(inline)]
pub use builder::{DomainBuilder, PushError};
pub const MAX_NAME_LEN: usize = 253;
#[derive(Debug, Clone)]
pub struct Domain(Bytes);
impl Domain {
pub const MAX_LEN: usize = MAX_NAME_LEN;
#[must_use]
#[expect(
clippy::panic,
reason = "static-str invariant: panic at compile time when the static is invalid"
)]
pub const fn from_static(s: &'static str) -> Self {
if !is_valid_name(s.as_bytes()) {
panic!("static str is an invalid domain");
}
Self(Bytes::from_static(s.as_bytes()))
}
pub(crate) unsafe fn from_maybe_borrowed_unchecked(s: impl Into<Bytes>) -> Self {
let bytes = s.into();
debug_assert!(
is_valid_name(&bytes),
"from_maybe_borrowed_unchecked called with invalid domain bytes"
);
Self(bytes)
}
#[must_use]
#[inline(always)]
pub const fn example() -> Self {
Self::from_static("example.com")
}
#[must_use]
#[inline(always)]
pub const fn tld_private() -> Self {
Self::from_static("internal")
}
#[must_use]
#[inline(always)]
pub const fn tld_localhost() -> Self {
Self::from_static("localhost")
}
#[must_use]
pub const fn into_host(self) -> Host {
Host::Name(self)
}
#[must_use]
pub fn is_fqdn(&self) -> bool {
self.0.last() == Some(&b'.')
}
#[must_use]
pub fn is_wildcard(&self) -> bool {
self.labels().next().is_some_and(Label::is_wildcard)
}
#[must_use]
pub fn is_tld(&self) -> bool {
self.suffix()
.map(|s| cmp_domain(self.as_str(), s).is_eq())
.unwrap_or_default()
}
#[must_use]
pub fn is_sld(&self) -> bool {
self.suffix()
.and_then(|s| self.as_str().strip_suffix(s))
.map(|s| {
let s = s.trim_matches('.');
!(s.is_empty() || s.contains('.'))
})
.unwrap_or_default()
}
#[must_use]
pub fn is_loopback(&self) -> bool {
is_loopback_name(self.as_str())
}
#[must_use]
pub fn as_wildcard_parent(&self) -> Option<Self> {
let mut it = self.labels();
let first = it.next()?;
if !first.is_wildcard() {
return None;
}
let mut b = DomainBuilder::new();
b.push_labels(it).ok()?;
b.finish().ok()
}
pub fn try_as_sub(&self, sub: impl AsDomainRef) -> Result<Self, PushError> {
let mut b = DomainBuilder::new();
b.push_label_segments(sub.domain_as_str())?;
b.append(self)?;
b.finish()
}
pub fn try_as_wildcard(&self) -> Result<Self, PushError> {
let mut b = DomainBuilder::new();
b.push_label("*")?;
b.append(self)?;
b.finish()
}
pub fn strip_sub(&self, prefix: impl AsDomainRef) -> Option<Self> {
let prefix_str = prefix.domain_as_str();
let mut self_labels = self.labels();
for prefix_seg in dotted_segments(prefix_str) {
let self_label = self_labels.next()?;
if !self_label.as_str().eq_ignore_ascii_case(prefix_seg) {
return None;
}
}
let mut b = DomainBuilder::new();
b.push_labels(&mut self_labels).ok()?;
if b.is_empty() {
return None;
}
b.finish().ok()
}
#[must_use]
pub fn is_sub_of(&self, other: &Self) -> bool {
<Self as DomainLabels>::is_subdomain_of(self, other)
}
#[must_use]
pub fn is_parent_of(&self, other: &Self) -> bool {
other.is_sub_of(self)
}
#[must_use]
pub fn have_same_registrable_domain(&self, other: &Self) -> bool {
let this_rd = psl::domain_str(self.as_str());
let other_rd = psl::domain_str(other.as_str());
this_rd == other_rd
}
#[must_use]
pub fn suffix(&self) -> Option<&str> {
psl::suffix_str(self.as_str())
}
#[expect(clippy::len_without_is_empty)]
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn as_str(&self) -> &str {
unsafe { core::str::from_utf8_unchecked(&self.0) }
}
#[must_use]
#[inline]
pub fn view(&self) -> DomainRef<'_> {
DomainRef::from(self)
}
#[cfg(feature = "idna")]
#[cfg_attr(docsrs, doc(cfg(feature = "idna")))]
#[must_use]
pub fn as_unicode(&self) -> crate::std::borrow::Cow<'_, str> {
DomainRef::from(self).as_unicode()
}
}
pub(crate) fn is_loopback_name(name: &str) -> bool {
let name = name.strip_prefix('.').unwrap_or(name);
let name = name.strip_suffix('.').unwrap_or(name);
name.rsplit('.')
.next()
.is_some_and(|tld| tld.eq_ignore_ascii_case("localhost"))
}
#[derive(Debug, Clone, Copy)]
pub struct DomainRef<'a> {
bytes: &'a [u8],
}
impl<'a> DomainRef<'a> {
#[must_use]
pub fn as_bytes(&self) -> &'a [u8] {
self.bytes
}
#[must_use]
pub fn as_str(&self) -> &'a str {
unsafe { core::str::from_utf8_unchecked(self.bytes) }
}
#[must_use]
pub fn is_loopback(self) -> bool {
is_loopback_name(self.as_str())
}
#[must_use]
pub fn into_owned(self) -> Domain {
let bytes = Bytes::copy_from_slice(self.bytes);
unsafe { Domain::from_maybe_borrowed_unchecked(bytes) }
}
#[cfg(feature = "idna")]
#[cfg_attr(docsrs, doc(cfg(feature = "idna")))]
#[must_use]
pub fn as_unicode(&self) -> crate::std::borrow::Cow<'a, str> {
let s = self.as_str();
if memchr::memmem::find(s.as_bytes(), b"xn--").is_none() {
return crate::std::borrow::Cow::Borrowed(s);
}
let (unicode, _result) = idna::domain_to_unicode(s);
crate::std::borrow::Cow::Owned(unicode)
}
}
impl<'a> From<&'a Domain> for DomainRef<'a> {
fn from(d: &'a Domain) -> Self {
Self { bytes: &d.0 }
}
}
impl PartialEq for DomainRef<'_> {
fn eq(&self, other: &Self) -> bool {
eq_segments(
dotted_segments(self.as_str()),
dotted_segments(other.as_str()),
)
}
}
impl Eq for DomainRef<'_> {}
impl Ord for DomainRef<'_> {
fn cmp(&self, other: &Self) -> Ordering {
cmp_segments(
dotted_segments(self.as_str()),
dotted_segments(other.as_str()),
)
}
}
impl PartialOrd for DomainRef<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl core::hash::Hash for DomainRef<'_> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
for seg in dotted_segments(self.as_str()) {
state.write_usize(seg.len());
for b in seg.bytes() {
state.write_u8(b.to_ascii_lowercase());
}
}
}
}
impl fmt::Display for DomainRef<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl core::hash::Hash for Domain {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
for label in self.labels() {
label.hash(state);
}
}
}
impl AsRef<str> for Domain {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for Domain {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
impl core::str::FromStr for Domain {
type Err = DomainParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s)
}
}
impl TryFrom<String> for Domain {
type Error = DomainParseError;
fn try_from(name: String) -> Result<Self, Self::Error> {
if name.is_ascii() {
validate_domain_str(&name)?;
return Ok(Self(Bytes::from(name.into_bytes())));
}
let ace = idn_to_ascii(&name)?;
validate_domain_str(&ace)?;
Ok(Self(Bytes::from(ace.into_bytes())))
}
}
impl<'a> TryFrom<&'a str> for Domain {
type Error = DomainParseError;
fn try_from(name: &'a str) -> Result<Self, Self::Error> {
if name.is_ascii() {
validate_domain_str(name)?;
return Ok(Self(Bytes::copy_from_slice(name.as_bytes())));
}
let ace = idn_to_ascii(name)?;
validate_domain_str(&ace)?;
Ok(Self(Bytes::from(ace.into_bytes())))
}
}
impl<'a> TryFrom<&'a [u8]> for Domain {
type Error = DomainParseError;
fn try_from(name: &'a [u8]) -> Result<Self, Self::Error> {
let s = core::str::from_utf8(name).map_err(DomainParseError::non_utf8)?;
Self::try_from(s)
}
}
impl TryFrom<Vec<u8>> for Domain {
type Error = DomainParseError;
fn try_from(name: Vec<u8>) -> Result<Self, Self::Error> {
let s = core::str::from_utf8(&name).map_err(DomainParseError::non_utf8)?;
if s.is_ascii() {
validate_domain_str(s)?;
return Ok(Self(Bytes::from(name)));
}
let ace = idn_to_ascii(s)?;
validate_domain_str(&ace)?;
Ok(Self(Bytes::from(ace.into_bytes())))
}
}
fn idn_to_ascii(input: &str) -> Result<String, DomainParseError> {
#[cfg(feature = "idna")]
{
idna::domain_to_ascii(input).map_err(|_e| DomainParseError::idna_processing())
}
#[cfg(not(feature = "idna"))]
{
let _ = input;
Err(DomainParseError::idna_not_enabled())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DomainParseError(DomainParseErrorKind);
#[derive(Debug, Clone, PartialEq, Eq)]
enum DomainParseErrorKind {
Empty,
TooLong {
len: usize,
},
NonUtf8 {
source: core::str::Utf8Error,
},
Label {
at: usize,
error: LabelError,
},
BadWildcard {
at: usize,
},
BracketedIpLiteral,
#[cfg(feature = "idna")]
IdnaProcessing,
#[cfg(not(feature = "idna"))]
IdnaNotEnabled,
}
impl DomainParseError {
#[inline]
const fn empty() -> Self {
Self(DomainParseErrorKind::Empty)
}
#[inline]
const fn too_long(len: usize) -> Self {
Self(DomainParseErrorKind::TooLong { len })
}
#[inline]
fn non_utf8(source: core::str::Utf8Error) -> Self {
Self(DomainParseErrorKind::NonUtf8 { source })
}
#[inline]
const fn label(at: usize, error: LabelError) -> Self {
Self(DomainParseErrorKind::Label { at, error })
}
#[inline]
const fn bad_wildcard(at: usize) -> Self {
Self(DomainParseErrorKind::BadWildcard { at })
}
#[inline]
pub(crate) const fn bracketed_ip_literal() -> Self {
Self(DomainParseErrorKind::BracketedIpLiteral)
}
#[cfg(feature = "idna")]
#[inline]
fn idna_processing() -> Self {
Self(DomainParseErrorKind::IdnaProcessing)
}
#[cfg(not(feature = "idna"))]
#[inline]
fn idna_not_enabled() -> Self {
Self(DomainParseErrorKind::IdnaNotEnabled)
}
#[must_use]
pub fn is_idna_not_enabled(&self) -> bool {
#[cfg(not(feature = "idna"))]
{
matches!(self.0, DomainParseErrorKind::IdnaNotEnabled)
}
#[cfg(feature = "idna")]
{
false
}
}
}
impl fmt::Display for DomainParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
DomainParseErrorKind::Empty => f.write_str("empty domain"),
DomainParseErrorKind::TooLong { len } => {
write!(f, "domain is {len} bytes long, max is {MAX_NAME_LEN}")
}
DomainParseErrorKind::NonUtf8 { source } => {
write!(f, "domain bytes are not valid UTF-8: {source}")
}
DomainParseErrorKind::Label { at, error } => {
write!(f, "invalid label at index {at}: {error}")
}
DomainParseErrorKind::BadWildcard { at } => write!(
f,
"'*' wildcard label is only valid at index 0, found at index {at}"
),
DomainParseErrorKind::BracketedIpLiteral => {
f.write_str("bracketed IP-literal is not a domain")
}
#[cfg(feature = "idna")]
DomainParseErrorKind::IdnaProcessing => {
f.write_str("IDN domain rejected by UTS #46 processing")
}
#[cfg(not(feature = "idna"))]
DomainParseErrorKind::IdnaNotEnabled => {
f.write_str("non-ASCII domain requires the `idna` feature")
}
}
}
}
impl core::error::Error for DomainParseError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match &self.0 {
DomainParseErrorKind::Label { error, .. } => Some(error),
DomainParseErrorKind::NonUtf8 { source } => Some(source),
_ => None,
}
}
}
const fn validate_domain_bytes(name: &[u8]) -> Result<(), DomainParseError> {
if name.is_empty() {
return Err(DomainParseError::empty());
}
let last = name[name.len() - 1];
let effective_len = if last == b'.' {
name.len() - 1
} else {
name.len()
};
if effective_len > MAX_NAME_LEN {
return Err(DomainParseError::too_long(name.len()));
}
let start = if name[0] == b'.' { 1 } else { 0 };
let stop = if last == b'.' && name.len() > 1 {
name.len() - 1
} else {
name.len()
};
if start >= stop {
return Err(DomainParseError::empty());
}
let mut idx: usize = 0;
let mut label_start = start;
let mut leftmost_was_wildcard = false;
let mut i = start;
while i <= stop {
if i == stop || name[i] == b'.' {
if label_start == i {
return Err(DomainParseError::label(idx, LabelError::empty()));
}
let (_, after_start) = name.split_at(label_start);
let (label, _) = after_start.split_at(i - label_start);
match label::validate_label_bytes(label) {
Ok(()) => {}
Err(e) => return Err(DomainParseError::label(idx, e)),
}
if label.len() == 1 && label[0] == b'*' {
if idx != 0 {
return Err(DomainParseError::bad_wildcard(idx));
}
leftmost_was_wildcard = true;
}
label_start = i + 1;
idx += 1;
if i == stop {
break;
}
}
i += 1;
}
if leftmost_was_wildcard && idx == 1 {
return Err(DomainParseError::bad_wildcard(0));
}
Ok(())
}
const fn is_valid_name(name: &[u8]) -> bool {
matches!(validate_domain_bytes(name), Ok(()))
}
fn validate_domain_str(s: &str) -> Result<(), DomainParseError> {
validate_domain_bytes(s.as_bytes())
}
fn dotted_segments(s: &str) -> impl DoubleEndedIterator<Item = &str> + Clone {
let s = s.strip_prefix('.').unwrap_or(s);
let s = s.strip_suffix('.').unwrap_or(s);
s.split('.').filter(|x| !x.is_empty())
}
fn cmp_segments<'a>(
mut a: impl Iterator<Item = &'a str>,
mut b: impl Iterator<Item = &'a str>,
) -> Ordering {
loop {
match (a.next(), b.next()) {
(None, None) => return Ordering::Equal,
(Some(_), None) => return Ordering::Greater,
(None, Some(_)) => return Ordering::Less,
(Some(x), Some(y)) => match label::cmp_ignore_ascii_case(x, y) {
Ordering::Equal => {}
non_eq => return non_eq,
},
}
}
}
fn eq_segments<'a>(
mut a: impl Iterator<Item = &'a str>,
mut b: impl Iterator<Item = &'a str>,
) -> bool {
loop {
match (a.next(), b.next()) {
(None, None) => return true,
(Some(x), Some(y)) if x.eq_ignore_ascii_case(y) => {}
_ => return false,
}
}
}
fn cmp_domain(a: impl AsRef<str>, b: impl AsRef<str>) -> Ordering {
cmp_segments(dotted_segments(a.as_ref()), dotted_segments(b.as_ref()))
}
impl PartialOrd<Self> for Domain {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Domain {
fn cmp(&self, other: &Self) -> Ordering {
cmp_segments(
self.labels().map(Label::as_str),
other.labels().map(Label::as_str),
)
}
}
impl PartialOrd<str> for Domain {
fn partial_cmp(&self, other: &str) -> Option<Ordering> {
Some(cmp_domain(self, other))
}
}
impl PartialOrd<Domain> for str {
fn partial_cmp(&self, other: &Domain) -> Option<Ordering> {
Some(cmp_domain(self, other))
}
}
impl PartialOrd<&str> for Domain {
fn partial_cmp(&self, other: &&str) -> Option<Ordering> {
Some(cmp_domain(self, other))
}
}
impl PartialOrd<Domain> for &str {
#[inline(always)]
fn partial_cmp(&self, other: &Domain) -> Option<Ordering> {
Some(cmp_domain(self, other))
}
}
impl PartialOrd<String> for Domain {
#[inline(always)]
fn partial_cmp(&self, other: &String) -> Option<Ordering> {
Some(cmp_domain(self, other))
}
}
impl PartialOrd<Domain> for String {
#[inline(always)]
fn partial_cmp(&self, other: &Domain) -> Option<Ordering> {
Some(cmp_domain(self, other))
}
}
fn partial_eq_domain(a: impl AsRef<str>, b: impl AsRef<str>) -> bool {
eq_segments(dotted_segments(a.as_ref()), dotted_segments(b.as_ref()))
}
impl PartialEq<Self> for Domain {
fn eq(&self, other: &Self) -> bool {
eq_segments(
self.labels().map(Label::as_str),
other.labels().map(Label::as_str),
)
}
}
impl Eq for Domain {}
impl PartialEq<str> for Domain {
fn eq(&self, other: &str) -> bool {
partial_eq_domain(self, other)
}
}
impl PartialEq<&str> for Domain {
fn eq(&self, other: &&str) -> bool {
partial_eq_domain(self, other)
}
}
impl PartialEq<Domain> for str {
fn eq(&self, other: &Domain) -> bool {
other == self
}
}
impl PartialEq<Domain> for &str {
#[inline(always)]
fn eq(&self, other: &Domain) -> bool {
partial_eq_domain(self, other)
}
}
impl PartialEq<String> for Domain {
#[inline(always)]
fn eq(&self, other: &String) -> bool {
partial_eq_domain(self, other)
}
}
impl PartialEq<Domain> for String {
#[inline(always)]
fn eq(&self, other: &Domain) -> bool {
partial_eq_domain(self, other)
}
}
use rama_utils::macros::serde_str::impl_serde_str;
impl_serde_str!(as_str Domain);
#[expect(private_bounds)]
pub trait AsDomainRef: seal::AsDomainRefPrivate {
fn as_wildcard_parent(&self) -> Option<Domain> {
self.domain_as_str()
.strip_prefix("*.")
.and_then(|s| s.parse().ok())
}
fn to_domain(&self) -> Domain {
unsafe {
Domain::from_maybe_borrowed_unchecked(Bytes::copy_from_slice(
self.domain_as_str().as_bytes(),
))
}
}
fn to_wildcard(&self) -> Result<Domain, PushError> {
let d = self.to_domain();
if d.is_wildcard() {
Ok(d)
} else {
d.try_as_wildcard()
}
}
fn as_wildcard(&self) -> Option<Domain> {
let s = self.domain_as_str();
let trimmed = s.strip_prefix('.').unwrap_or(s);
trimmed.starts_with("*.").then(|| self.to_domain())
}
}
impl AsDomainRef for &'static str {}
impl AsDomainRef for Domain {}
impl<T: seal::AsDomainRefPrivate> AsDomainRef for &T {}
pub trait IntoDomain: seal::IntoDomainImpl {}
impl IntoDomain for &'static str {}
impl IntoDomain for Domain {}
pub(super) mod seal {
pub(in crate::address) trait AsDomainRefPrivate {
fn domain_as_str(&self) -> &str;
}
impl AsDomainRefPrivate for &'static str {
#[expect(
clippy::panic,
reason = "static-str invariant: matches Domain::from_static panicking style"
)]
fn domain_as_str(&self) -> &str {
if !super::is_valid_name(self.as_bytes()) {
panic!("static str is an invalid domain");
}
self
}
}
impl AsDomainRefPrivate for super::Domain {
fn domain_as_str(&self) -> &str {
self.as_str()
}
}
impl<T: AsDomainRefPrivate> AsDomainRefPrivate for &T {
#[inline(always)]
fn domain_as_str(&self) -> &str {
(**self).domain_as_str()
}
}
pub trait IntoDomainImpl {
fn into_domain(self) -> super::Domain;
}
impl IntoDomainImpl for &'static str {
#[inline(always)]
fn into_domain(self) -> super::Domain {
super::Domain::from_static(self)
}
}
impl IntoDomainImpl for super::Domain {
#[inline]
fn into_domain(self) -> super::Domain {
self
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ahash::{HashMap, HashMapExt as _};
#[test]
fn test_specials() {
assert_eq!(Domain::tld_localhost(), "localhost");
assert_eq!(Domain::tld_private(), "internal");
assert_eq!(Domain::example(), "example.com");
}
#[test]
fn test_is_loopback() {
for s in [
"localhost",
"LOCALHOST",
"LocalHost",
"foo.localhost",
"a.b.localhost",
"localhost.",
".localhost",
] {
let d = Domain::try_from(s.to_owned()).unwrap();
assert!(d.is_loopback(), "{s} should be a loopback name");
assert!(d.view().is_loopback(), "{s} (ref) should be loopback");
}
for s in [
"example.com",
"localhost.example.com",
"mylocalhost",
"localhostx",
"localhost.com",
] {
let d = Domain::try_from(s.to_owned()).unwrap();
assert!(!d.is_loopback(), "{s} should not be a loopback name");
assert!(!d.view().is_loopback(), "{s} (ref) should not be loopback");
}
}
#[test]
fn test_domain_parse_valid() {
for str in [
"example.com",
"_acme-challenge.example.com",
"_acme-challenge_.example.com",
"_acme_challenge_.example.com",
"www.example.com",
"*.com", "*.example.com",
"a-b-c.com",
"a-b-c.example.com",
"a-b-c.example",
"aA1",
".example.com",
"example.com.",
".example.com.",
"rr5---sn-q4fl6n6s.video.com", "127.0.0.1",
] {
let msg = format!("to parse: {str}");
assert_eq!(Domain::try_from(str.to_owned()).expect(msg.as_str()), str);
assert_eq!(
Domain::try_from(str.as_bytes().to_vec()).expect(msg.as_str()),
str
);
}
}
#[test]
fn test_domain_is_wildcard() {
assert!(!Domain::from_static("localhost").is_wildcard());
assert!(!Domain::from_static("example.com").is_wildcard());
assert!(!Domain::from_static("foo.example.com").is_wildcard());
assert!(Domain::from_static("*.com").is_wildcard());
assert!(Domain::from_static("*.example.com").is_wildcard());
assert!(Domain::from_static("*.foo.example.com").is_wildcard());
}
#[test]
fn test_domain_as_wildcard_parent() {
assert!(
Domain::from_static("localhost")
.as_wildcard_parent()
.is_none()
);
assert!(
Domain::from_static("example.com")
.as_wildcard_parent()
.is_none()
);
assert!(
Domain::from_static("foo.example.com")
.as_wildcard_parent()
.is_none()
);
assert_eq!(
Some(Domain::from_static("com")),
Domain::from_static("*.com").as_wildcard_parent()
);
assert_eq!(
Some(Domain::from_static("example.com")),
Domain::from_static("*.example.com").as_wildcard_parent()
);
assert_eq!(
Some(Domain::from_static("foo.example.com")),
Domain::from_static("*.foo.example.com").as_wildcard_parent()
);
}
#[test]
fn test_domain_parse_invalid() {
for str in [
"",
".",
"..",
"-",
"*",
".*",
"*.",
".*.",
".-",
"-.",
".-.",
"-.-.",
"-.-.-",
".-.-",
"2001:db8:3333:4444:5555:6666:7777:8888",
"-example.com",
"foo.*.com",
"*example.com",
"*foo",
"o*o",
"fo*",
"local!host",
"thislabeliswaytoolongforbeingeversomethingwewishtocareabout-example.com",
"example-thislabeliswaytoolongforbeingeversomethingwewishtocareabout.com",
#[cfg(not(feature = "idna"))]
"こんにちは",
#[cfg(not(feature = "idna"))]
"こんにちは.com",
#[cfg(not(feature = "idna"))]
"😀",
"example..com",
"example dot com",
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
] {
assert!(Domain::try_from(str.to_owned()).is_err(), "input = '{str}'");
assert!(
Domain::try_from(str.as_bytes().to_vec()).is_err(),
"input = '{str}'"
);
}
}
#[test]
fn is_parent() {
let test_cases = vec![
("www.example.com", "www.example.com"),
("www.example.com", "www.example.com."),
("www.example.com", ".www.example.com."),
(".www.example.com", "www.example.com"),
(".www.example.com", "www.example.com."),
(".www.example.com.", "www.example.com."),
("www.example.com", "WwW.ExamplE.COM"),
("example.com", "www.example.com"),
("example.com", "m.example.com"),
("example.com", "www.EXAMPLE.com"),
("example.com", "M.example.com"),
];
for (a, b) in test_cases.into_iter() {
let a = Domain::from_static(a);
let b = Domain::from_static(b);
assert!(a.is_parent_of(&b), "({a:?}).is_parent_of({b})");
}
}
#[test]
fn as_wildcard_sub() {
let test_cases = vec![
("example.com", "*.example.com"),
("fp.ramaproxy.org", "*.fp.ramaproxy.org"),
("print.co.uk", "*.print.co.uk"),
];
for (domain_raw, expected_output) in test_cases.into_iter() {
let domain = Domain::from_static(domain_raw);
let msg = format!("{:?}", (domain_raw, expected_output));
let subdomain = domain.try_as_wildcard().expect(&msg);
assert_eq!(expected_output, subdomain);
assert!(subdomain.is_wildcard());
assert_eq!(Some(domain), subdomain.as_wildcard_parent())
}
}
#[test]
fn as_sub_success() {
let test_cases = vec![
("example.com", "www", "www.example.com"),
("fp.ramaproxy.org", "h1", "h1.fp.ramaproxy.org"),
(
"dadadadadadadadadad.llgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.co.uk",
"a",
"a.dadadadadadadadadad.llgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.co.uk",
),
];
for (domain_raw, sub, expected_output) in test_cases.into_iter() {
let domain = Domain::from_static(domain_raw);
let msg = format!("{:?}", (domain_raw, sub, expected_output));
let subdomain = domain.try_as_sub(sub).expect(&msg);
assert_eq!(expected_output, subdomain);
}
}
#[test]
fn as_sub_failure() {
let test_cases = vec![
(
"adadadadadadadadadad.llgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.co.uk",
"a",
),
];
for (domain_raw, sub) in test_cases.into_iter() {
let domain = Domain::from_static(domain_raw);
let msg = format!("{:?}", (domain_raw, sub));
_ = domain.try_as_sub(sub).expect_err(&msg);
}
}
#[test]
fn strip_sub() {
let test_cases = vec![
("www.example.com", "www", Some("example.com")),
("example.com", "www", None),
("www.www.example.com", "www", Some("www.example.com")),
("a.b.example.com", "a.b", Some("example.com")),
("a.b.example.com", "a.x", None),
("example.com", "example.com", None),
("WWW.example.com", "www", Some("example.com")),
("www.example.com", "WWW", Some("example.com")),
];
for (sub_raw, prefix, expected_output) in test_cases.into_iter() {
let sub = Domain::from_static(sub_raw);
let result = sub.strip_sub(prefix);
let expected_result = expected_output.map(Domain::from_static);
assert_eq!(expected_result, result, "sub={sub_raw} prefix={prefix}");
}
}
#[test]
fn is_not_parent() {
let test_cases = vec![
("www.example.com", "www.example.co"),
("www.example.com", "www.ejemplo.com"),
("www.example.com", "www3.example.com"),
("w.example.com", "www.example.com"),
("gel.com", "kegel.com"),
];
for (a, b) in test_cases.into_iter() {
let a = Domain::from_static(a);
let b = Domain::from_static(b);
assert!(!a.is_parent_of(&b), "!({a:?}).is_parent_of({b})");
}
}
#[test]
fn is_equal() {
let test_cases = vec![
("example.com", "example.com"),
("example.com", "EXAMPLE.com"),
(".example.com", ".example.com"),
(".example.com", "example.com"),
("example.com", ".example.com"),
("example.com", "example.com."),
("example.com.", "example.com"),
(".example.com.", "example.com"),
(".example.com", "example.com."),
];
for (a, b) in test_cases.into_iter() {
assert_eq!(Domain::from_static(a), b);
assert_eq!(Domain::from_static(a), b.to_owned());
assert_eq!(Domain::from_static(a), Domain::from_static(b));
assert_eq!(a, Domain::from_static(b));
assert_eq!(a.to_owned(), Domain::from_static(b));
}
}
#[test]
fn is_tld() {
for (expected, input) in [
(true, ".com"),
(true, "com"),
(true, "co.uk"),
(true, ".co.uk"),
(false, "example.com"),
(false, "foo.uk"),
(false, "foo.example.com"),
] {
assert_eq!(
expected,
Domain::from_static(input).is_tld(),
"input: {input}"
)
}
}
#[test]
fn is_sld() {
for (expected, input) in [
(false, "com"),
(false, "co.uk"),
(true, "example.com"),
(true, ".example.com"),
(true, "foo.uk"),
(true, ".foo.uk"),
(false, "foo.example.com"),
] {
assert_eq!(
expected,
Domain::from_static(input).is_sld(),
"input: {input}"
)
}
}
#[test]
fn is_not_equal() {
let test_cases = vec![
("example.com", "localhost"),
("example.com", "example.co"),
("example.com", "examine.com"),
("example.com", "example.com.us"),
("example.com", "www.example.com"),
];
for (a, b) in test_cases.into_iter() {
assert_ne!(Domain::from_static(a), b);
assert_ne!(Domain::from_static(a), b.to_owned());
assert_ne!(Domain::from_static(a), Domain::from_static(b));
assert_ne!(a, Domain::from_static(b));
assert_ne!(a.to_owned(), Domain::from_static(b));
}
}
#[test]
fn cmp() {
let test_cases = vec![
("example.com", "example.com", Ordering::Equal),
("example.com", "EXAMPLE.com", Ordering::Equal),
(".example.com", ".example.com", Ordering::Equal),
(".example.com", "example.com", Ordering::Equal),
("example.com", ".example.com", Ordering::Equal),
("example.com", "localhost", Ordering::Less),
("example.com", "example.com.", Ordering::Equal),
("example.com.", "example.com", Ordering::Equal),
("example.com", "example.co", Ordering::Greater),
("example.com", "examine.com", Ordering::Greater),
("example.com", "example.com.us", Ordering::Less),
("example.com", "www.example.com", Ordering::Less),
];
for (a, b, expected) in test_cases.into_iter() {
assert_eq!(Some(expected), Domain::from_static(a).partial_cmp(&b));
assert_eq!(
Some(expected),
Domain::from_static(a).partial_cmp(&b.to_owned())
);
assert_eq!(
Some(expected),
Domain::from_static(a).partial_cmp(&Domain::from_static(b))
);
assert_eq!(
expected,
Domain::from_static(a).cmp(&Domain::from_static(b))
);
assert_eq!(Some(expected), a.partial_cmp(&Domain::from_static(b)));
assert_eq!(
Some(expected),
a.to_owned().partial_cmp(&Domain::from_static(b))
);
}
}
#[test]
fn test_hash() {
let mut m = HashMap::new();
assert!(!m.contains_key(&Domain::from_static("example.com")));
assert!(!m.contains_key(&Domain::from_static("EXAMPLE.COM")));
assert!(!m.contains_key(&Domain::from_static(".example.com")));
assert!(!m.contains_key(&Domain::from_static(".example.COM")));
m.insert(Domain::from_static("eXaMpLe.COm"), ());
assert!(m.contains_key(&Domain::from_static("example.com")));
assert!(m.contains_key(&Domain::from_static("EXAMPLE.COM")));
assert!(m.contains_key(&Domain::from_static(".example.com")));
assert!(m.contains_key(&Domain::from_static(".example.COM")));
assert!(m.contains_key(&Domain::from_static("example.com.")));
assert!(m.contains_key(&Domain::from_static(".example.com.")));
assert!(!m.contains_key(&Domain::from_static("www.example.com")));
assert!(!m.contains_key(&Domain::from_static("examine.com")));
assert!(!m.contains_key(&Domain::from_static("example.co")));
assert!(!m.contains_key(&Domain::from_static("example.commerce")));
}
#[test]
fn domain_and_domainref_hash_identically() {
use crate::test_hash::hash;
for name in ["example.com", "EXAMPLE.com", "sub.example.com", "x.y"] {
let owned = Domain::from_static(name);
let borrowed = DomainRef::from(&owned);
assert_eq!(hash(&owned), hash(&borrowed), "hash mismatch for {name:?}");
}
}
#[test]
fn parse_error_variant_empty() {
let err = Domain::try_from(String::new()).unwrap_err();
assert_eq!(format!("{err}"), "empty domain");
let err = Domain::try_from(".".to_owned()).unwrap_err();
assert_eq!(format!("{err}"), "empty domain");
}
#[test]
fn parse_error_variant_too_long() {
let too_long = "a".repeat(MAX_NAME_LEN + 1);
let err = Domain::try_from(too_long).unwrap_err();
assert!(format!("{err}").contains("max is 253"), "got: {err}");
}
#[test]
fn regression_domain_fqdn_trailing_dot_length() {
let l63 = "a".repeat(63);
let l62 = "a".repeat(62);
let prefix = format!("{l63}.{l63}.{l62}.{l62}");
assert_eq!(prefix.len(), 253);
Domain::try_from(prefix.clone()).unwrap();
let fqdn = format!("{prefix}.");
assert_eq!(fqdn.len(), 254);
Domain::try_from(fqdn)
.expect("FQDN with trailing dot should be accepted at 254 octets total");
let too_long = format!("{prefix}c");
assert_eq!(too_long.len(), 254);
Domain::try_from(too_long).unwrap_err();
}
#[test]
fn regression_domain_const_validator_matches_public_validator() {
for input in [
".A", ".*.k", ".com", ".com.", "*.x", "A", "com.uk", ] {
Domain::try_from(input)
.unwrap_or_else(|e| panic!("try_from({input:?}) rejected unexpectedly: {e}"));
let _domain = Domain::from_static(input);
}
let uri: crate::uri::Uri = "k://.A/".parse().unwrap();
assert_eq!(uri.host().unwrap().to_str(), ".A");
let uri: crate::uri::Uri = "k://.*.k".parse().unwrap();
assert_eq!(uri.host().unwrap().to_str(), ".*.k");
}
#[test]
fn parse_error_variant_label_at_index() {
let err = Domain::try_from("a..b".to_owned()).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("index 1"), "msg: {msg}");
let src = core::error::Error::source(&err);
assert!(src.is_some(), "label error should be source-chained");
}
#[test]
fn parse_error_variant_bad_wildcard() {
let err = Domain::try_from("foo.*.com".to_owned()).unwrap_err();
assert!(
format!("{err}").contains("wildcard"),
"expected wildcard mention: {err}"
);
let err = Domain::try_from("*".to_owned()).unwrap_err();
assert!(
format!("{err}").contains("wildcard"),
"expected wildcard mention: {err}"
);
}
#[test]
fn parse_error_variant_non_utf8() {
let bytes: Vec<u8> = vec![0xff, b'x', b'.', b'c', b'o', b'm'];
let err = <Domain as TryFrom<Vec<u8>>>::try_from(bytes.clone()).unwrap_err();
assert!(format!("{err}").contains("UTF-8"), "got: {err}");
let err = <Domain as TryFrom<&[u8]>>::try_from(bytes.as_slice()).unwrap_err();
assert!(format!("{err}").contains("UTF-8"), "got: {err}");
}
#[test]
fn parse_error_converts_to_box_error_via_questionmark() {
fn use_questionmark(s: &str) -> Result<Domain, rama_core::error::BoxError> {
let d: Domain = s.parse()?;
Ok(d)
}
use_questionmark("example.com").unwrap();
use_questionmark("").unwrap_err();
}
}