tendns 0.2.0

Encrypted TXT discovery and hexadecimal IPv4 resolution, as a library and DNS server
Documentation
use anyhow::{Result, ensure};
use hickory_proto::rr::Name;

pub fn canonical_name(value: &str) -> Result<Name> {
    let value = value.strip_suffix('.').unwrap_or(value);
    ensure!(!value.is_empty(), "DNS name must not be the root");
    for label in value.split('.') {
        validate_label(label)?;
    }
    Ok(Name::from_ascii(format!(
        "{}.",
        value.to_ascii_lowercase()
    ))?)
}

pub fn validate_label(value: &str) -> Result<()> {
    ensure!(
        !value.is_empty()
            && value.len() <= 63
            && value
                .bytes()
                .all(|b| b.is_ascii_alphanumeric() || b == b'-')
            && !value.starts_with('-')
            && !value.ends_with('-'),
        "expected one ASCII hostname label (1-63 bytes)"
    );
    Ok(())
}

pub fn entry_name(owner: &Name, subdomain: &str) -> Result<Name> {
    if subdomain.is_empty() {
        return Ok(owner.clone());
    }
    validate_label(subdomain)?;
    Ok(Name::from_ascii(subdomain)?
        .append_domain(owner)?
        .to_lowercase())
}

pub fn canonical_wire(owner: &Name) -> Vec<u8> {
    let mut bytes = Vec::with_capacity(255);
    for label in owner.iter() {
        bytes.push(label.len() as u8);
        bytes.extend(label.iter().map(u8::to_ascii_lowercase));
    }
    bytes.push(0);
    bytes
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn canonical_names_and_boundaries() {
        let name = canonical_name("HOME.Example.com").unwrap();
        assert_eq!(name.to_ascii(), "home.example.com.");
        assert_eq!(canonical_wire(&name), b"\x04home\x07example\x03com\0");
        for bad in [
            "",
            ".",
            "a..com",
            "a\\.b.com",
            "-a.com",
            "a-.com",
            "a_b.com",
        ] {
            assert!(canonical_name(bad).is_err(), "{bad}");
        }
        assert!(entry_name(&name, "a.b").is_err());
    }
}