ascii_domain

Domains whose labels are only ASCII.
git clone https://git.philomathiclife.com/repos/ascii_domain
Log | Files | Refs | README

dom.rs (57813B)


      1 extern crate alloc;
      2 #[cfg(test)]
      3 /// Unit tests.
      4 mod tests;
      5 use crate::char_set::{ASCII_HYPHEN_DIGITS_LETTERS, AllowedAscii};
      6 use alloc::{string::String, vec::Vec};
      7 use core::{
      8     borrow::Borrow,
      9     cmp::Ordering,
     10     convert,
     11     error::Error,
     12     fmt::{self, Display, Formatter},
     13     hash::{Hash, Hasher},
     14     iter::FusedIterator,
     15     num::NonZeroU8,
     16     ops::Deref,
     17     str,
     18 };
     19 /// The `AllowedAscii` used by `Rfc1123Domain`.
     20 static RFC_CHARS: &AllowedAscii<[u8; 63]> = &ASCII_HYPHEN_DIGITS_LETTERS;
     21 /// Returned by [`Domain::cmp_by_domain_ordering`].
     22 ///
     23 /// It is more informative than [`Ordering`] in that it
     24 /// distinguishes between a `Domain` that is greater than another `Domain` due to a [`Label`] being greater
     25 /// from a `Domain` that has the same `Label`s as another but simply more.
     26 ///
     27 /// Another way to view this is that [`Self::Shorter`] is "closer" to being [`Self::Equal`] than [`Self::Less`]
     28 /// since the `Domain`s are still part of the same branch in the DNS hierarchy. Ditto for [`Self::Longer`].
     29 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
     30 pub enum DomainOrdering {
     31     /// The `Domain` is less than another since a `Label` was less.
     32     Less,
     33     /// The `Domain` is less than other but only because it had fewer `Label`s.
     34     Shorter,
     35     /// The `Domain` is equal to another.
     36     Equal,
     37     /// The `Domain` is greater than another but only because it had more `Label`s.
     38     Longer,
     39     /// The `Domain` is greater than another since a `Label` was greater.
     40     Greater,
     41 }
     42 impl Display for DomainOrdering {
     43     #[inline]
     44     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
     45         match *self {
     46             Self::Less => f.write_str("less since a label was less"),
     47             Self::Shorter => f.write_str("less since there were fewer labels"),
     48             Self::Equal => f.write_str("equal"),
     49             Self::Longer => f.write_str("greater since there were more labels"),
     50             Self::Greater => f.write_str("greater since a label was greater"),
     51         }
     52     }
     53 }
     54 impl From<DomainOrdering> for Ordering {
     55     #[inline]
     56     fn from(value: DomainOrdering) -> Self {
     57         match value {
     58             DomainOrdering::Less | DomainOrdering::Shorter => Self::Less,
     59             DomainOrdering::Equal => Self::Equal,
     60             DomainOrdering::Longer | DomainOrdering::Greater => Self::Greater,
     61         }
     62     }
     63 }
     64 /// A domain that consists of at least one [`Label`] with each `Label` only containing the ASCII `u8`s in
     65 /// the [`AllowedAscii`] passed to [`Self::try_from_bytes`].
     66 ///
     67 /// The total length of a `Domain` is at most
     68 /// 253 bytes[^note] in length including the `b'.'` separator. The trailing `b'.'`, if one exists, is always
     69 /// ignored.
     70 ///
     71 /// This is more restrictive than what a domain is allowed to be per the
     72 /// [Domain Name System (DNS)](https://www.rfc-editor.org/rfc/rfc2181) since all octets/`u8`s are allowed in a
     73 /// label. Additionally there is no way to represent the root domain.
     74 ///
     75 /// Last, ASCII uppercase letters are treated as lowercase; however for better comparison performance
     76 /// that doesn't lead to intermediate memory allocations, two `Domain`s should consist entirely of the same
     77 /// case.
     78 ///
     79 /// [^note]: It is a common misconception that the max length of a domain is 255, but that is only true for
     80 ///          domains in _wire_ format. In representation format, which `Domain` can be thought of when only visible
     81 ///          ASCII bytes are used, the max length is 253 when the last byte is not `b'.'`; otherwise the max length is
     82 ///          254. This is due to the fact that there is no way to explicitly represent the root label which in wire format
     83 ///          contributes one byte due to each label being preceded by the octet that represents its length.
     84 ///
     85 /// Note this only contains `T`, so this is allocation-free and the same size as `T`.
     86 #[derive(Clone, Copy, Debug)]
     87 pub struct Domain<T> {
     88     /// The domain value. `value.as_ref().len()` is guaranteed to have length between 1 and 253 when the last `u8`
     89     /// is not `b'.'`; otherwise the length is between 2 and 254.
     90     /// Guaranteed to only contain `b'.'` and the ASCII `u8`s in `allowed_ascii`.
     91     value: T,
     92 }
     93 impl<T> Domain<T> {
     94     /// A reference to the contained `T`.
     95     ///
     96     /// # Example
     97     ///
     98     /// ```
     99     /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE};
    100     /// assert!(*Domain::try_from_bytes("example.com.", &ASCII_LOWERCASE).unwrap().as_inner() == "example.com.");
    101     /// ```
    102     #[inline]
    103     pub const fn as_inner(&self) -> &T {
    104         &self.value
    105     }
    106     /// Same as [`Self::as_inner`] except `self` is consumed.
    107     ///
    108     /// # Example
    109     ///
    110     /// ```
    111     /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE};
    112     /// assert!(Domain::try_from_bytes("example.com.", &ASCII_LOWERCASE).unwrap().into_inner() == "example.com.");
    113     /// ```
    114     #[inline]
    115     pub fn into_inner(self) -> T {
    116         self.value
    117     }
    118 }
    119 impl<T: AsRef<[u8]>> Domain<T> {
    120     /// Returns `true` iff the domain contains a trailing `b'.'`.
    121     ///
    122     /// # Example
    123     ///
    124     /// ```
    125     /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE};
    126     /// assert!(Domain::try_from_bytes("example.com.", &ASCII_LOWERCASE).unwrap().contains_trailing_dot());
    127     /// ```
    128     #[expect(
    129         clippy::arithmetic_side_effects,
    130         clippy::indexing_slicing,
    131         reason = "comments explain their correctness"
    132     )]
    133     #[inline]
    134     pub fn contains_trailing_dot(&self) -> bool {
    135         let bytes = self.value.as_ref();
    136         // This won't underflow or `panic` since `Domain`s are not empty.
    137         bytes[bytes.len() - 1] == b'.'
    138     }
    139     /// The domain without a trailing `b'.'` if there was one.
    140     ///
    141     /// # Example
    142     ///
    143     /// ```
    144     /// use ascii_domain::{dom::Domain, char_set::ASCII_LETTERS};
    145     /// assert!(Domain::try_from_bytes("Example.com.", &ASCII_LETTERS).unwrap().as_str() == "Example.com");
    146     /// ```
    147     #[inline]
    148     pub fn as_str(&self) -> &str {
    149         <&str>::from(Domain::<&str>::from(Domain::<&[u8]>::from(self)))
    150     }
    151     /// The domain without a trailing `b'.'` if there was one.
    152     ///
    153     /// # Example
    154     ///
    155     /// ```
    156     /// use ascii_domain::{dom::Domain, char_set::ASCII_LETTERS};
    157     /// assert!(Domain::try_from_bytes("Example.com", &ASCII_LETTERS).unwrap().as_bytes() == b"Example.com");
    158     /// ```
    159     #[inline]
    160     pub fn as_bytes(&self) -> &[u8] {
    161         <&[u8]>::from(Domain::<&[u8]>::from(self))
    162     }
    163     /// The length of the `Domain`. This does _not_ include the trailing `b'.'` if there was one.
    164     ///
    165     /// # Example
    166     ///
    167     /// ```
    168     /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE};
    169     /// assert!(Domain::try_from_bytes("example.com.", &ASCII_LOWERCASE).unwrap().len().get() == 11);
    170     /// ```
    171     #[expect(
    172         unsafe_code,
    173         reason = "we enforce nonzero lengths, so NonZeroU8::new_unchecked is fine"
    174     )]
    175     #[expect(
    176         clippy::arithmetic_side_effects,
    177         clippy::as_conversions,
    178         clippy::cast_possible_truncation,
    179         reason = "comments justify their correctness"
    180     )]
    181     #[inline]
    182     pub fn len(&self) -> NonZeroU8 {
    183         // No fear of underflow since the length of `value` is at least 1 _not including_ the
    184         // trailing `b'.'` if there was one.
    185         // `true as usize` is guaranteed to be 1 and `false as usize` is guaranteed to be 0.
    186         // No fear of truncation either since the length is guaranteed to be less than 255.
    187         // `Domain` is immutable ensuring such invariants are kept.
    188         let len = (self.value.as_ref().len() - usize::from(self.contains_trailing_dot())) as u8;
    189         // SAFETY:
    190         // The only way to construct a `Domain` is via `try_from_bytes` which ensures `len` is
    191         // is at least 1.
    192         unsafe { NonZeroU8::new_unchecked(len) }
    193     }
    194     /// Function that transforms `v` into a `Domain` by only allowing [`Label`]s to contain the ASCII `u8`s in
    195     /// `allowed_ascii`. A trailing `b'.'` is ignored.
    196     ///
    197     /// Note that while ASCII uppercase is treated as ASCII lowercase, `allowed_ascii` MUST still contain
    198     /// each ASCII `u8` (e.g., if `!allowed_ascii.contains(b'A')`, then `b'A'` is not allowed even if
    199     /// `allowed_ascii.contains(b'a')`).
    200     ///
    201     /// # Examples
    202     ///
    203     /// ```
    204     /// use ascii_domain::{dom::{Domain, DomainErr}, char_set::ASCII_LOWERCASE};
    205     /// assert!(Domain::try_from_bytes("example.com", &ASCII_LOWERCASE).is_ok());
    206     /// assert!(Domain::try_from_bytes("exam2ple.com", &ASCII_LOWERCASE).map_or_else(|err| err == DomainErr::InvalidByte(b'2'), |_| false));
    207     /// ```
    208     ///
    209     /// # Errors
    210     ///
    211     /// Returns [`DomainErr`] iff `v.as_ref()` is an invalid `Domain`.
    212     #[expect(
    213         clippy::arithmetic_side_effects,
    214         reason = "comment justifies its correctness"
    215     )]
    216     #[inline]
    217     pub fn try_from_bytes<T2: AsRef<[u8]>>(
    218         v: T,
    219         allowed_ascii: &AllowedAscii<T2>,
    220     ) -> Result<Self, DomainErr> {
    221         let val = v.as_ref();
    222         let value = val
    223             .split_last()
    224             .ok_or(DomainErr::Empty)
    225             .and_then(|(lst, rem)| {
    226                 if *lst == b'.' {
    227                     rem.split_last()
    228                         .ok_or(DomainErr::RootDomain)
    229                         .and_then(|(lst_2, _)| {
    230                             if *lst_2 == b'.' {
    231                                 Err(DomainErr::EmptyLabel)
    232                             } else {
    233                                 Ok(rem)
    234                             }
    235                         })
    236                 } else {
    237                     Ok(val)
    238                 }
    239             })?;
    240         if value.len() > 253 {
    241             Err(DomainErr::LenExceeds253(value.len()))
    242         } else {
    243             value
    244                 .iter()
    245                 .try_fold(0, |label_len, byt| {
    246                     let b = *byt;
    247                     if b == b'.' {
    248                         NonZeroU8::new(label_len).map_or(Err(DomainErr::EmptyLabel), |_| Ok(0))
    249                     } else if !allowed_ascii.contains(b) {
    250                         Err(DomainErr::InvalidByte(b))
    251                     } else if label_len == 63 {
    252                         Err(DomainErr::LabelLenExceeds63)
    253                     } else {
    254                         // This is less than 63 due to the above check, so this won't overflow;
    255                         Ok(label_len + 1)
    256                     }
    257                 })
    258                 .map(|_| Self { value: v })
    259         }
    260     }
    261     /// Returns an [`Iterator`] of [`Label`]s without consuming the `Domain`.
    262     /// # Example
    263     ///
    264     /// ```
    265     /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE};
    266     /// assert!(Domain::try_from_bytes("example.com", &ASCII_LOWERCASE).unwrap().into_iter().next().unwrap().as_str() == "com");
    267     /// ```
    268     #[inline]
    269     pub fn iter(&self) -> LabelIter<'_> {
    270         LabelIter {
    271             domain: self.as_bytes(),
    272         }
    273     }
    274     /// Returns `true` iff `self` and `right` are part of the same branch in the DNS hierarchy.
    275     ///
    276     /// For example `www.example.com` and `example.com` are in the `same_branch`, but `example.com` and
    277     /// `foo.com` are not.
    278     ///
    279     /// Note that trailing `b'.'`s are ignored and ASCII uppercase and lowercase are treated the same.
    280     ///
    281     /// # Examples
    282     ///
    283     /// ```
    284     /// use ascii_domain::{dom::Domain, char_set::{ASCII_LETTERS, ASCII_LOWERCASE}};
    285     /// let dom1 = Domain::try_from_bytes("Example.com", &ASCII_LETTERS).unwrap();
    286     /// let dom2 = Domain::try_from_bytes("www.example.com", &ASCII_LOWERCASE).unwrap();
    287     /// assert!(dom1.same_branch(&dom2));
    288     /// let dom3 = Domain::try_from_bytes("foo.com", &ASCII_LOWERCASE).unwrap();
    289     /// assert!(!dom1.same_branch(&dom3));
    290     /// ```
    291     #[inline]
    292     pub fn same_branch<T2: AsRef<[u8]>>(&self, right: &Domain<T2>) -> bool {
    293         // Faster to check the values as bytes and not iterate each `Label`.
    294         if self == right {
    295             true
    296         } else {
    297             self.iter()
    298                 .zip(right)
    299                 .try_fold(
    300                     (),
    301                     |(), (label, label2)| if label == label2 { Ok(()) } else { Err(()) },
    302                 )
    303                 .is_ok_and(|()| true)
    304         }
    305     }
    306     /// Same as [`Self::cmp_doms`] except returns [`DomainOrdering::Longer`] iff `self > right` due solely
    307     /// to having more [`Label`]s and [`DomainOrdering::Shorter`] iff `self < right` due solely to having
    308     /// fewer `Label`s.
    309     ///
    310     /// For example `example.com` < `www.example.com` and `bar.com` < `www.example.com`; but with this function,
    311     /// `example.com` is [`DomainOrdering::Shorter`] than `www.example.com` and `www.example.com` is
    312     /// [`DomainOrdering::Longer`] than `example.com`; while `bar.com` is [`DomainOrdering::Less`] than
    313     /// `www.example.com` and `www.example.com` is [`DomainOrdering::Greater`] than `bar.com`.
    314     ///
    315     /// In other words `DomainOrdering::Shorter` implies `Ordering::Less` and `DomainOrdering::Longer` implies
    316     /// `Ordering::Greater` with additional information pertaining to the quantity of `Label`s.
    317     ///
    318     /// # Examples
    319     ///
    320     /// ```
    321     /// use ascii_domain::{dom::{Domain, DomainOrdering}, char_set::{ASCII_LETTERS, ASCII_LOWERCASE}};
    322     /// let dom1 = Domain::try_from_bytes("Example.com", &ASCII_LETTERS).unwrap();
    323     /// assert!(matches!(dom1.cmp_by_domain_ordering(&dom1), DomainOrdering::Equal));
    324     /// let dom2 = Domain::try_from_bytes("www.example.com", &ASCII_LOWERCASE).unwrap();
    325     /// assert!(matches!(dom1.cmp_by_domain_ordering(&dom2), DomainOrdering::Shorter));
    326     /// assert!(matches!(dom2.cmp_by_domain_ordering(&dom1), DomainOrdering::Longer));
    327     /// let dom3 = Domain::try_from_bytes("foo.com", &ASCII_LOWERCASE).unwrap();
    328     /// assert!(matches!(dom1.cmp_by_domain_ordering(&dom3), DomainOrdering::Less));
    329     /// assert!(matches!(dom3.cmp_by_domain_ordering(&dom1), DomainOrdering::Greater));
    330     /// ```
    331     #[inline]
    332     pub fn cmp_by_domain_ordering<T2: AsRef<[u8]>>(&self, right: &Domain<T2>) -> DomainOrdering {
    333         // Faster to compare the entire value when we can instead each `Label`.
    334         if self == right {
    335             DomainOrdering::Equal
    336         } else {
    337             let mut right_iter = right.iter();
    338             self.iter()
    339                 .try_fold(false, |_, label| {
    340                     right_iter
    341                         .next()
    342                         .map_or(Ok(true), |label2| match label.cmp(&label2) {
    343                             Ordering::Less => Err(DomainOrdering::Less),
    344                             Ordering::Equal => Ok(false),
    345                             Ordering::Greater => Err(DomainOrdering::Greater),
    346                         })
    347                 })
    348                 .map_or_else(convert::identity, |flag| {
    349                     // We iterate `self` before `right`, so `flag` is `true` iff `right`
    350                     // has fewer `Label`s than `self`.
    351                     if flag {
    352                         DomainOrdering::Longer
    353                     } else {
    354                         // `self` has as many or fewer `Label`s than `right`; however if it had as many
    355                         // `Label`s as `right`, then all `Label`s are the same which is impossible since
    356                         // we already checked if `self == right`.
    357                         DomainOrdering::Shorter
    358                     }
    359                 })
    360         }
    361     }
    362     /// The total order that is defined follows the following hierarchy:
    363     /// 1. Pairwise comparisons of each [`Label`] starting from the TLDs.
    364     /// 2. If 1. evaluates as not equivalent, then return the result.
    365     /// 3. Return the comparison of `Label` counts.
    366     ///
    367     /// For example, `com` < `example.com` < `net` < `example.net`.
    368     ///
    369     /// This is the same as the [canonical DNS name order](https://datatracker.ietf.org/doc/html/rfc4034#section-6.1).
    370     /// ASCII uppercase is treated as ASCII lowercase and trailing `b'.'`s are ignored.
    371     /// The [`AllowedAscii`]s in the `Domain`s are ignored.
    372     ///
    373     /// # Examples
    374     ///
    375     /// ```
    376     /// use core::cmp::Ordering;
    377     /// use ascii_domain::{dom::Domain, char_set::{ASCII_LETTERS, ASCII_LOWERCASE}};
    378     /// let dom1 = Domain::try_from_bytes("Example.com", &ASCII_LETTERS).unwrap();
    379     /// assert!(matches!(dom1.cmp_doms(&dom1), Ordering::Equal));
    380     /// let dom2 = Domain::try_from_bytes("www.example.com", &ASCII_LOWERCASE).unwrap();
    381     /// assert!(matches!(dom1.cmp_doms(&dom2), Ordering::Less));
    382     /// assert!(matches!(dom2.cmp_doms(&dom1), Ordering::Greater));
    383     /// let dom3 = Domain::try_from_bytes("foo.com", &ASCII_LOWERCASE).unwrap();
    384     /// assert!(matches!(dom1.cmp_doms(&dom3), Ordering::Less));
    385     /// assert!(matches!(dom3.cmp_doms(&dom1), Ordering::Greater));
    386     /// ```
    387     #[inline]
    388     pub fn cmp_doms<T2: AsRef<[u8]>>(&self, right: &Domain<T2>) -> Ordering {
    389         self.cmp_by_domain_ordering(right).into()
    390     }
    391     /// Returns the first `Label`.
    392     ///
    393     /// # Example
    394     ///
    395     /// ```
    396     /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE};
    397     /// assert!(Domain::try_from_bytes("example.com", &ASCII_LOWERCASE).unwrap().first_label().as_str() == "example");
    398     /// ```
    399     #[expect(clippy::unreachable, reason = "bug in code, so we want to crash")]
    400     #[inline]
    401     pub fn first_label(&self) -> Label<'_> {
    402         self.iter()
    403             .next_back()
    404             .unwrap_or_else(|| unreachable!("there is a bug in Domain::try_from_bytes"))
    405     }
    406     /// Returns the last `Label` (i.e., the TLD).
    407     ///
    408     /// # Example
    409     ///
    410     /// ```
    411     /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE};
    412     /// assert!(Domain::try_from_bytes("example.com", &ASCII_LOWERCASE).unwrap().tld().as_str() == "com");
    413     /// ```
    414     #[expect(clippy::unreachable, reason = "bug in code, so we want to crash")]
    415     #[inline]
    416     pub fn tld(&self) -> Label<'_> {
    417         self.iter()
    418             .next()
    419             .unwrap_or_else(|| unreachable!("there is a bug in Domain::try_from_bytes"))
    420     }
    421 }
    422 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<Domain<T>> for Domain<T2> {
    423     /// Ignores the provided [`AllowedAscii`] and simply compares the two `Domain`s as [`Label`]s
    424     /// of bytes. Note uppercase ASCII is treated as lowercase ASCII and trailing `b'.'`s are ignored.
    425     #[inline]
    426     fn eq(&self, other: &Domain<T>) -> bool {
    427         self.as_bytes().eq_ignore_ascii_case(other.as_bytes())
    428     }
    429 }
    430 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<&Domain<T>> for Domain<T2> {
    431     #[inline]
    432     fn eq(&self, other: &&Domain<T>) -> bool {
    433         *self == **other
    434     }
    435 }
    436 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<Domain<T>> for &Domain<T2> {
    437     #[inline]
    438     fn eq(&self, other: &Domain<T>) -> bool {
    439         **self == *other
    440     }
    441 }
    442 impl<T: AsRef<[u8]>> Eq for Domain<T> {}
    443 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialOrd<Domain<T>> for Domain<T2> {
    444     /// Consult [`Self::cmp_doms`].
    445     #[inline]
    446     fn partial_cmp(&self, other: &Domain<T>) -> Option<Ordering> {
    447         Some(self.cmp_doms(other))
    448     }
    449 }
    450 impl<T: AsRef<[u8]>> Ord for Domain<T> {
    451     /// Consult [`Self::cmp_doms`].
    452     #[inline]
    453     fn cmp(&self, other: &Self) -> Ordering {
    454         self.cmp_doms(other)
    455     }
    456 }
    457 impl<T: AsRef<[u8]>> Hash for Domain<T> {
    458     #[inline]
    459     fn hash<H: Hasher>(&self, state: &mut H) {
    460         self.as_bytes().to_ascii_lowercase().hash(state);
    461     }
    462 }
    463 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> TryFrom<(T, &AllowedAscii<T2>)> for Domain<T> {
    464     type Error = DomainErr;
    465     #[inline]
    466     fn try_from(value: (T, &AllowedAscii<T2>)) -> Result<Self, Self::Error> {
    467         Self::try_from_bytes(value.0, value.1)
    468     }
    469 }
    470 impl<T: AsRef<[u8]>> Display for Domain<T> {
    471     #[inline]
    472     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    473         f.write_str(self)
    474     }
    475 }
    476 impl<T: AsRef<[u8]>> AsRef<str> for Domain<T> {
    477     #[inline]
    478     fn as_ref(&self) -> &str {
    479         self.as_str()
    480     }
    481 }
    482 impl<T: AsRef<[u8]>> AsRef<[u8]> for Domain<T> {
    483     #[inline]
    484     fn as_ref(&self) -> &[u8] {
    485         self.as_bytes()
    486     }
    487 }
    488 impl<T: AsRef<[u8]>> Deref for Domain<T> {
    489     type Target = str;
    490     #[inline]
    491     fn deref(&self) -> &Self::Target {
    492         self.as_str()
    493     }
    494 }
    495 impl From<Domain<Vec<u8>>> for Domain<String> {
    496     #[expect(
    497         unsafe_code,
    498         reason = "we enforce ASCII, so String::from_utf8_unchecked is fine"
    499     )]
    500     #[inline]
    501     fn from(value: Domain<Vec<u8>>) -> Self {
    502         // SAFETY:
    503         // We only allow ASCII, so this is fine.
    504         let val = unsafe { String::from_utf8_unchecked(value.value) };
    505         Self { value: val }
    506     }
    507 }
    508 impl<'a: 'b, 'b, T: AsRef<[u8]>> From<&'a Domain<T>> for Domain<&'b [u8]> {
    509     #[inline]
    510     fn from(value: &'a Domain<T>) -> Self {
    511         Self {
    512             value: value.value.as_ref(),
    513         }
    514     }
    515 }
    516 impl<'a: 'b, 'b, T: AsRef<str>> From<&'a Domain<T>> for Domain<&'b str> {
    517     #[inline]
    518     fn from(value: &'a Domain<T>) -> Self {
    519         Self {
    520             value: value.value.as_ref(),
    521         }
    522     }
    523 }
    524 impl From<Domain<String>> for Domain<Vec<u8>> {
    525     #[inline]
    526     fn from(value: Domain<String>) -> Self {
    527         Self {
    528             value: value.value.into_bytes(),
    529         }
    530     }
    531 }
    532 impl<'a: 'b, 'b> From<Domain<&'a [u8]>> for Domain<&'b str> {
    533     #[expect(
    534         unsafe_code,
    535         reason = "we enforce ASCII, so str::from_utf8_unchecked is fine"
    536     )]
    537     #[inline]
    538     fn from(value: Domain<&'a [u8]>) -> Self {
    539         // SAFETY:
    540         // We only allow ASCII, so this is fine.
    541         let val = unsafe { str::from_utf8_unchecked(value.value) };
    542         Self { value: val }
    543     }
    544 }
    545 impl<'a: 'b, 'b> From<Domain<&'a str>> for Domain<&'b [u8]> {
    546     #[inline]
    547     fn from(value: Domain<&'a str>) -> Self {
    548         Self {
    549             value: value.value.as_bytes(),
    550         }
    551     }
    552 }
    553 impl From<Domain<Self>> for String {
    554     /// Returns the contained `String` _without_ a trailing `'.'` if there was one.
    555     ///
    556     /// # Example
    557     ///
    558     /// ```
    559     /// use ascii_domain::{dom::Domain, char_set::ASCII_LETTERS};
    560     /// assert!(String::from(Domain::try_from_bytes(String::from("Example.com."), &ASCII_LETTERS).unwrap()).as_str() == "Example.com");
    561     /// ```
    562     #[inline]
    563     fn from(value: Domain<Self>) -> Self {
    564         if value.contains_trailing_dot() {
    565             let mut val = value.value;
    566             _ = val.pop();
    567             val
    568         } else {
    569             value.value
    570         }
    571     }
    572 }
    573 impl<'a: 'b, 'b> From<Domain<&'a str>> for &'b str {
    574     /// Returns the contained `str` _without_ a trailing `'.'` if there was one.
    575     ///
    576     /// # Example
    577     ///
    578     /// ```
    579     /// use ascii_domain::{dom::Domain, char_set::ASCII_LETTERS};
    580     /// assert!(<&str>::from(Domain::try_from_bytes("Example.com.", &ASCII_LETTERS).unwrap()) == "Example.com");
    581     /// ```
    582     #[expect(
    583         unsafe_code,
    584         reason = "we enforce ASCII, so str::from_utf8_unchecked is fine"
    585     )]
    586     #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")]
    587     #[inline]
    588     fn from(value: Domain<&'a str>) -> Self {
    589         // Indexing won't `panic` since `value.len()` is at most as long as `value.value`.
    590         let utf8 = &value.value.as_bytes()[..value.len().get().into()];
    591         // SAFETY:
    592         // Only ASCII is allowed, so this is fine.
    593         unsafe { str::from_utf8_unchecked(utf8) }
    594     }
    595 }
    596 impl From<Domain<Self>> for Vec<u8> {
    597     /// Returns the contained `Vec` _without_ a trailing `b'.'` if there was one.
    598     ///
    599     /// # Example
    600     ///
    601     /// ```
    602     /// use ascii_domain::{dom::Domain, char_set::ASCII_LETTERS};
    603     /// assert!(Vec::from(Domain::try_from_bytes(vec![b'F', b'o', b'o', b'.', b'c', b'o', b'm'], &ASCII_LETTERS).unwrap()).as_slice() == b"Foo.com");
    604     /// ```
    605     #[inline]
    606     fn from(value: Domain<Self>) -> Self {
    607         if value.contains_trailing_dot() {
    608             let mut val = value.value;
    609             _ = val.pop();
    610             val
    611         } else {
    612             value.value
    613         }
    614     }
    615 }
    616 impl<'a: 'b, 'b> From<Domain<&'a [u8]>> for &'b [u8] {
    617     /// Returns the contained slice _without_ a trailing `b'.'` if there was one.
    618     ///
    619     /// # Example
    620     ///
    621     /// ```
    622     /// use ascii_domain::{dom::Domain, char_set::ASCII_LETTERS};
    623     /// assert!(<&[u8]>::from(Domain::try_from_bytes(b"Example.com.".as_slice(), &ASCII_LETTERS).unwrap()) == b"Example.com");
    624     /// ```
    625     #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")]
    626     #[inline]
    627     fn from(value: Domain<&'a [u8]>) -> Self {
    628         // Indexing won't `panic` since `value.len()` is at most as long as `value.value`.
    629         &value.value[..value.len().get().into()]
    630     }
    631 }
    632 /// Error returned from [`Domain::try_from_bytes`].
    633 #[expect(variant_size_differences, reason = "usize is fine in size")]
    634 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    635 pub enum DomainErr {
    636     /// The domain was empty.
    637     Empty,
    638     /// The domain was the root domain that is to say it was the domain that only contained the root
    639     /// zone (i.e., `b'.'`).
    640     RootDomain,
    641     /// The length of the domain was greater than 253 not counting a terminating `b'.'` if there was one.
    642     LenExceeds253(usize),
    643     /// The domain contained at least one empty label.
    644     EmptyLabel,
    645     /// The domain contained at least one label whose length exceeded 63.
    646     LabelLenExceeds63,
    647     /// The domain contained an invalid byte value.
    648     InvalidByte(u8),
    649 }
    650 impl Display for DomainErr {
    651     #[inline]
    652     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    653         match *self {
    654             Self::Empty => f.write_str("domain is empty"),
    655             Self::RootDomain => f.write_str("domain is the root domain"),
    656             Self::LenExceeds253(len) => write!(
    657                 f,
    658                 "domain has length {len} which is greater than the max length of 253"
    659             ),
    660             Self::EmptyLabel => f.write_str("domain has an empty label"),
    661             Self::LabelLenExceeds63 => {
    662                 f.write_str("domain has a label that exceeds the max length of 63")
    663             }
    664             Self::InvalidByte(byt) => {
    665                 write!(f, "domain has a label with the invalid byte value {byt}")
    666             }
    667         }
    668     }
    669 }
    670 impl Error for DomainErr {}
    671 /// A label of a [`Domain`]. The total length of a `Label` is inclusively between 1 and 63.
    672 #[derive(Clone, Copy, Debug)]
    673 pub struct Label<'a> {
    674     /// The label value.
    675     value: &'a str,
    676 }
    677 impl<'a> Label<'a> {
    678     /// The label.
    679     ///
    680     /// # Example
    681     ///
    682     /// ```
    683     /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE};
    684     /// assert!(Domain::try_from_bytes("example.com", &ASCII_LOWERCASE).unwrap().into_iter().next().map_or(false, |label| label.as_str() == "com"));
    685     /// ```
    686     #[inline]
    687     #[must_use]
    688     pub const fn as_str(self) -> &'a str {
    689         self.value
    690     }
    691     /// Returns `true` iff the label only contains ASCII letters.
    692     ///
    693     /// # Example
    694     ///
    695     /// ```
    696     /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE};
    697     /// assert!(Domain::try_from_bytes("example.com", &ASCII_LOWERCASE).unwrap().into_iter().next().map_or(false, |label| label.is_alphabetic()));
    698     /// ```
    699     #[inline]
    700     #[must_use]
    701     pub fn is_alphabetic(self) -> bool {
    702         self.value
    703             .as_bytes()
    704             .iter()
    705             .try_fold((), |(), byt| {
    706                 if byt.is_ascii_alphabetic() {
    707                     Ok(())
    708                 } else {
    709                     Err(())
    710                 }
    711             })
    712             .is_ok()
    713     }
    714     /// Returns `true` iff the label only contains ASCII digits.
    715     ///
    716     /// # Example
    717     ///
    718     /// ```
    719     /// use ascii_domain::{dom::Domain, char_set::ASCII_DIGITS_LOWERCASE};
    720     /// assert!(Domain::try_from_bytes("example.123", &ASCII_DIGITS_LOWERCASE).unwrap().into_iter().next().map_or(false, |label| label.is_digits()));
    721     /// ```
    722     #[inline]
    723     #[must_use]
    724     pub fn is_digits(self) -> bool {
    725         self.value
    726             .as_bytes()
    727             .iter()
    728             .try_fold((), |(), byt| {
    729                 if byt.is_ascii_digit() {
    730                     Ok(())
    731                 } else {
    732                     Err(())
    733                 }
    734             })
    735             .is_ok()
    736     }
    737     /// Returns `true` iff the label only contains ASCII digits or letters.
    738     ///
    739     /// # Example
    740     ///
    741     /// ```
    742     /// use ascii_domain::{dom::Domain, char_set::ASCII_DIGITS_LOWERCASE};
    743     /// assert!(Domain::try_from_bytes("example.1com", &ASCII_DIGITS_LOWERCASE).unwrap().into_iter().next().map_or(false, |label| label.is_alphanumeric()));
    744     /// ```
    745     #[inline]
    746     #[must_use]
    747     pub fn is_alphanumeric(self) -> bool {
    748         self.value
    749             .as_bytes()
    750             .iter()
    751             .try_fold((), |(), byt| {
    752                 if byt.is_ascii_alphanumeric() {
    753                     Ok(())
    754                 } else {
    755                     Err(())
    756                 }
    757             })
    758             .is_ok()
    759     }
    760     /// Returns `true` iff the label only contains ASCII hyphen, digits, or letters.
    761     ///
    762     /// # Example
    763     ///
    764     /// ```
    765     /// use ascii_domain::{dom::Domain, char_set::ASCII_HYPHEN_DIGITS_LOWERCASE};
    766     /// assert!(Domain::try_from_bytes("example.1-com", &ASCII_HYPHEN_DIGITS_LOWERCASE).unwrap().into_iter().next().map_or(false, |label| label.is_hyphen_or_alphanumeric()));
    767     /// ```
    768     #[inline]
    769     #[must_use]
    770     pub fn is_hyphen_or_alphanumeric(self) -> bool {
    771         self.value
    772             .as_bytes()
    773             .iter()
    774             .try_fold((), |(), byt| {
    775                 if *byt == b'-' || byt.is_ascii_alphanumeric() {
    776                     Ok(())
    777                 } else {
    778                     Err(())
    779                 }
    780             })
    781             .is_ok()
    782     }
    783     /// The length of the `Label`. This is inclusively between 1 and 63.
    784     ///
    785     /// # Example
    786     ///
    787     /// ```
    788     /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE};
    789     /// assert!(Domain::try_from_bytes("example.com.", &ASCII_LOWERCASE).unwrap().into_iter().next().map_or(false, |label| label.len().get() == 3));
    790     /// ```
    791     #[expect(
    792         unsafe_code,
    793         reason = "we enforce label lengths, so NonZeroU8::new_unchecked is fine"
    794     )]
    795     #[expect(
    796         clippy::as_conversions,
    797         clippy::cast_possible_truncation,
    798         reason = "comments justify their correctness"
    799     )]
    800     #[inline]
    801     #[must_use]
    802     pub const fn len(self) -> NonZeroU8 {
    803         // The max length of a `Label` is 63.
    804         let len = self.value.len() as u8;
    805         // SAFETY:
    806         // `Label`s are never empty.
    807         unsafe { NonZeroU8::new_unchecked(len) }
    808     }
    809 }
    810 impl PartialEq<Label<'_>> for Label<'_> {
    811     #[inline]
    812     fn eq(&self, other: &Label<'_>) -> bool {
    813         self.value.eq_ignore_ascii_case(other.value)
    814     }
    815 }
    816 impl PartialEq<&Label<'_>> for Label<'_> {
    817     #[inline]
    818     fn eq(&self, other: &&Label<'_>) -> bool {
    819         *self == **other
    820     }
    821 }
    822 impl PartialEq<Label<'_>> for &Label<'_> {
    823     #[inline]
    824     fn eq(&self, other: &Label<'_>) -> bool {
    825         **self == *other
    826     }
    827 }
    828 impl Eq for Label<'_> {}
    829 impl PartialOrd<Label<'_>> for Label<'_> {
    830     #[inline]
    831     fn partial_cmp(&self, other: &Label<'_>) -> Option<Ordering> {
    832         Some(self.cmp(other))
    833     }
    834 }
    835 impl Ord for Label<'_> {
    836     #[inline]
    837     fn cmp(&self, other: &Self) -> Ordering {
    838         self.value
    839             .to_ascii_lowercase()
    840             .cmp(&other.value.to_ascii_lowercase())
    841     }
    842 }
    843 impl Hash for Label<'_> {
    844     #[inline]
    845     fn hash<H: Hasher>(&self, state: &mut H) {
    846         self.value.to_ascii_lowercase().hash(state);
    847     }
    848 }
    849 impl Display for Label<'_> {
    850     #[inline]
    851     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    852         f.write_str(self.value)
    853     }
    854 }
    855 impl<'a> AsRef<[u8]> for Label<'a> {
    856     #[inline]
    857     fn as_ref(&self) -> &'a [u8] {
    858         self.value.as_bytes()
    859     }
    860 }
    861 impl<'a> AsRef<str> for Label<'a> {
    862     #[inline]
    863     fn as_ref(&self) -> &'a str {
    864         self.value
    865     }
    866 }
    867 impl<'a> Deref for Label<'a> {
    868     type Target = str;
    869     #[inline]
    870     fn deref(&self) -> &'a Self::Target {
    871         self.value
    872     }
    873 }
    874 /// [`Iterator`] that iterates [`Label`]s from a [`Domain`] or [`Rfc1123Domain`] starting from the TLD down.
    875 ///
    876 /// This iterates `Label`s on demand; so if repeated iteration is desired, it may be better to collect the `Label`s
    877 /// in a collection (e.g., [`Vec`]) than create the iterator again. This is also why [`ExactSizeIterator`] is not
    878 /// implemented.
    879 #[derive(Clone, Debug)]
    880 pub struct LabelIter<'a> {
    881     /// Domain as ASCII.
    882     domain: &'a [u8],
    883 }
    884 impl<'a> Iterator for LabelIter<'a> {
    885     type Item = Label<'a>;
    886     #[expect(
    887         unsafe_code,
    888         reason = "we only allow ASCII, so str::from_utf8_unchecked is fine"
    889     )]
    890     #[expect(
    891         clippy::arithmetic_side_effects,
    892         clippy::indexing_slicing,
    893         reason = "comments justify their correctness"
    894     )]
    895     #[inline]
    896     fn next(&mut self) -> Option<Self::Item> {
    897         (!self.domain.is_empty()).then(|| {
    898             self.domain
    899                 .iter()
    900                 .rev()
    901                 .try_fold(1, |count, byt| {
    902                     if *byt == b'.' {
    903                         let len = self.domain.len();
    904                         // `count` < `len` since there is at least one more `u8` before `b'.'`.
    905                         let idx = len - count;
    906                         // `idx + 1` < `len` since `count` is > 1 since `Label`s are never empty.
    907                         let ascii = &self.domain[idx + 1..len];
    908                         // SAFETY:
    909                         // We only allow ASCII, so this is safe.
    910                         let value = unsafe { str::from_utf8_unchecked(ascii) };
    911                         self.domain = &self.domain[..idx];
    912                         Err(Label { value })
    913                     } else {
    914                         Ok(count + 1)
    915                     }
    916                 })
    917                 .map_or_else(convert::identity, |_| {
    918                     // SAFETY:
    919                     // We only allow ASCII, so this is safe.
    920                     let value = unsafe { str::from_utf8_unchecked(self.domain) };
    921                     self.domain = &[];
    922                     Label { value }
    923                 })
    924         })
    925     }
    926     #[inline]
    927     fn last(mut self) -> Option<Self::Item>
    928     where
    929         Self: Sized,
    930     {
    931         self.next_back()
    932     }
    933     #[inline]
    934     fn size_hint(&self) -> (usize, Option<usize>) {
    935         if self.domain.is_empty() {
    936             (0, Some(0))
    937         } else {
    938             // The max size of a `Label` is 63; and all but the last have a `b'.'` that follow it.
    939             // This means the fewest `Label`s possible is the floor of the length divided by 64 with
    940             // the added requirement that it's at least one since we know the domain is not empty.
    941             // The min size of a `Label` is 1; and all but the last have a `b'.'` that follow it.
    942             // This means the max number of `Label`s is the ceiling of the length divided by 2.
    943             (
    944                 (self.domain.len() >> 6).max(1),
    945                 Some(self.domain.len().div_ceil(2)),
    946             )
    947         }
    948     }
    949 }
    950 impl FusedIterator for LabelIter<'_> {}
    951 impl DoubleEndedIterator for LabelIter<'_> {
    952     #[expect(
    953         unsafe_code,
    954         reason = "we only allow ASCII, so str::from_utf8_unchecked is fine"
    955     )]
    956     #[expect(
    957         clippy::arithmetic_side_effects,
    958         clippy::indexing_slicing,
    959         reason = "comments justify their correctness"
    960     )]
    961     #[inline]
    962     fn next_back(&mut self) -> Option<Self::Item> {
    963         (!self.domain.is_empty()).then(|| {
    964             self.domain
    965                 .iter()
    966                 .try_fold(0, |count, byt| {
    967                     if *byt == b'.' {
    968                         // `count + 1` < `self.domain.len()` since there is at least one more `Label` and `Label`s
    969                         // are not empty.
    970                         let ascii = &self.domain[..count];
    971                         // SAFETY:
    972                         // We only allow ASCII, so this is safe.
    973                         let value = unsafe { str::from_utf8_unchecked(ascii) };
    974                         // `count + 1` < `self.domain.len()` since there is at least one more `Label` and `Label`s
    975                         // are not empty.
    976                         self.domain = &self.domain[count + 1..];
    977                         Err(Label { value })
    978                     } else {
    979                         Ok(count + 1)
    980                     }
    981                 })
    982                 .map_or_else(convert::identity, |_| {
    983                     // SAFETY:
    984                     // We only allow ASCII, so this is safe.
    985                     let value = unsafe { str::from_utf8_unchecked(self.domain) };
    986                     self.domain = &[];
    987                     Label { value }
    988                 })
    989         })
    990     }
    991 }
    992 impl<'a, T: AsRef<[u8]>> IntoIterator for &'a Domain<T> {
    993     type Item = Label<'a>;
    994     type IntoIter = LabelIter<'a>;
    995     #[inline]
    996     fn into_iter(self) -> Self::IntoIter {
    997         LabelIter {
    998             domain: self.as_bytes(),
    999         }
   1000     }
   1001 }
   1002 impl<'a> IntoIterator for Domain<&'a str> {
   1003     type Item = Label<'a>;
   1004     type IntoIter = LabelIter<'a>;
   1005     #[inline]
   1006     fn into_iter(self) -> Self::IntoIter {
   1007         LabelIter {
   1008             domain: <&str>::from(self).as_bytes(),
   1009         }
   1010     }
   1011 }
   1012 impl<'a> IntoIterator for Domain<&'a [u8]> {
   1013     type Item = Label<'a>;
   1014     type IntoIter = LabelIter<'a>;
   1015     #[inline]
   1016     fn into_iter(self) -> Self::IntoIter {
   1017         LabelIter {
   1018             domain: <&[u8]>::from(self),
   1019         }
   1020     }
   1021 }
   1022 /// Error returned from [`Rfc1123Domain::try_from`] and [`Rfc1123Domain::try_from_bytes`].
   1023 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
   1024 pub enum Rfc1123Err {
   1025     /// The inputs was not a valid [`Domain`].
   1026     DomainErr(DomainErr),
   1027     /// A [`Label`] of [`Domain`] starts with an ASCII hyphen.
   1028     LabelStartsWithAHyphen,
   1029     /// A [`Label`] of [`Domain`] ends with an ASCII hyphen.
   1030     LabelEndsWithAHyphen,
   1031     /// The last [`Label`] (i.e., TLD) was invalid which means it was not all ASCII letters nor
   1032     /// had length of at least five with the first 4 characters being `xn--`.
   1033     InvalidTld,
   1034 }
   1035 impl Display for Rfc1123Err {
   1036     #[inline]
   1037     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
   1038         match *self {
   1039             Self::DomainErr(err) => err.fmt(f),
   1040             Self::LabelStartsWithAHyphen => {
   1041                 f.write_str("a label in the domain starts with a hyphen")
   1042             }
   1043             Self::LabelEndsWithAHyphen => f.write_str("a label in the domain ends with a hyphen"),
   1044             Self::InvalidTld => f.write_str("the TLD in the domain was not all letters nor had length of at least five with the first 4 characters being 'xn--'")
   1045         }
   1046     }
   1047 }
   1048 impl Error for Rfc1123Err {}
   1049 /// **TL;DR** Wrapper type around a [`Domain`] that enforces conformance to
   1050 /// [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123#page-13).
   1051 ///
   1052 /// * Each [`Label`] must only contain ASCII digits, letters, or hyphen.
   1053 /// * Each `Label` must not begin or end with a hyphen.
   1054 /// * The last `Label` (i.e., TLD) must either contain only ASCII letters or have length of at least five and
   1055 ///   begin with `xn--`.
   1056 /// ---
   1057 /// Unsurprisingly, RFC 1123 is not super precise as it uses "host name" to mean label and also domain:
   1058 /// "Host software MUST handle host names \[labels\] of up to 63 characters and SHOULD handle host
   1059 /// names \[domains\] of up to 255 characters". It also states that only "one aspect of host name \[label\]
   1060 /// syntax is hereby changed" from [RFC 952](https://www.rfc-editor.org/rfc/rfc952): "the restriction on the
   1061 /// first character is relaxed to allow either a letter or a digit". Despite that, it goes on to mention other
   1062 /// restrictions not mentioned in RFC 952: "the highest-level component label will be alphabetic". It is therefore
   1063 /// important to understand how this type interprets that RFC and why it does so.
   1064 ///
   1065 /// The primary issue with RFC 1123 is the unjustified comment about the TLD being alphabetic. It is given
   1066 /// as if it is common knowledge. As explained by (the rejected)
   1067 /// [Errata 1353](https://www.rfc-editor.org/errata/eid1353), there seemed to be the assumption that the TLDs
   1068 /// at the time would be the only ones that would ever exist or at least that the format of them would always be
   1069 /// true. This leads to several possible interpretations:
   1070 ///
   1071 /// * Strictest: enforce the TLD is one of the TLDs that existed at the time of the RFC.
   1072 /// * Strict: enforce the TLD has the same format as the TLDs at the time (i.e., two or three letters long).
   1073 /// * Literal: enforce the TLD is alphabetic regardless of the lack of justification.
   1074 /// * Relaxed: enforce the "spirit" that the TLD must exist.
   1075 /// * More relaxed: enforce the "spirit" that the TLD must have the same format of a valid TLD.
   1076 /// * Much more relaxed: enforce the "spirit" that the domain cannot have the form of an IPv4 address.
   1077 /// * Most relaxed: treat TLDs no differently than other labels (i.e., don't make assumptions about what will be
   1078 ///   a valid TLD in the future).
   1079 ///
   1080 /// RFC 1123 is not obsolete, and it is clear from more recent RFCs like
   1081 /// [RFC 5891](https://www.rfc-editor.org/rfc/rfc5891) that it is designed to be a foundation (i.e., domains that
   1082 /// are valid per newer RFCs are valid per RFC 1123). Clearly due to RFCs like RFC 5891, requiring the TLD
   1083 /// to be alphabetic or exactly two or three characters long would violate that. For those reasons the strictest,
   1084 /// strict, and literal interpretations are rejected.
   1085 ///
   1086 /// Assuming TLDs are static is absurd, and relying on some dynamic list of TLDs is undesirable. For that reason
   1087 /// the relaxed interpretation is rejected.
   1088 ///
   1089 /// Enforcing that domains do not have the form of an IPv4 address opens up the question of what is an IPv4
   1090 /// address? Should leading 0s be allowed? What about hexadecimal? Should there be length limits for each octet?
   1091 /// It also has the undesirable effect where subdomains that are all numeric exist but their parent domain does
   1092 /// not which goes against the hierarchical nature of DNS. For those reasons the much more relaxed interpretation
   1093 /// is rejected.
   1094 ///
   1095 /// Treating TLDs no differently than other labels is nice from a consistency perspective, but it suffers from
   1096 /// the fact that domains that have the form of an IPv4 address are now allowed. For that reason the most
   1097 /// relaxed interpretation is rejected.
   1098 ///
   1099 /// [ICANN](https://newgtlds.icann.org/sites/default/files/guidebook-full-04jun12-en.pdf) requires TLDs to either
   1100 /// be alphabetic or a valid A-label per RFC 5891. Verifying a label is a valid A-label is not a cheap operation
   1101 /// though. For that reason the more relaxed interpretation is accepted but with a twist: fake and valid A-labels
   1102 /// are allowed in addition to entirely alphabetic labels. More specifically the TLD must either contain only
   1103 /// letters or must be at least five characters long with the first 4 characters being `xn--`.
   1104 ///
   1105 /// If one wants to enforce the literal interpretation, one can use [`Self::is_literal_interpretation`]. Similarly,
   1106 /// if one wants to enforce the strict interpretation, one can use [`Self::is_strict_interpretation`].
   1107 #[derive(Clone, Copy, Debug)]
   1108 pub struct Rfc1123Domain<T> {
   1109     /// The domain.
   1110     dom: Domain<T>,
   1111 }
   1112 impl<T> Rfc1123Domain<T> {
   1113     /// Returns a reference to the contained [`Domain`].
   1114     ///
   1115     /// # Example
   1116     ///
   1117     /// ```
   1118     /// use ascii_domain::dom::Rfc1123Domain;
   1119     /// assert!(Rfc1123Domain::try_from_bytes("example.com").unwrap().domain().len().get() == 11);
   1120     /// ```
   1121     #[inline]
   1122     pub const fn domain(&self) -> &Domain<T> {
   1123         &self.dom
   1124     }
   1125     /// Returns the contained [`Domain`] consuming `self`.
   1126     ///
   1127     /// # Example
   1128     ///
   1129     /// ```
   1130     /// use ascii_domain::dom::Rfc1123Domain;
   1131     /// assert!(Rfc1123Domain::try_from_bytes("example.com").unwrap().into_domain().len().get() == 11);
   1132     /// ```
   1133     #[inline]
   1134     pub fn into_domain(self) -> Domain<T> {
   1135         self.dom
   1136     }
   1137 }
   1138 impl<T: AsRef<[u8]>> Rfc1123Domain<T> {
   1139     /// Function that transforms `v` into an `Rfc1123Domain` by only allowing [`Label`]s to contain the ASCII `u8`s
   1140     /// in [`ASCII_HYPHEN_DIGITS_LETTERS`] with each `Label` not starting or ending with a `b'-'`. A trailing `b'.'`
   1141     /// is ignored. The last `Label` (i.e., TLD) must either only contain ASCII letters or must have length of at
   1142     /// least five with the first 4 bytes being `b"xn--"`.
   1143     ///
   1144     /// Unliked calling [`Domain::try_from_bytes`] then [`Rfc1123Domain::try_from`] which performs two traversals
   1145     /// of `v`, this performs a single traversal of `v`.
   1146     ///
   1147     /// # Examples
   1148     ///
   1149     /// ```
   1150     /// use ascii_domain::dom::{Rfc1123Domain, Rfc1123Err};
   1151     /// assert!(Rfc1123Domain::try_from_bytes("example.com").is_ok());
   1152     /// assert!(Rfc1123Domain::try_from_bytes("example.xn--abc").is_ok());
   1153     /// assert!(Rfc1123Domain::try_from_bytes("a-.com").map_or_else(|err| err == Rfc1123Err::LabelEndsWithAHyphen, |_| false));
   1154     /// ```
   1155     ///
   1156     /// # Errors
   1157     ///
   1158     /// Returns [`Rfc1123Err`] iff `v.as_ref()` is an invalid `Rfc1123Domain`.
   1159     #[expect(
   1160         clippy::arithmetic_side_effects,
   1161         clippy::indexing_slicing,
   1162         reason = "comments justify their correctness"
   1163     )]
   1164     #[expect(clippy::redundant_else, reason = "prefer else with else-if")]
   1165     #[inline]
   1166     pub fn try_from_bytes(v: T) -> Result<Self, Rfc1123Err> {
   1167         // The easiest implementation would be redirecting to `Domain::try_from_bytes`; and upon success,
   1168         // verify each `Label` doesn't begin or end with a hyphen. That requires traversing `v` twice though.
   1169         // We opt to traverse just once.
   1170         let val = v.as_ref();
   1171         let value = match val.last() {
   1172             None => return Err(Rfc1123Err::DomainErr(DomainErr::Empty)),
   1173             Some(byt) => {
   1174                 let b = *byt;
   1175                 if b == b'.' {
   1176                     if val.len() == 1 {
   1177                         return Err(Rfc1123Err::DomainErr(DomainErr::RootDomain));
   1178                     }
   1179                     // We know `val.len` is at least 2.
   1180                     let len = val.len() - 1;
   1181                     let lst = val[len - 1];
   1182                     if lst == b'.' {
   1183                         return Err(Rfc1123Err::DomainErr(DomainErr::EmptyLabel));
   1184                     } else if lst == b'-' {
   1185                         return Err(Rfc1123Err::LabelEndsWithAHyphen);
   1186                     } else {
   1187                         &val[..len]
   1188                     }
   1189                 } else if b == b'-' {
   1190                     return Err(Rfc1123Err::LabelEndsWithAHyphen);
   1191                 } else {
   1192                     val
   1193                 }
   1194             }
   1195         };
   1196         if value.len() > 253 {
   1197             Err(Rfc1123Err::DomainErr(DomainErr::LenExceeds253(value.len())))
   1198         } else {
   1199             let mut count = 0;
   1200             value
   1201                 .iter()
   1202                 .try_fold(0, |label_len, byt| {
   1203                     let b = *byt;
   1204                     if b == b'.' {
   1205                         NonZeroU8::new(label_len).map_or(
   1206                             Err(Rfc1123Err::DomainErr(DomainErr::EmptyLabel)),
   1207                             |_| {
   1208                                 // We verify the last character in the `Label` is not a hyphen.
   1209                                 // `count` > 0 since `label_len` > 0 and `count` < `value.len()` since
   1210                                 // it's the index of the `b'.'`.
   1211                                 if value[count - 1] == b'-' {
   1212                                     Err(Rfc1123Err::LabelEndsWithAHyphen)
   1213                                 } else {
   1214                                     Ok(0)
   1215                                 }
   1216                             },
   1217                         )
   1218                     } else if !RFC_CHARS.contains(b) {
   1219                         Err(Rfc1123Err::DomainErr(DomainErr::InvalidByte(b)))
   1220                     } else if b == b'-' && label_len == 0 {
   1221                         Err(Rfc1123Err::LabelStartsWithAHyphen)
   1222                     } else if label_len == 63 {
   1223                         Err(Rfc1123Err::DomainErr(DomainErr::LabelLenExceeds63))
   1224                     } else {
   1225                         // This caps at 253, so no overflow.
   1226                         count += 1;
   1227                         // This is less than 64 due to the above check, so this won't overflow;
   1228                         Ok(label_len + 1)
   1229                     }
   1230                 })
   1231                 .and_then(|tld_len| {
   1232                     // `tld_len <= value.len()`.
   1233                     let tld = &value[value.len() - usize::from(tld_len)..];
   1234                     if (tld
   1235                         .split_at_checked(4)
   1236                         .is_some_and(|(fst, rem)| !rem.is_empty() && fst == b"xn--"))
   1237                         || tld
   1238                             .iter()
   1239                             .try_fold((), |(), byt| {
   1240                                 if byt.is_ascii_alphabetic() {
   1241                                     Ok(())
   1242                                 } else {
   1243                                     Err(())
   1244                                 }
   1245                             })
   1246                             .is_ok()
   1247                     {
   1248                         Ok(())
   1249                     } else {
   1250                         Err(Rfc1123Err::InvalidTld)
   1251                     }
   1252                 })
   1253                 .map(|()| Self {
   1254                     dom: Domain { value: v },
   1255                 })
   1256         }
   1257     }
   1258     /// Returns `true` iff the domain adheres to the literal interpretation of RFC 1123. For more information
   1259     /// read the description of [`Rfc1123Domain`].
   1260     ///
   1261     /// # Examples
   1262     ///
   1263     /// ```
   1264     /// use ascii_domain::dom::Rfc1123Domain;
   1265     /// assert!(Rfc1123Domain::try_from_bytes("example.commmm").unwrap().is_literal_interpretation());
   1266     /// assert!(!Rfc1123Domain::try_from_bytes("example.xn--abc").unwrap().is_literal_interpretation());
   1267     /// ```
   1268     #[inline]
   1269     pub fn is_literal_interpretation(&self) -> bool {
   1270         self.dom.tld().is_alphabetic()
   1271     }
   1272     /// Returns `true` iff the domain adheres to the strict interpretation of RFC 1123. For more information
   1273     /// read the description of [`Rfc1123Domain`].
   1274     ///
   1275     /// # Examples
   1276     ///
   1277     /// ```
   1278     /// use ascii_domain::dom::Rfc1123Domain;
   1279     /// assert!(Rfc1123Domain::try_from_bytes("example.Com").unwrap().is_strict_interpretation());
   1280     /// assert!(!Rfc1123Domain::try_from_bytes("example.comm").unwrap().is_strict_interpretation());
   1281     /// ```
   1282     #[inline]
   1283     pub fn is_strict_interpretation(&self) -> bool {
   1284         let tld = self.dom.tld();
   1285         (2..4).contains(&tld.len().get()) && tld.is_alphabetic()
   1286     }
   1287 }
   1288 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<Rfc1123Domain<T>> for Rfc1123Domain<T2> {
   1289     #[inline]
   1290     fn eq(&self, other: &Rfc1123Domain<T>) -> bool {
   1291         self.dom == other.dom
   1292     }
   1293 }
   1294 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<&Rfc1123Domain<T>> for Rfc1123Domain<T2> {
   1295     #[inline]
   1296     fn eq(&self, other: &&Rfc1123Domain<T>) -> bool {
   1297         self.dom == other.dom
   1298     }
   1299 }
   1300 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<Rfc1123Domain<T>> for &Rfc1123Domain<T2> {
   1301     #[inline]
   1302     fn eq(&self, other: &Rfc1123Domain<T>) -> bool {
   1303         self.dom == other.dom
   1304     }
   1305 }
   1306 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<Rfc1123Domain<T>> for Domain<T2> {
   1307     #[inline]
   1308     fn eq(&self, other: &Rfc1123Domain<T>) -> bool {
   1309         *self == other.dom
   1310     }
   1311 }
   1312 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<Rfc1123Domain<T>> for &Domain<T2> {
   1313     #[inline]
   1314     fn eq(&self, other: &Rfc1123Domain<T>) -> bool {
   1315         **self == other.dom
   1316     }
   1317 }
   1318 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<&Rfc1123Domain<T>> for Domain<T2> {
   1319     #[inline]
   1320     fn eq(&self, other: &&Rfc1123Domain<T>) -> bool {
   1321         *self == other.dom
   1322     }
   1323 }
   1324 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<Domain<T>> for Rfc1123Domain<T2> {
   1325     #[inline]
   1326     fn eq(&self, other: &Domain<T>) -> bool {
   1327         self.dom == *other
   1328     }
   1329 }
   1330 impl<T: AsRef<[u8]>> Eq for Rfc1123Domain<T> {}
   1331 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialOrd<Rfc1123Domain<T>> for Rfc1123Domain<T2> {
   1332     #[inline]
   1333     fn partial_cmp(&self, other: &Rfc1123Domain<T>) -> Option<Ordering> {
   1334         self.dom.partial_cmp(&other.dom)
   1335     }
   1336 }
   1337 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialOrd<Rfc1123Domain<T>> for Domain<T2> {
   1338     #[inline]
   1339     fn partial_cmp(&self, other: &Rfc1123Domain<T>) -> Option<Ordering> {
   1340         self.partial_cmp(&other.dom)
   1341     }
   1342 }
   1343 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialOrd<Domain<T>> for Rfc1123Domain<T2> {
   1344     #[inline]
   1345     fn partial_cmp(&self, other: &Domain<T>) -> Option<Ordering> {
   1346         self.dom.partial_cmp(other)
   1347     }
   1348 }
   1349 impl<T: AsRef<[u8]>> Ord for Rfc1123Domain<T> {
   1350     #[inline]
   1351     fn cmp(&self, other: &Self) -> Ordering {
   1352         self.dom.cmp(&other.dom)
   1353     }
   1354 }
   1355 impl<T: AsRef<[u8]>> Hash for Rfc1123Domain<T> {
   1356     #[inline]
   1357     fn hash<H: Hasher>(&self, state: &mut H) {
   1358         self.dom.hash(state);
   1359     }
   1360 }
   1361 impl<T> AsRef<Domain<T>> for Rfc1123Domain<T> {
   1362     #[inline]
   1363     fn as_ref(&self) -> &Domain<T> {
   1364         &self.dom
   1365     }
   1366 }
   1367 impl<T> Borrow<Domain<T>> for Rfc1123Domain<T> {
   1368     #[inline]
   1369     fn borrow(&self) -> &Domain<T> {
   1370         &self.dom
   1371     }
   1372 }
   1373 impl<T> Deref for Rfc1123Domain<T> {
   1374     type Target = Domain<T>;
   1375     #[inline]
   1376     fn deref(&self) -> &Self::Target {
   1377         &self.dom
   1378     }
   1379 }
   1380 impl<T> From<Rfc1123Domain<T>> for Domain<T> {
   1381     #[inline]
   1382     fn from(value: Rfc1123Domain<T>) -> Self {
   1383         value.dom
   1384     }
   1385 }
   1386 impl From<Rfc1123Domain<Vec<u8>>> for Rfc1123Domain<String> {
   1387     #[inline]
   1388     fn from(value: Rfc1123Domain<Vec<u8>>) -> Self {
   1389         Self {
   1390             dom: Domain::<String>::from(value.dom),
   1391         }
   1392     }
   1393 }
   1394 impl<'a: 'b, 'b, T: AsRef<[u8]>> From<&'a Rfc1123Domain<T>> for Rfc1123Domain<&'b [u8]> {
   1395     #[inline]
   1396     fn from(value: &'a Rfc1123Domain<T>) -> Self {
   1397         Self {
   1398             dom: Domain::<&'b [u8]>::from(&value.dom),
   1399         }
   1400     }
   1401 }
   1402 impl<'a: 'b, 'b, T: AsRef<str>> From<&'a Rfc1123Domain<T>> for Rfc1123Domain<&'b str> {
   1403     #[inline]
   1404     fn from(value: &'a Rfc1123Domain<T>) -> Self {
   1405         Self {
   1406             dom: Domain::<&'b str>::from(&value.dom),
   1407         }
   1408     }
   1409 }
   1410 impl From<Rfc1123Domain<String>> for Rfc1123Domain<Vec<u8>> {
   1411     #[inline]
   1412     fn from(value: Rfc1123Domain<String>) -> Self {
   1413         Self {
   1414             dom: Domain::<Vec<u8>>::from(value.dom),
   1415         }
   1416     }
   1417 }
   1418 impl<'a: 'b, 'b> From<Rfc1123Domain<&'a [u8]>> for Rfc1123Domain<&'b str> {
   1419     #[inline]
   1420     fn from(value: Rfc1123Domain<&'a [u8]>) -> Self {
   1421         Self {
   1422             dom: Domain::<&'b str>::from(value.dom),
   1423         }
   1424     }
   1425 }
   1426 impl<'a: 'b, 'b> From<Rfc1123Domain<&'a str>> for Rfc1123Domain<&'b [u8]> {
   1427     #[inline]
   1428     fn from(value: Rfc1123Domain<&'a str>) -> Self {
   1429         Self {
   1430             dom: Domain::<&'b [u8]>::from(value.dom),
   1431         }
   1432     }
   1433 }
   1434 impl<T: AsRef<[u8]>> TryFrom<Domain<T>> for Rfc1123Domain<T> {
   1435     type Error = Rfc1123Err;
   1436     #[expect(
   1437         clippy::arithmetic_side_effects,
   1438         clippy::indexing_slicing,
   1439         clippy::unreachable,
   1440         reason = "comments explain their correctness"
   1441     )]
   1442     #[inline]
   1443     fn try_from(value: Domain<T>) -> Result<Self, Self::Error> {
   1444         let mut labels = value.iter();
   1445         let tld = labels
   1446             .next()
   1447             .unwrap_or_else(|| unreachable!("there is a bug in Domain::try_from_bytes"));
   1448         if tld.is_alphabetic()
   1449             || tld
   1450                 .split_at_checked(4)
   1451                 .is_some_and(|(fst, rem)| !rem.is_empty() && fst == "xn--")
   1452         {
   1453             labels
   1454                 .try_fold((), |(), label| {
   1455                     let bytes = label.value.as_bytes();
   1456                     // `Label`s are never empty, so the below indexing is fine.
   1457                     // Underflow won't occur for the same reason.
   1458                     if bytes[0] == b'-' {
   1459                         Err(Rfc1123Err::LabelStartsWithAHyphen)
   1460                     } else if bytes[bytes.len() - 1] == b'-' {
   1461                         Err(Rfc1123Err::LabelEndsWithAHyphen)
   1462                     } else {
   1463                         bytes.iter().try_fold((), |(), byt| match *byt {
   1464                             b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' => Ok(()),
   1465                             val => Err(Rfc1123Err::DomainErr(DomainErr::InvalidByte(val))),
   1466                         })
   1467                     }
   1468                 })
   1469                 .map(|()| Self { dom: value })
   1470         } else {
   1471             Err(Rfc1123Err::InvalidTld)
   1472         }
   1473     }
   1474 }
   1475 impl<T: AsRef<[u8]>> Display for Rfc1123Domain<T> {
   1476     #[inline]
   1477     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
   1478         self.dom.fmt(f)
   1479     }
   1480 }
   1481 impl<'a, T: AsRef<[u8]>> IntoIterator for &'a Rfc1123Domain<T> {
   1482     type Item = Label<'a>;
   1483     type IntoIter = LabelIter<'a>;
   1484     #[inline]
   1485     fn into_iter(self) -> Self::IntoIter {
   1486         LabelIter {
   1487             domain: self.dom.as_bytes(),
   1488         }
   1489     }
   1490 }
   1491 impl<'a> IntoIterator for Rfc1123Domain<&'a str> {
   1492     type Item = Label<'a>;
   1493     type IntoIter = LabelIter<'a>;
   1494     #[inline]
   1495     fn into_iter(self) -> Self::IntoIter {
   1496         LabelIter {
   1497             domain: <&str>::from(self.dom).as_bytes(),
   1498         }
   1499     }
   1500 }
   1501 impl<'a> IntoIterator for Rfc1123Domain<&'a [u8]> {
   1502     type Item = Label<'a>;
   1503     type IntoIter = LabelIter<'a>;
   1504     #[inline]
   1505     fn into_iter(self) -> Self::IntoIter {
   1506         LabelIter {
   1507             domain: <&[u8]>::from(self.dom),
   1508         }
   1509     }
   1510 }