use super::{compute, Parts};
pub use super::Type;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Suffix<'a> {
bytes: &'a [u8],
typ: Option<Type>,
fqdn: bool,
}
impl<'a> Suffix<'a> {
#[inline]
pub fn as_bytes(&self) -> &'a [u8] {
self.bytes
}
#[inline]
pub fn typ(&self) -> Option<Type> {
self.typ
}
#[inline]
pub fn is_known(&self) -> bool {
self.typ.is_some()
}
#[inline]
pub fn is_fqdn(&self) -> bool {
self.fqdn
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Domain<'a> {
bytes: &'a [u8],
prefix: Option<&'a [u8]>,
suffix: Suffix<'a>,
}
impl<'a> Domain<'a> {
#[inline]
pub fn as_bytes(&self) -> &'a [u8] {
self.bytes
}
#[inline]
pub fn suffix(&self) -> Suffix<'a> {
self.suffix
}
#[inline]
pub fn prefix(&self) -> Option<&'a [u8]> {
self.prefix
}
}
fn parse(name: &[u8]) -> Option<(&str, Parts, bool)> {
let s = core::str::from_utf8(name).ok()?;
if s.is_empty() {
return None;
}
let fqdn = s.as_bytes().last() == Some(&b'.');
let host = if fqdn { &s[..s.len() - 1] } else { s };
if host.is_empty() || has_empty_label(host) {
return None;
}
let parts = compute(host)?;
Some((host, parts, fqdn))
}
fn has_empty_label(host: &str) -> bool {
let b = host.as_bytes();
if b[0] == b'.' || b[b.len() - 1] == b'.' {
return true;
}
let mut prev_dot = false;
for &c in b {
if c == b'.' {
if prev_dot {
return true;
}
prev_dot = true;
} else {
prev_dot = false;
}
}
false
}
#[inline]
pub fn suffix(name: &[u8]) -> Option<Suffix<'_>> {
let (host, parts, fqdn) = parse(name)?;
Some(Suffix {
bytes: &host.as_bytes()[parts.suffix_off..],
typ: parts.typ,
fqdn,
})
}
#[inline]
pub fn suffix_str(name: &str) -> Option<Suffix<'_>> {
suffix(name.as_bytes())
}
#[inline]
pub fn domain(name: &[u8]) -> Option<Domain<'_>> {
let (host, parts, fqdn) = parse(name)?;
let domain_off = parts.domain_off?;
let hb = host.as_bytes();
Some(Domain {
bytes: &hb[domain_off..],
prefix: if domain_off > 0 {
Some(&hb[..domain_off - 1])
} else {
None
},
suffix: Suffix {
bytes: &hb[parts.suffix_off..],
typ: parts.typ,
fqdn,
},
})
}
#[inline]
pub fn domain_str(name: &str) -> Option<Domain<'_>> {
domain(name.as_bytes())
}
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
#[test]
fn domain_and_suffix() {
let d = domain_str("www.example.co.uk").unwrap();
assert_eq!(d.as_bytes(), b"example.co.uk");
assert_eq!(d.suffix().as_bytes(), b"co.uk");
assert!(d.suffix().is_known());
assert_eq!(d.suffix().typ(), Some(Type::Icann));
assert_eq!(suffix_str("com").unwrap().as_bytes(), b"com");
assert_eq!(domain_str("com"), None); }
#[test]
fn unknown_tld_uses_default_rule() {
let s = suffix_str("foo.example").unwrap();
assert_eq!(s.as_bytes(), b"example");
assert!(!s.is_known());
assert_eq!(
domain_str("foo.example").unwrap().as_bytes(),
b"foo.example"
);
}
#[test]
fn fully_qualified() {
let s = suffix_str("example.com.").unwrap();
assert_eq!(s.as_bytes(), b"com");
assert!(s.is_fqdn());
assert!(!suffix_str("example.com").unwrap().is_fqdn());
}
#[test]
fn byte_and_str_agree() {
assert_eq!(domain(b"a.b.example.com"), domain_str("a.b.example.com"));
assert_eq!(
domain_str("a.b.example.com").unwrap().as_bytes(),
b"example.com"
);
}
#[test]
fn invalid() {
assert_eq!(suffix(b""), None);
assert_eq!(domain(b""), None);
assert_eq!(suffix(&[0xff, 0xfe]), None); }
#[test]
fn empty_labels_rejected() {
for bad in [
"", ".", "..", "...", ".com", "com..", "..com", "a..b", "a.b..",
] {
assert_eq!(suffix_str(bad), None, "suffix_str({bad:?})");
assert_eq!(domain_str(bad), None, "domain_str({bad:?})");
}
assert!(suffix_str("example.com.").is_some());
assert_eq!(
domain_str("example.com.").unwrap().as_bytes(),
b"example.com"
);
}
#[test]
fn prefix() {
assert_eq!(
domain_str("www.example.co.uk").unwrap().prefix(),
Some(&b"www"[..])
);
assert_eq!(
domain_str("a.b.example.com").unwrap().prefix(),
Some(&b"a.b"[..])
);
assert_eq!(domain_str("example.com").unwrap().prefix(), None);
assert_eq!(
domain_str("www.example.co.uk")
.unwrap()
.prefix()
.map(|b| core::str::from_utf8(b).unwrap()),
crate::lookup("www.example.co.uk").unwrap().subdomain()
);
}
}