1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum DnsRecordType {
7 A,
9 AAAA,
11 CNAME,
13 MX,
15 TXT,
17 NS,
19 SOA,
21 SRV,
23 PTR,
25 CAA,
27 Unknown,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct DnsRecord {
34 pub name: String,
36 pub record_type: DnsRecordType,
38 pub value: String,
40 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
75pub 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
92pub 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
109pub fn is_address_record(record_type: DnsRecordType) -> bool {
111 matches!(record_type, DnsRecordType::A | DnsRecordType::AAAA)
112}
113
114pub fn is_alias_record(record_type: DnsRecordType) -> bool {
116 matches!(record_type, DnsRecordType::CNAME)
117}
118
119pub fn is_mail_record(record_type: DnsRecordType) -> bool {
121 matches!(record_type, DnsRecordType::MX)
122}
123
124pub fn is_text_record(record_type: DnsRecordType) -> bool {
126 matches!(record_type, DnsRecordType::TXT)
127}
128
129pub fn looks_like_dns_name(input: &str) -> bool {
131 normalize_candidate(input).is_some()
132}
133
134pub fn normalize_dns_name(input: &str) -> Option<String> {
136 normalize_candidate(input)
137}