Skip to main content

use_dns/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4/// Supported DNS record types for small helper routines.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum DnsRecordType {
7    /// IPv4 address record.
8    A,
9    /// IPv6 address record.
10    AAAA,
11    /// Canonical name record.
12    CNAME,
13    /// Mail exchanger record.
14    MX,
15    /// Text record.
16    TXT,
17    /// Name server record.
18    NS,
19    /// Start of authority record.
20    SOA,
21    /// Service locator record.
22    SRV,
23    /// Pointer record.
24    PTR,
25    /// Certification authority authorization record.
26    CAA,
27    /// Unknown or unsupported record type.
28    Unknown,
29}
30
31/// Stores a simple DNS record value.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct DnsRecord {
34    /// Record name.
35    pub name: String,
36    /// Record type.
37    pub record_type: DnsRecordType,
38    /// Record payload string.
39    pub value: String,
40    /// Optional TTL value.
41    pub ttl: Option<u32>,
42}
43
44fn is_valid_label(label: &str) -> bool {
45    !label.is_empty()
46        && label.len() <= 63
47        && !label.starts_with('-')
48        && !label.ends_with('-')
49        && label.chars().all(|character| {
50            character.is_ascii_alphanumeric() || character == '-' || character == '_'
51        })
52}
53
54fn normalize_candidate(input: &str) -> Option<String> {
55    let trimmed = input.trim().trim_end_matches('.');
56
57    if trimmed.is_empty()
58        || trimmed.len() > 253
59        || trimmed.contains(':')
60        || trimmed.contains('/')
61        || trimmed.chars().any(char::is_whitespace)
62    {
63        return None;
64    }
65
66    let normalized = trimmed.to_ascii_lowercase();
67
68    if normalized.split('.').all(is_valid_label) {
69        Some(normalized)
70    } else {
71        None
72    }
73}
74
75/// Parses a DNS record type from text.
76pub fn parse_record_type(input: &str) -> DnsRecordType {
77    match input.trim().to_ascii_uppercase().as_str() {
78        "A" => DnsRecordType::A,
79        "AAAA" => DnsRecordType::AAAA,
80        "CNAME" => DnsRecordType::CNAME,
81        "MX" => DnsRecordType::MX,
82        "TXT" => DnsRecordType::TXT,
83        "NS" => DnsRecordType::NS,
84        "SOA" => DnsRecordType::SOA,
85        "SRV" => DnsRecordType::SRV,
86        "PTR" => DnsRecordType::PTR,
87        "CAA" => DnsRecordType::CAA,
88        _ => DnsRecordType::Unknown,
89    }
90}
91
92/// Formats a DNS record type as uppercase text.
93pub fn format_record_type(record_type: DnsRecordType) -> &'static str {
94    match record_type {
95        DnsRecordType::A => "A",
96        DnsRecordType::AAAA => "AAAA",
97        DnsRecordType::CNAME => "CNAME",
98        DnsRecordType::MX => "MX",
99        DnsRecordType::TXT => "TXT",
100        DnsRecordType::NS => "NS",
101        DnsRecordType::SOA => "SOA",
102        DnsRecordType::SRV => "SRV",
103        DnsRecordType::PTR => "PTR",
104        DnsRecordType::CAA => "CAA",
105        DnsRecordType::Unknown => "UNKNOWN",
106    }
107}
108
109/// Returns `true` when the record type stores an IP address.
110pub fn is_address_record(record_type: DnsRecordType) -> bool {
111    matches!(record_type, DnsRecordType::A | DnsRecordType::AAAA)
112}
113
114/// Returns `true` when the record type stores an alias target.
115pub fn is_alias_record(record_type: DnsRecordType) -> bool {
116    matches!(record_type, DnsRecordType::CNAME)
117}
118
119/// Returns `true` when the record type is mail-related.
120pub fn is_mail_record(record_type: DnsRecordType) -> bool {
121    matches!(record_type, DnsRecordType::MX)
122}
123
124/// Returns `true` when the record type stores free-form text.
125pub fn is_text_record(record_type: DnsRecordType) -> bool {
126    matches!(record_type, DnsRecordType::TXT)
127}
128
129/// Returns `true` when the input looks like a DNS-style name.
130pub fn looks_like_dns_name(input: &str) -> bool {
131    normalize_candidate(input).is_some()
132}
133
134/// Normalizes a DNS-style name.
135pub fn normalize_dns_name(input: &str) -> Option<String> {
136    normalize_candidate(input)
137}