1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5use crate::error::{Result, SeerError};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(rename_all = "UPPERCASE")]
9pub enum RecordType {
10 A,
11 AAAA,
12 CNAME,
13 MX,
14 NS,
15 TXT,
16 SOA,
17 PTR,
18 SRV,
19 CAA,
20 NAPTR,
21 DNSKEY,
22 DS,
23 TLSA,
24 SSHFP,
25 ANY,
26}
27
28impl RecordType {
29 pub const ALL: &'static [RecordType] = &[
37 RecordType::A,
38 RecordType::AAAA,
39 RecordType::CNAME,
40 RecordType::MX,
41 RecordType::NS,
42 RecordType::TXT,
43 RecordType::SOA,
44 RecordType::PTR,
45 RecordType::SRV,
46 RecordType::CAA,
47 RecordType::NAPTR,
48 RecordType::DNSKEY,
49 RecordType::DS,
50 RecordType::TLSA,
51 RecordType::SSHFP,
52 RecordType::ANY,
53 ];
54
55 pub const ALL_NAMES: &'static [&'static str] = &[
58 "A", "AAAA", "CNAME", "MX", "NS", "TXT", "SOA", "PTR", "SRV", "CAA", "NAPTR", "DNSKEY",
59 "DS", "TLSA", "SSHFP", "ANY",
60 ];
61
62 pub const fn as_str(&self) -> &'static str {
65 match self {
66 RecordType::A => "A",
67 RecordType::AAAA => "AAAA",
68 RecordType::CNAME => "CNAME",
69 RecordType::MX => "MX",
70 RecordType::NS => "NS",
71 RecordType::TXT => "TXT",
72 RecordType::SOA => "SOA",
73 RecordType::PTR => "PTR",
74 RecordType::SRV => "SRV",
75 RecordType::CAA => "CAA",
76 RecordType::NAPTR => "NAPTR",
77 RecordType::DNSKEY => "DNSKEY",
78 RecordType::DS => "DS",
79 RecordType::TLSA => "TLSA",
80 RecordType::SSHFP => "SSHFP",
81 RecordType::ANY => "ANY",
82 }
83 }
84}
85
86impl fmt::Display for RecordType {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 f.write_str(self.as_str())
89 }
90}
91
92impl FromStr for RecordType {
93 type Err = SeerError;
94
95 fn from_str(s: &str) -> Result<Self> {
96 match s.to_uppercase().as_str() {
97 "A" => Ok(RecordType::A),
98 "AAAA" => Ok(RecordType::AAAA),
99 "CNAME" => Ok(RecordType::CNAME),
100 "MX" => Ok(RecordType::MX),
101 "NS" => Ok(RecordType::NS),
102 "TXT" => Ok(RecordType::TXT),
103 "SOA" => Ok(RecordType::SOA),
104 "PTR" => Ok(RecordType::PTR),
105 "SRV" => Ok(RecordType::SRV),
106 "CAA" => Ok(RecordType::CAA),
107 "NAPTR" => Ok(RecordType::NAPTR),
108 "DNSKEY" => Ok(RecordType::DNSKEY),
109 "DS" => Ok(RecordType::DS),
110 "TLSA" => Ok(RecordType::TLSA),
111 "SSHFP" => Ok(RecordType::SSHFP),
112 "ANY" | "*" => Ok(RecordType::ANY),
113 _ => Err(SeerError::InvalidRecordType(s.to_string())),
114 }
115 }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct DnsRecord {
120 pub name: String,
121 pub record_type: RecordType,
122 pub ttl: u32,
123 pub data: RecordData,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127#[serde(tag = "record_type", content = "value", rename_all = "UPPERCASE")]
128#[allow(clippy::upper_case_acronyms)]
129pub enum RecordData {
130 A {
131 address: String,
132 },
133 AAAA {
134 address: String,
135 },
136 CNAME {
137 target: String,
138 },
139 MX {
140 preference: u16,
141 exchange: String,
142 },
143 NS {
144 nameserver: String,
145 },
146 TXT {
147 text: String,
148 },
149 SOA {
150 mname: String,
151 rname: String,
152 serial: u32,
153 refresh: u32,
154 retry: u32,
155 expire: u32,
156 minimum: u32,
157 },
158 PTR {
159 target: String,
160 },
161 SRV {
162 priority: u16,
163 weight: u16,
164 port: u16,
165 target: String,
166 },
167 CAA {
168 flags: u8,
169 tag: String,
170 value: String,
171 },
172 DNSKEY {
173 flags: u16,
174 protocol: u8,
175 algorithm: u8,
176 public_key: String,
177 },
178 DS {
179 key_tag: u16,
180 algorithm: u8,
181 digest_type: u8,
182 digest: String,
183 },
184 TLSA {
185 cert_usage: u8,
186 selector: u8,
187 matching: u8,
188 cert_data: String,
190 },
191 SSHFP {
192 algorithm: u8,
193 fingerprint_type: u8,
194 fingerprint: String,
196 },
197 NAPTR {
198 order: u16,
199 preference: u16,
200 flags: String,
201 services: String,
202 regexp: String,
203 replacement: String,
204 },
205 Unknown {
206 raw: String,
207 },
208}
209
210impl fmt::Display for RecordData {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 match self {
213 RecordData::A { address } => write!(f, "{}", address),
214 RecordData::AAAA { address } => write!(f, "{}", address),
215 RecordData::CNAME { target } => write!(f, "{}", target),
216 RecordData::MX {
217 preference,
218 exchange,
219 } => write!(f, "{} {}", preference, exchange),
220 RecordData::NS { nameserver } => write!(f, "{}", nameserver),
221 RecordData::TXT { text } => write!(f, "\"{}\"", text),
222 RecordData::SOA {
223 mname,
224 rname,
225 serial,
226 refresh,
227 retry,
228 expire,
229 minimum,
230 } => write!(
231 f,
232 "{} {} {} {} {} {} {}",
233 mname, rname, serial, refresh, retry, expire, minimum
234 ),
235 RecordData::PTR { target } => write!(f, "{}", target),
236 RecordData::SRV {
237 priority,
238 weight,
239 port,
240 target,
241 } => write!(f, "{} {} {} {}", priority, weight, port, target),
242 RecordData::CAA { flags, tag, value } => write!(f, "{} {} \"{}\"", flags, tag, value),
243 RecordData::DNSKEY {
244 flags,
245 protocol,
246 algorithm,
247 public_key,
248 } => write!(f, "{} {} {} {}", flags, protocol, algorithm, public_key),
249 RecordData::DS {
250 key_tag,
251 algorithm,
252 digest_type,
253 digest,
254 } => write!(f, "{} {} {} {}", key_tag, algorithm, digest_type, digest),
255 RecordData::TLSA {
256 cert_usage,
257 selector,
258 matching,
259 cert_data,
260 } => write!(f, "{} {} {} {}", cert_usage, selector, matching, cert_data),
261 RecordData::SSHFP {
262 algorithm,
263 fingerprint_type,
264 fingerprint,
265 } => write!(f, "{} {} {}", algorithm, fingerprint_type, fingerprint),
266 RecordData::NAPTR {
267 order,
268 preference,
269 flags,
270 services,
271 regexp,
272 replacement,
273 } => write!(
274 f,
275 "{} {} \"{}\" \"{}\" \"{}\" {}",
276 order, preference, flags, services, regexp, replacement
277 ),
278 RecordData::Unknown { raw } => write!(f, "{}", raw),
279 }
280 }
281}
282
283impl DnsRecord {
284 pub fn format_short(&self) -> String {
285 format!("{}", self.data)
286 }
287
288 pub fn format_full(&self) -> String {
289 format!(
290 "{}\t{}\tIN\t{}\t{}",
291 self.name, self.ttl, self.record_type, self.data
292 )
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn test_record_type_from_str() {
302 assert_eq!("A".parse::<RecordType>().unwrap(), RecordType::A);
303 assert_eq!("aaaa".parse::<RecordType>().unwrap(), RecordType::AAAA);
304 assert_eq!("MX".parse::<RecordType>().unwrap(), RecordType::MX);
305 assert_eq!("*".parse::<RecordType>().unwrap(), RecordType::ANY);
306 assert!("INVALID".parse::<RecordType>().is_err());
307 }
308
309 #[test]
310 fn test_record_type_display() {
311 assert_eq!(RecordType::A.to_string(), "A");
312 assert_eq!(RecordType::AAAA.to_string(), "AAAA");
313 assert_eq!(RecordType::MX.to_string(), "MX");
314 assert_eq!(RecordType::SOA.to_string(), "SOA");
315 }
316
317 #[test]
318 fn test_dns_record_format_short() {
319 let record = DnsRecord {
320 name: "example.com".to_string(),
321 record_type: RecordType::A,
322 ttl: 300,
323 data: RecordData::A {
324 address: "1.2.3.4".to_string(),
325 },
326 };
327 assert_eq!(record.format_short(), "1.2.3.4");
328 }
329
330 #[test]
331 fn test_dns_record_format_full() {
332 let record = DnsRecord {
333 name: "example.com".to_string(),
334 record_type: RecordType::A,
335 ttl: 300,
336 data: RecordData::A {
337 address: "1.2.3.4".to_string(),
338 },
339 };
340 assert_eq!(record.format_full(), "example.com\t300\tIN\tA\t1.2.3.4");
341 }
342
343 #[test]
344 fn test_record_data_display() {
345 let mx = RecordData::MX {
346 preference: 10,
347 exchange: "mail.example.com".to_string(),
348 };
349 assert_eq!(format!("{}", mx), "10 mail.example.com");
350
351 let txt = RecordData::TXT {
352 text: "v=spf1 include:example.com".to_string(),
353 };
354 assert_eq!(format!("{}", txt), "\"v=spf1 include:example.com\"");
355
356 let srv = RecordData::SRV {
357 priority: 10,
358 weight: 5,
359 port: 443,
360 target: "server.example.com".to_string(),
361 };
362 assert_eq!(format!("{}", srv), "10 5 443 server.example.com");
363 }
364
365 #[test]
366 fn test_record_serialization_roundtrip() {
367 let record = DnsRecord {
368 name: "example.com".to_string(),
369 record_type: RecordType::A,
370 ttl: 300,
371 data: RecordData::A {
372 address: "1.2.3.4".to_string(),
373 },
374 };
375 let json = serde_json::to_string(&record).unwrap();
376 assert!(json.contains("\"A\""));
377 assert!(json.contains("1.2.3.4"));
378 }
379
380 #[test]
381 fn test_soa_display() {
382 let soa = RecordData::SOA {
383 mname: "ns1.example.com".to_string(),
384 rname: "admin.example.com".to_string(),
385 serial: 2024010101,
386 refresh: 3600,
387 retry: 900,
388 expire: 604800,
389 minimum: 86400,
390 };
391 let display = format!("{}", soa);
392 assert!(display.contains("ns1.example.com"));
393 assert!(display.contains("2024010101"));
394 }
395
396 #[test]
397 fn test_naptr_display() {
398 let naptr = RecordData::NAPTR {
400 order: 100,
401 preference: 50,
402 flags: "s".to_string(),
403 services: "http+N2L+N2C+N2R".to_string(),
404 regexp: String::new(),
405 replacement: "www.example.com.".to_string(),
406 };
407 assert_eq!(
408 format!("{}", naptr),
409 "100 50 \"s\" \"http+N2L+N2C+N2R\" \"\" www.example.com."
410 );
411 }
412
413 #[test]
418 fn all_is_complete() {
419 assert_eq!(
420 RecordType::ALL.len(),
421 16,
422 "new RecordType variant — add it to ALL and ALL_NAMES too"
423 );
424 assert_eq!(RecordType::ALL.len(), RecordType::ALL_NAMES.len());
425 }
426
427 #[test]
430 fn all_names_match_and_round_trip() {
431 for (rt, name) in RecordType::ALL.iter().zip(RecordType::ALL_NAMES) {
432 assert_eq!(&rt.as_str(), name, "ALL/ALL_NAMES out of order");
433 assert_eq!(
434 name.parse::<RecordType>().expect("advertised name parses"),
435 *rt
436 );
437 }
438 }
439}