use crate::{read_u8, DnsError};
use core::convert::TryFrom;
use core::fmt::{Display, Formatter};
use fixed_buffer::FixedBuf;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DnsName(String);
impl DnsName {
fn is_letter(b: u8) -> bool {
b.is_ascii_lowercase() || b.is_ascii_uppercase()
}
fn is_letter_digit(b: u8) -> bool {
Self::is_letter(b) || b.is_ascii_digit()
}
fn is_letter_digit_hyphen(b: u8) -> bool {
Self::is_letter_digit(b) || b == b'-'
}
fn is_valid_label(label: &str) -> bool {
if label.is_empty() || label.len() > 63 {
return false;
}
let bytes = label.as_bytes();
Self::is_letter(bytes[0])
&& bytes.iter().copied().all(Self::is_letter_digit_hyphen)
&& Self::is_letter_digit(*bytes.last().unwrap())
}
fn is_valid_name(value: &str) -> bool {
if !value.is_ascii() {
return false;
}
value.split('.').all(Self::is_valid_label)
}
pub fn new(value: &str) -> Result<Self, String> {
let trimmed = value.strip_suffix('.').unwrap_or(value);
if trimmed.len() > 255 || !Self::is_valid_name(trimmed) {
return Err(format!("not a valid DNS name: {value:?}"));
}
Ok(Self(trimmed.to_string()))
}
pub fn read<const N: usize>(buf: &mut FixedBuf<N>) -> Result<DnsName, DnsError> {
let mut value = String::new();
for _ in 0..63 {
let len = read_u8(buf)? as usize;
if len == 0 {
if value.len() > 255 {
return Err(DnsError::NameTooLong);
}
return Ok(Self(value));
}
if buf.readable().len() < len {
return Err(DnsError::Truncated);
}
let label_bytes = buf.read_bytes(len);
let label = std::str::from_utf8(label_bytes).map_err(|_| DnsError::InvalidLabel)?;
if !Self::is_valid_label(label) {
return Err(DnsError::InvalidLabel);
}
if !value.is_empty() {
value.push('.');
}
value.push_str(label);
}
Err(DnsError::TooManyLabels)
}
pub fn write<const N: usize>(&self, out: &mut FixedBuf<N>) -> Result<(), DnsError> {
for label in self.0.split('.') {
if label.len() > 63 {
return Err(DnsError::Unreachable(file!(), line!()));
}
let len =
u8::try_from(label.len()).map_err(|_| DnsError::Unreachable(file!(), line!()))?;
out.write_bytes(&[len])
.map_err(|_| DnsError::ResponseBufferFull)?;
out.write_bytes(label.as_bytes())
.map_err(|_| DnsError::ResponseBufferFull)?;
}
out.write_bytes(&[0])
.map_err(|_| DnsError::ResponseBufferFull)?;
Ok(())
}
pub fn as_bytes(&self) -> Result<FixedBuf<256>, DnsError> {
let mut buf: FixedBuf<256> = FixedBuf::new();
self.write(&mut buf)?;
Ok(buf)
}
#[must_use]
pub fn inner(&self) -> &str {
&self.0
}
}
impl Display for DnsName {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), core::fmt::Error> {
write!(f, "{}", self.0)
}
}
impl TryFrom<&'static str> for DnsName {
type Error = String;
fn try_from(value: &'static str) -> Result<Self, Self::Error> {
DnsName::new(value)
}
}
#[cfg(test)]
#[test]
fn test_err() {
assert_eq!(
<Result<DnsName, String>>::Err("not a valid DNS name: \"abc!\"".to_string()),
DnsName::new("abc!")
);
}
#[cfg(test)]
#[test]
fn test_new_label_separators() {
DnsName::new(".").unwrap_err();
assert_eq!("a", DnsName::new("a.").unwrap().inner());
DnsName::new("a..").unwrap_err();
DnsName::new(".a").unwrap_err();
DnsName::new("b..a").unwrap_err();
DnsName::new(".b.a").unwrap_err();
}
#[cfg(test)]
#[test]
fn test_new_label_charset() {
const ALLOWED: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-.";
for c in ALLOWED.chars() {
let value = format!("a{c}a");
DnsName::new(&value).expect(&value);
}
for b in 0..=255_u8 {
let c = char::from(b);
if !ALLOWED.contains(c) {
let value = format!("a{c}a");
assert_eq!(
<Result<DnsName, String>>::Err(format!("not a valid DNS name: {value:?}")),
DnsName::new(&value)
);
}
}
assert_eq!(
<Result<DnsName, String>>::Err("not a valid DNS name: \"a\u{263A}\"".to_string()),
DnsName::new("a\u{263A}")
);
}
#[cfg(test)]
#[test]
fn test_new_label() {
assert_eq!(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789",
DnsName::new("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789")
.unwrap()
.inner()
);
assert_eq!("aB-Cd.eFg", DnsName::new("aB-Cd.eFg").unwrap().inner());
}
#[cfg(test)]
#[test]
fn test_new_label_format() {
DnsName::new("a").unwrap();
DnsName::new("1").unwrap_err();
DnsName::new("1a").unwrap_err();
DnsName::new("a1").unwrap();
DnsName::new("a9876543210").unwrap();
DnsName::new("-").unwrap_err();
DnsName::new("a-").unwrap_err();
DnsName::new("-a").unwrap_err();
DnsName::new("a-.b").unwrap_err();
DnsName::new("a.-b").unwrap_err();
DnsName::new("a-b").unwrap();
DnsName::new("a-0").unwrap();
DnsName::new("a---b").unwrap();
}
#[cfg(test)]
#[test]
fn test_new_label_length() {
DnsName::new("").unwrap_err();
DnsName::new("a").unwrap();
DnsName::new("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
DnsName::new("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap_err();
}
#[cfg(test)]
#[test]
fn test_new_name_length() {
DnsName::new(concat!(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
))
.unwrap();
DnsName::new(concat!(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa."
))
.unwrap();
DnsName::new(concat!(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.a"
))
.unwrap_err();
DnsName::new(concat!(
"a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.",
"a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.",
"a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.",
"a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.",
"a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.",
"a.a.a",
))
.unwrap();
DnsName::new(concat!(
"a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.",
"a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.",
"a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.",
"a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.",
"a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.",
"a.a.aa",
))
.unwrap_err();
}
#[cfg(test)]
#[test]
fn test_inner() {
assert_eq!("abc", DnsName::new("abc").unwrap().inner());
}
#[cfg(test)]
#[test]
fn test_display() {
assert_eq!(
"example.com",
format!("{}", DnsName::new("example.com").unwrap())
);
}