use memchr::Memchr;
#[cfg(not(any(feature = "alloc", feature = "std")))]
#[allow(unused)]
use core::str::from_utf8;
#[cfg(any(feature = "alloc", feature = "std"))]
use simdutf8::basic::from_utf8;
use core::borrow::Borrow;
pub use inlined::Buffer;
mod inlined;
#[derive(Debug, Clone, Copy, thiserror::Error)]
#[error("{}", self.as_str())]
pub struct ParseDomainError(pub(super) ());
impl ParseDomainError {
#[inline]
pub const fn as_str(&self) -> &'static str {
"invalid domain"
}
}
#[derive(Debug, Clone, Copy, thiserror::Error)]
#[error("{}", self.as_str())]
pub struct ParseAsciiDomainError(pub(super) ());
impl ParseAsciiDomainError {
#[inline]
pub const fn as_str(&self) -> &'static str {
"invalid ASCII domain"
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, derive_more::Display, derive_more::AsRef,
)]
#[repr(transparent)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct Domain<S: ?Sized>(pub(super) S);
impl<S: ?Sized> Domain<Domain<S>> {
#[inline]
pub fn flatten(self) -> Domain<S>
where
S: Sized,
{
Domain(self.0 .0)
}
#[inline]
pub const fn flatten_const(self) -> Domain<S>
where
S: Sized + Copy,
{
Domain(self.0 .0)
}
}
impl<S: ?Sized> Domain<S> {
#[inline]
pub(super) const fn new_unchecked(s: S) -> Self
where
S: Sized,
{
Self(s)
}
#[inline]
pub(super) const fn from_ref_unchecked(s: &S) -> &Self {
unsafe { &*(s as *const S as *const Domain<S>) }
}
#[inline]
pub const fn as_inner(&self) -> &S {
&self.0
}
#[inline]
pub fn into_inner(self) -> S
where
S: Sized,
{
self.0
}
#[inline]
pub const fn as_ref(&self) -> Domain<&S> {
Domain(&self.0)
}
#[inline]
pub fn as_deref(&self) -> Domain<&S::Target>
where
S: core::ops::Deref,
{
Domain(self.0.deref())
}
#[inline]
pub fn to_fqdn(&self) -> Option<Domain<Buffer>>
where
S: AsRef<[u8]>,
{
let bytes = self.0.as_ref();
if bytes.ends_with(b".") {
None
} else {
let mut buf = Buffer::copy_from_slice(bytes);
buf
.push(b'.')
.expect("valid domain length guarantees capacity for FQDN dot");
Some(Domain(buf))
}
}
#[inline]
pub fn is_fqdn(&self) -> bool
where
S: AsRef<[u8]>,
{
self.0.as_ref().ends_with(b".")
}
}
impl<S: ?Sized> Borrow<S> for Domain<S> {
#[inline]
fn borrow(&self) -> &S {
&self.0
}
}
impl<S> AsRef<str> for Domain<S>
where
S: AsRef<str>,
{
#[inline]
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl<S> AsRef<[u8]> for Domain<S>
where
S: AsRef<[u8]>,
{
#[inline]
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl<S> Domain<&S> {
#[inline]
pub const fn copied(self) -> Domain<S>
where
S: Copy,
{
Domain(*self.0)
}
#[inline]
pub fn cloned(self) -> Domain<S>
where
S: Clone,
{
Domain(self.0.clone())
}
}
impl<S: ?Sized> Domain<S> {
#[inline]
const fn ref_cast(s: &S) -> &Domain<S> {
unsafe { &*(s as *const S as *const Domain<S>) }
}
}
impl Domain<str> {
#[inline]
pub const fn try_from_ascii_str(input: &str) -> Result<&Self, ParseAsciiDomainError> {
if !input.is_ascii() {
return Err(ParseAsciiDomainError(()));
}
match verify_ascii_domain(input.as_bytes()) {
Ok(_) => Ok(Self::ref_cast(input)),
Err(_) => Err(ParseAsciiDomainError(())),
}
}
#[inline]
pub const fn as_bytes(&self) -> &Domain<[u8]> {
Domain::<[u8]>::ref_cast(self.0.as_bytes())
}
}
impl Domain<[u8]> {
#[inline]
pub const fn try_from_ascii_bytes(input: &[u8]) -> Result<&Self, ParseAsciiDomainError> {
if !input.is_ascii() {
return Err(ParseAsciiDomainError(()));
}
match verify_ascii_domain(input) {
Ok(_) => Ok(Self::ref_cast(input)),
Err(_) => Err(ParseAsciiDomainError(())),
}
}
#[inline]
pub const fn as_str(&self) -> &Domain<str> {
unsafe { Domain::<str>::ref_cast(core::str::from_utf8_unchecked(&self.0)) }
}
}
impl From<Buffer> for Domain<Buffer> {
fn from(value: Buffer) -> Self {
Domain(value)
}
}
impl From<Domain<Buffer>> for Buffer {
fn from(value: Domain<Buffer>) -> Self {
value.0
}
}
impl From<Domain<&str>> for Domain<Buffer> {
fn from(value: Domain<&str>) -> Self {
Domain(Buffer::copy_from_str(value.0))
}
}
impl From<Domain<&[u8]>> for Domain<Buffer> {
fn from(value: Domain<&[u8]>) -> Self {
Domain(Buffer::copy_from_slice(value.0))
}
}
impl From<&Domain<str>> for Domain<Buffer> {
fn from(value: &Domain<str>) -> Self {
Domain(Buffer::copy_from_str(&value.0))
}
}
impl From<&Domain<[u8]>> for Domain<Buffer> {
fn from(value: &Domain<[u8]>) -> Self {
Domain(Buffer::copy_from_slice(&value.0))
}
}
#[allow(unused)]
macro_rules! from_domain_buffer {
($(:<const $n:ident: usize>:)? $ty:ty => $conv:ident::$from:ident) => {
impl $(<const $n: usize>)? From<Domain<Buffer>> for Domain<$ty> {
fn from(value: Domain<Buffer>) -> Self {
Domain(<$ty>::$from(value.0.$conv()))
}
}
impl $(<const $n: usize>)? From<&Domain<Buffer>> for Domain<$ty> {
fn from(value: &Domain<Buffer>) -> Self {
Domain(<$ty>::$from(value.0.$conv()))
}
}
};
}
#[allow(unused)]
macro_rules! impl_try_from {
(@str $($from:expr => $ty:ty), +$(,)?) => {
$(
impl core::str::FromStr for Domain<$ty> {
impl_try_from!(@from_str_impl $from);
}
impl<'a> TryFrom<&'a str> for Domain<$ty> {
impl_try_from!(@try_from_str_impl $from);
}
)*
};
(@str_const_n $($from:expr => $ty:ty), +$(,)?) => {
$(
impl<const N: usize> core::str::FromStr for Domain<$ty> {
impl_try_from!(@from_str_impl $from);
}
impl<'a, const N: usize> TryFrom<&'a str> for Domain<$ty> {
impl_try_from!(@try_from_str_impl $from);
}
)*
};
(@from_str_impl $from:expr) => {
type Err = ParseDomainError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Domain::try_from_str(s).map(|res| match res {
either::Either::Left(d) => Self($from(d)),
either::Either::Right(d) => Self(d.into()),
})
}
};
(@try_from_str_impl $($from:expr), +$(,)?) => {
type Error = ParseDomainError;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
core::str::FromStr::from_str(value)
}
};
(@bytes $($from:expr => $ty:ty), +$(,)?) => {
$(
impl<'a> TryFrom<&'a [u8]> for Domain<$ty> {
impl_try_from!(@bytes_impl $from);
}
)*
};
(@bytes_const_n $($from:expr => $ty:ty), +$(,)?) => {
$(
impl<'a, const N: usize> TryFrom<&'a [u8]> for Domain<$ty> {
impl_try_from!(@bytes_impl $from);
}
)*
};
(@bytes_impl $($from:expr), +$(,)?) => {
$(
type Error = ParseDomainError;
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
Domain::try_from_bytes(value).map(|res| match res {
either::Either::Left(d) => Self(($from)(d)),
either::Either::Right(d) => Self(d.into()),
})
}
)*
};
(@owned $($try_from:ident($as:ident, $ty:ty)), +$(,)?) => {
$(
impl TryFrom<$ty> for Domain<$ty> {
impl_try_from!(@owned_try_from_impl $try_from($as, $ty));
}
impl<'a> TryFrom<&'a $ty> for Domain<$ty> {
impl_try_from!(@owned_try_from_ref_impl $try_from($as, $ty));
}
)*
};
(@owned_const_n $($try_from:ident($as:ident, $ty:ty)), +$(,)?) => {
$(
impl<const N: usize> TryFrom<$ty> for Domain<$ty> {
impl_try_from!(@owned_try_from_impl $try_from($as, $ty));
}
impl<'a, const N: usize> TryFrom<&'a $ty> for Domain<$ty> {
impl_try_from!(@owned_try_from_ref_impl $try_from($as, $ty));
}
)*
};
(@owned_try_from_impl $try_from:ident($as:ident, $ty:ty)) => {
type Error = ParseDomainError;
fn try_from(value: $ty) -> Result<Self, Self::Error> {
Ok(match Domain::$try_from(value.$as())? {
either::Either::Left(_) => Self(value.clone()),
either::Either::Right(d) => Self(d.into()),
})
}
};
(@owned_try_from_ref_impl $try_from:ident($as:ident, $ty:ty)) => {
type Error = ParseDomainError;
fn try_from(value: &'a $ty) -> Result<Self, Self::Error> {
Ok(match Domain::$try_from(value.$as())? {
either::Either::Left(_) => Self(value.clone()),
either::Either::Right(d) => Self(d.into()),
})
}
};
}
#[cfg(not(any(feature = "alloc", feature = "std")))]
const _: () = {
impl TryFrom<&str> for Domain<Buffer> {
type Error = ParseDomainError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Domain::try_from_ascii_str(value)
.map(Into::into)
.map_err(|_| ParseDomainError(()))
}
}
impl TryFrom<&[u8]> for Domain<Buffer> {
type Error = ParseDomainError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Domain::try_from_ascii_bytes(value)
.map(Into::into)
.map_err(|_| ParseDomainError(()))
}
}
};
#[cfg(any(feature = "alloc", feature = "std"))]
const _: () = {
use std::{
borrow::Cow,
string::{String, ToString},
vec::Vec,
};
use idna::{
uts46::{verify_dns_length, ErrorPolicy, Hyphens, ProcessingSuccess, Uts46},
AsciiDenyList,
};
impl<S> Domain<S> {
#[inline]
pub fn try_from_bytes(input: S) -> Result<either::Either<Self, Buffer>, ParseDomainError>
where
S: AsRef<[u8]>,
{
macro_rules! validate_length {
($buf:expr) => {{
if !verify_dns_length($buf, true) {
return Err(ParseDomainError(()));
}
}};
}
let domain = input.as_ref();
if Memchr::new(b'%', domain).next().is_some() {
let input = percent_encoding::percent_decode(domain);
let mut domain_buf = Buffer::new();
for byte in input {
domain_buf.push(byte).map_err(|_| ParseDomainError(()))?;
}
let input = domain_buf.as_bytes();
if input.is_ascii() {
return verify_ascii_domain(input)
.map(|_| either::Either::Right(domain_buf))
.map_err(|_| ParseDomainError(()));
}
let mut sinker = Buffer::new();
let buf = match domain_to_ascii(input, &mut sinker)? {
either::Either::Left(_) => domain_buf,
either::Either::Right(_) => sinker,
};
validate_length!(buf.as_str());
return Ok(either::Either::Right(buf));
}
if domain.is_ascii() {
return verify_ascii_domain(domain)
.map(|_| either::Either::Left(Self(input)))
.map_err(|_| ParseDomainError(()));
}
let mut sinker = Buffer::new();
Ok(match domain_to_ascii(domain, &mut sinker)? {
either::Either::Left(_) => {
validate_length!(from_utf8(domain).map_err(|_| ParseDomainError(()))?);
either::Either::Left(Self(input))
}
either::Either::Right(_) => {
validate_length!(sinker.as_str());
either::Either::Right(sinker)
}
})
}
#[inline]
pub fn try_from_str(input: S) -> Result<either::Either<Self, Buffer>, ParseDomainError>
where
S: AsRef<str>,
{
let domain = input.as_ref();
Ok(
match Domain::try_from_bytes(domain.as_bytes()).map_err(|_| ParseDomainError(()))? {
either::Either::Left(_) => either::Either::Left(Self(input)),
either::Either::Right(d) => either::Either::Right(d),
},
)
}
}
impl<'a> TryFrom<&'a str> for Domain<Cow<'a, str>> {
type Error = ParseDomainError;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
Domain::try_from_str(value).map(|res| match res {
either::Either::Left(d) => Self(Cow::Borrowed(d.0)),
either::Either::Right(d) => Self(d.into()),
})
}
}
impl<'a> TryFrom<&'a [u8]> for Domain<Cow<'a, [u8]>> {
type Error = ParseDomainError;
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
Domain::try_from_bytes(value).map(|res| match res {
either::Either::Left(d) => Self(Cow::Borrowed(d.0)),
either::Either::Right(d) => Self(d.into()),
})
}
}
impl_try_from!(
@owned
try_from_str(as_str, String),
try_from_str(as_ref, std::sync::Arc<str>),
try_from_str(as_ref, std::boxed::Box<str>),
try_from_str(as_ref, std::rc::Rc<str>),
try_from_bytes(as_slice, Vec<u8>),
try_from_bytes(as_ref, std::sync::Arc<[u8]>),
try_from_bytes(as_ref, std::boxed::Box<[u8]>),
try_from_bytes(as_ref, std::rc::Rc<[u8]>),
);
impl_try_from!(
@str
|d: Domain<_>| String::from(d.0) => String,
|d: Domain<_>| std::sync::Arc::from(d.0) => std::sync::Arc<str>,
|d: Domain<_>| std::boxed::Box::from(d.0) => std::boxed::Box<str>,
|d: Domain<_>| std::rc::Rc::from(d.0) => std::rc::Rc<str>,
|d: Domain<&str>| d.0.into() => Vec<u8>,
|d: Domain<&str>| std::sync::Arc::from(d.0.as_bytes()) => std::sync::Arc<[u8]>,
|d: Domain<&str>| std::boxed::Box::from(d.0.as_bytes()) => std::boxed::Box<[u8]>,
|d: Domain<&str>| std::rc::Rc::from(d.0.as_bytes()) => std::rc::Rc<[u8]>,
|d: Domain<&str>| {
Buffer::copy_from_str(d.0)
} => Buffer,
);
impl_try_from!(
@bytes
|d: Domain<_>| unsafe { core::str::from_utf8_unchecked(d.0) }.to_string() => String,
|d: Domain<_>| std::sync::Arc::from(unsafe { core::str::from_utf8_unchecked(d.0) }) => std::sync::Arc<str>,
|d: Domain<_>| std::boxed::Box::from(unsafe { core::str::from_utf8_unchecked(d.0) }) => std::boxed::Box<str>,
|d: Domain<_>| std::rc::Rc::from(unsafe { core::str::from_utf8_unchecked(d.0) }) => std::rc::Rc<str>,
|d: Domain<&[u8]>| d.0.to_vec() => Vec<u8>,
|d: Domain<_>| std::sync::Arc::from(d.0) => std::sync::Arc<[u8]>,
|d: Domain<_>| std::boxed::Box::from(d.0) => std::boxed::Box<[u8]>,
|d: Domain<_>| std::rc::Rc::from(d.0) => std::rc::Rc<[u8]>,
|d: Domain<&[u8]>| Buffer::copy_from_slice(d.0) => Buffer,
);
from_domain_buffer!(String => as_str::from);
from_domain_buffer!(std::sync::Arc<str> => as_str::from);
from_domain_buffer!(std::boxed::Box<str> => as_str::from);
from_domain_buffer!(std::rc::Rc<str> => as_str::from);
from_domain_buffer!(Vec<u8> => as_bytes::from);
from_domain_buffer!(std::sync::Arc<[u8]> => as_bytes::from);
from_domain_buffer!(std::boxed::Box<[u8]> => as_bytes::from);
from_domain_buffer!(std::rc::Rc<[u8]> => as_bytes::from);
fn domain_to_ascii<S>(
domain: &[u8],
mut sinker: S,
) -> Result<either::Either<&str, S>, ParseDomainError>
where
S: core::fmt::Write,
{
let uts46 = Uts46::new();
let result = uts46.process(
domain,
AsciiDenyList::URL,
Hyphens::Allow,
ErrorPolicy::FailFast,
|_, _, _| false, &mut sinker,
None,
);
Ok(match result {
Ok(res) => match res {
ProcessingSuccess::WroteToSink => either::Either::Right(sinker),
ProcessingSuccess::Passthrough => {
either::Either::Left(from_utf8(domain).map_err(|_| ParseDomainError(()))?)
}
},
Err(_) => return Err(ParseDomainError(())),
})
}
};
#[cfg(all(feature = "smol_str_0_3", any(feature = "std", feature = "alloc")))]
const _: () = {
use smol_str_0_3::SmolStr;
impl_try_from!(@str
|d: Domain<_>| SmolStr::from(d.0) => SmolStr,
);
impl_try_from!(@bytes
|d: Domain<_>| SmolStr::from(unsafe { core::str::from_utf8_unchecked(d.0) }) => SmolStr,
);
impl_try_from!(@owned try_from_str(as_str, SmolStr));
from_domain_buffer!(SmolStr => as_str::new);
};
#[cfg(all(feature = "triomphe_0_1", any(feature = "std", feature = "alloc")))]
const _: () = {
use triomphe_0_1::Arc;
impl_try_from!(@str
|d: Domain<_>| Arc::<str>::from(d.0) => Arc<str>,
|d: Domain<&str>| Arc::<[u8]>::from(d.0.as_bytes()) => Arc<[u8]>,
);
impl_try_from!(@bytes
|d: Domain<_>| Arc::from(d.0) => Arc<[u8]>,
|d: Domain<&[u8]>| Arc::from(from_utf8(d.0).expect("doamain is ASCII")) => Arc<str>,
);
impl_try_from!(@owned try_from_str(as_ref, Arc<str>), try_from_bytes(as_ref, Arc<[u8]>));
from_domain_buffer!(Arc<str> => as_str::from);
from_domain_buffer!(Arc<[u8]> => as_bytes::from);
};
#[cfg(all(feature = "bytes_1", any(feature = "std", feature = "alloc")))]
const _: () = {
use bytes_1::Bytes;
impl_try_from!(@str
|d: Domain<&str>| Bytes::copy_from_slice(d.0.as_bytes()) => Bytes,
);
impl_try_from!(@bytes
|d: Domain<_>| Bytes::copy_from_slice(d.0) => Bytes,
);
impl_try_from!(@owned try_from_bytes(as_ref, Bytes));
from_domain_buffer!(Bytes => as_bytes::copy_from_slice);
};
#[cfg(feature = "cheap-clone")]
impl<S: cheap_clone::CheapClone> cheap_clone::CheapClone for Domain<S> {}
#[cfg(all(feature = "tinyvec_1", any(feature = "std", feature = "alloc")))]
const _: () = {
use tinyvec_1::TinyVec;
impl_try_from!(@str_const_n
|d: Domain<&str>| TinyVec::from(d.0.as_bytes()) => TinyVec<[u8; N]>,
);
impl_try_from!(@bytes_const_n
|d: Domain<_>| TinyVec::from(d.0) => TinyVec<[u8; N]>,
);
impl_try_from!(
@owned_const_n try_from_bytes(as_ref, TinyVec<[u8; N]>),
);
from_domain_buffer!(:<const N: usize>: TinyVec<[u8; N]> => as_bytes::from);
};
#[cfg(all(feature = "smallvec_1", any(feature = "std", feature = "alloc")))]
const _: () = {
use smallvec_1::SmallVec;
impl_try_from!(@str_const_n
|d: Domain<&str>| SmallVec::from_slice(d.0.as_bytes()) => SmallVec<[u8; N]>,
);
impl_try_from!(@bytes_const_n
|d: Domain<_>| SmallVec::from_slice(d.0) => SmallVec<[u8; N]>,
);
impl_try_from!(@owned_const_n try_from_bytes(as_ref, SmallVec<[u8; N]>));
from_domain_buffer!(:<const N: usize>: SmallVec<[u8; N]> => as_bytes::from_slice);
};
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))]
pub fn verify_domain(input: &[u8]) -> Result<(), ParseDomainError> {
use idna::{
uts46::{ErrorPolicy, Hyphens, Uts46},
AsciiDenyList,
};
#[derive(Default)]
struct Eat {
len: usize,
last: u8,
}
impl core::fmt::Write for Eat {
fn write_str(&mut self, val: &str) -> core::fmt::Result {
self.len += val.len();
if let Some(last) = val.as_bytes().last() {
self.last = *last;
}
Ok(())
}
}
fn domain_to_ascii(domain: &[u8], sinker: &mut Eat) -> Result<(), ParseDomainError> {
let uts46 = Uts46::new();
let result = uts46.process(
domain,
AsciiDenyList::URL,
Hyphens::Allow,
ErrorPolicy::FailFast,
|_, _, _| false, sinker,
None,
);
match result {
Ok(_) => Ok(()),
Err(_) => Err(ParseDomainError(())),
}
}
macro_rules! validate_length {
($eat:ident) => {{
if $eat.len > 0 {
if $eat.last == b'.' {
if $eat.len > 254 {
return Err(ParseDomainError(()));
}
} else if $eat.len > 253 {
return Err(ParseDomainError(()));
}
}
}};
}
let domain = input;
if Memchr::new(b'%', domain).next().is_some() {
let input = percent_encoding::percent_decode(domain);
let mut domain_buf = Buffer::new();
for byte in input {
domain_buf.push(byte).map_err(|_| ParseDomainError(()))?;
}
let input = domain_buf.as_bytes();
if input.is_ascii() {
return verify_ascii_domain(input).map_err(|_| ParseDomainError(()));
}
let mut eat = Eat::default();
domain_to_ascii(input, &mut eat)?;
validate_length!(eat);
return Ok(());
}
if domain.is_ascii() {
return verify_ascii_domain(domain).map_err(|_| ParseDomainError(()));
}
let mut eat = Eat::default();
domain_to_ascii(domain, &mut eat)?;
validate_length!(eat);
Ok(())
}
pub fn verify_ascii_domain_allow_percent_encoding(
domain: &[u8],
) -> Result<(), ParseAsciiDomainError> {
if Memchr::new(b'%', domain).next().is_some() {
let input = percent_encoding::percent_decode(domain);
let mut domain_buf = Buffer::new();
for byte in input {
domain_buf
.push(byte)
.map_err(|_| ParseAsciiDomainError(()))?;
}
let input = domain_buf.as_bytes();
if input.is_ascii() {
return verify_ascii_domain(input).map_err(|_| ParseAsciiDomainError(()));
}
return Err(ParseAsciiDomainError(()));
}
if domain.is_ascii() {
return verify_ascii_domain(domain).map_err(|_| ParseAsciiDomainError(()));
}
Err(ParseAsciiDomainError(()))
}
pub const fn verify_ascii_domain(input: &[u8]) -> Result<(), ParseAsciiDomainError> {
enum State {
Start,
Next,
NumericOnly { len: usize },
NextAfterNumericOnly,
Subsequent { len: usize },
Hyphen { len: usize },
}
use State::*;
let mut state = Start;
const MAX_LABEL_LENGTH: usize = 63;
const MAX_NAME_LENGTH: usize = 253;
let len = input.len();
if len == 0 {
return Err(ParseAsciiDomainError(()));
}
if len > MAX_NAME_LENGTH {
if len == MAX_NAME_LENGTH + 1 {
let Some(b'.') = input.last() else {
return Err(ParseAsciiDomainError(()));
};
} else {
return Err(ParseAsciiDomainError(()));
}
}
if input[0] == b'.' && len == 1 {
return Ok(());
}
let mut i = 0;
while i < len {
let ch = input[i];
state = match (state, ch) {
(Start | Next | NextAfterNumericOnly | Hyphen { .. }, b'.') => {
return Err(ParseAsciiDomainError(()))
}
(Subsequent { .. }, b'.') => Next,
(NumericOnly { .. }, b'.') => NextAfterNumericOnly,
(Subsequent { len } | NumericOnly { len } | Hyphen { len }, _) if len >= MAX_LABEL_LENGTH => {
return Err(ParseAsciiDomainError(()));
}
(Start | Next | NextAfterNumericOnly, b'0'..=b'9') => NumericOnly { len: 1 },
(NumericOnly { len }, b'0'..=b'9') => NumericOnly { len: len + 1 },
(Start | Next | NextAfterNumericOnly, b'a'..=b'z' | b'A'..=b'Z' | b'_') => {
Subsequent { len: 1 }
}
(Subsequent { len } | NumericOnly { len } | Hyphen { len }, b'-') => Hyphen { len: len + 1 },
(
Subsequent { len } | NumericOnly { len } | Hyphen { len },
b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'0'..=b'9',
) => Subsequent { len: len + 1 },
_ => return Err(ParseAsciiDomainError(())),
};
i += 1;
}
if matches!(
state,
Start | Hyphen { .. } | NumericOnly { .. } | NextAfterNumericOnly
) {
return Err(ParseAsciiDomainError(()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn negative_try_from_ascii_bytes() {
let err = Domain::<[u8]>::try_from_ascii_bytes("测试.中国".as_bytes()).unwrap_err();
assert_eq!(err.as_str(), "invalid ASCII domain");
let err = Domain::<[u8]>::try_from_ascii_bytes("@example.com".as_bytes()).unwrap_err();
assert_eq!(err.as_str(), "invalid ASCII domain");
}
#[test]
fn negative_try_from_ascii_str() {
let err = Domain::<str>::try_from_ascii_str("测试.中国").unwrap_err();
assert_eq!(err.as_str(), "invalid ASCII domain");
let err = Domain::<str>::try_from_ascii_str("@example.com").unwrap_err();
assert_eq!(err.as_str(), "invalid ASCII domain");
}
#[test]
#[cfg(feature = "std")]
fn domain_length_boundary() {
fn make_domain(label_lens: &[usize]) -> std::string::String {
label_lens
.iter()
.enumerate()
.map(|(i, &len)| std::string::String::from_utf8(vec![b'a' + (i as u8 % 26); len]).unwrap())
.collect::<std::vec::Vec<_>>()
.join(".")
}
let d253 = make_domain(&[50, 50, 50, 50, 49]);
assert_eq!(d253.len(), 253);
assert!(verify_ascii_domain(d253.as_bytes()).is_ok());
let d254_fqdn = std::format!("{}.", d253);
assert_eq!(d254_fqdn.len(), 254);
assert!(verify_ascii_domain(d254_fqdn.as_bytes()).is_ok());
let d254 = make_domain(&[50, 50, 50, 50, 50]);
assert_eq!(d254.len(), 254);
assert!(verify_ascii_domain(d254.as_bytes()).is_err());
let d255 = make_domain(&[50, 50, 50, 50, 51]);
assert_eq!(d255.len(), 255);
assert!(verify_ascii_domain(d255.as_bytes()).is_err());
let d300 = make_domain(&[50, 50, 50, 50, 50, 45]);
assert_eq!(d300.len(), 300);
assert!(verify_ascii_domain(d300.as_bytes()).is_err());
}
}