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 fmt::Display for RecordType {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 RecordType::A => write!(f, "A"),
32 RecordType::AAAA => write!(f, "AAAA"),
33 RecordType::CNAME => write!(f, "CNAME"),
34 RecordType::MX => write!(f, "MX"),
35 RecordType::NS => write!(f, "NS"),
36 RecordType::TXT => write!(f, "TXT"),
37 RecordType::SOA => write!(f, "SOA"),
38 RecordType::PTR => write!(f, "PTR"),
39 RecordType::SRV => write!(f, "SRV"),
40 RecordType::CAA => write!(f, "CAA"),
41 RecordType::NAPTR => write!(f, "NAPTR"),
42 RecordType::DNSKEY => write!(f, "DNSKEY"),
43 RecordType::DS => write!(f, "DS"),
44 RecordType::TLSA => write!(f, "TLSA"),
45 RecordType::SSHFP => write!(f, "SSHFP"),
46 RecordType::ANY => write!(f, "ANY"),
47 }
48 }
49}
50
51impl FromStr for RecordType {
52 type Err = SeerError;
53
54 fn from_str(s: &str) -> Result<Self> {
55 match s.to_uppercase().as_str() {
56 "A" => Ok(RecordType::A),
57 "AAAA" => Ok(RecordType::AAAA),
58 "CNAME" => Ok(RecordType::CNAME),
59 "MX" => Ok(RecordType::MX),
60 "NS" => Ok(RecordType::NS),
61 "TXT" => Ok(RecordType::TXT),
62 "SOA" => Ok(RecordType::SOA),
63 "PTR" => Ok(RecordType::PTR),
64 "SRV" => Ok(RecordType::SRV),
65 "CAA" => Ok(RecordType::CAA),
66 "NAPTR" => Ok(RecordType::NAPTR),
67 "DNSKEY" => Ok(RecordType::DNSKEY),
68 "DS" => Ok(RecordType::DS),
69 "TLSA" => Ok(RecordType::TLSA),
70 "SSHFP" => Ok(RecordType::SSHFP),
71 "ANY" | "*" => Ok(RecordType::ANY),
72 _ => Err(SeerError::InvalidRecordType(s.to_string())),
73 }
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct DnsRecord {
79 pub name: String,
80 pub record_type: RecordType,
81 pub ttl: u32,
82 pub data: RecordData,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86#[serde(tag = "record_type", content = "value", rename_all = "UPPERCASE")]
87#[allow(clippy::upper_case_acronyms)]
88pub enum RecordData {
89 A {
90 address: String,
91 },
92 AAAA {
93 address: String,
94 },
95 CNAME {
96 target: String,
97 },
98 MX {
99 preference: u16,
100 exchange: String,
101 },
102 NS {
103 nameserver: String,
104 },
105 TXT {
106 text: String,
107 },
108 SOA {
109 mname: String,
110 rname: String,
111 serial: u32,
112 refresh: u32,
113 retry: u32,
114 expire: u32,
115 minimum: u32,
116 },
117 PTR {
118 target: String,
119 },
120 SRV {
121 priority: u16,
122 weight: u16,
123 port: u16,
124 target: String,
125 },
126 CAA {
127 flags: u8,
128 tag: String,
129 value: String,
130 },
131 DNSKEY {
132 flags: u16,
133 protocol: u8,
134 algorithm: u8,
135 public_key: String,
136 },
137 DS {
138 key_tag: u16,
139 algorithm: u8,
140 digest_type: u8,
141 digest: String,
142 },
143 TLSA {
144 cert_usage: u8,
145 selector: u8,
146 matching: u8,
147 cert_data: String,
149 },
150 SSHFP {
151 algorithm: u8,
152 fingerprint_type: u8,
153 fingerprint: String,
155 },
156 NAPTR {
157 order: u16,
158 preference: u16,
159 flags: String,
160 services: String,
161 regexp: String,
162 replacement: String,
163 },
164 Unknown {
165 raw: String,
166 },
167}
168
169impl fmt::Display for RecordData {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 match self {
172 RecordData::A { address } => write!(f, "{}", address),
173 RecordData::AAAA { address } => write!(f, "{}", address),
174 RecordData::CNAME { target } => write!(f, "{}", target),
175 RecordData::MX {
176 preference,
177 exchange,
178 } => write!(f, "{} {}", preference, exchange),
179 RecordData::NS { nameserver } => write!(f, "{}", nameserver),
180 RecordData::TXT { text } => write!(f, "\"{}\"", text),
181 RecordData::SOA {
182 mname,
183 rname,
184 serial,
185 refresh,
186 retry,
187 expire,
188 minimum,
189 } => write!(
190 f,
191 "{} {} {} {} {} {} {}",
192 mname, rname, serial, refresh, retry, expire, minimum
193 ),
194 RecordData::PTR { target } => write!(f, "{}", target),
195 RecordData::SRV {
196 priority,
197 weight,
198 port,
199 target,
200 } => write!(f, "{} {} {} {}", priority, weight, port, target),
201 RecordData::CAA { flags, tag, value } => write!(f, "{} {} \"{}\"", flags, tag, value),
202 RecordData::DNSKEY {
203 flags,
204 protocol,
205 algorithm,
206 public_key,
207 } => write!(f, "{} {} {} {}", flags, protocol, algorithm, public_key),
208 RecordData::DS {
209 key_tag,
210 algorithm,
211 digest_type,
212 digest,
213 } => write!(f, "{} {} {} {}", key_tag, algorithm, digest_type, digest),
214 RecordData::TLSA {
215 cert_usage,
216 selector,
217 matching,
218 cert_data,
219 } => write!(f, "{} {} {} {}", cert_usage, selector, matching, cert_data),
220 RecordData::SSHFP {
221 algorithm,
222 fingerprint_type,
223 fingerprint,
224 } => write!(f, "{} {} {}", algorithm, fingerprint_type, fingerprint),
225 RecordData::NAPTR {
226 order,
227 preference,
228 flags,
229 services,
230 regexp,
231 replacement,
232 } => write!(
233 f,
234 "{} {} \"{}\" \"{}\" \"{}\" {}",
235 order, preference, flags, services, regexp, replacement
236 ),
237 RecordData::Unknown { raw } => write!(f, "{}", raw),
238 }
239 }
240}
241
242impl DnsRecord {
243 pub fn format_short(&self) -> String {
244 format!("{}", self.data)
245 }
246
247 pub fn format_full(&self) -> String {
248 format!(
249 "{}\t{}\tIN\t{}\t{}",
250 self.name, self.ttl, self.record_type, self.data
251 )
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_record_type_from_str() {
261 assert_eq!("A".parse::<RecordType>().unwrap(), RecordType::A);
262 assert_eq!("aaaa".parse::<RecordType>().unwrap(), RecordType::AAAA);
263 assert_eq!("MX".parse::<RecordType>().unwrap(), RecordType::MX);
264 assert_eq!("*".parse::<RecordType>().unwrap(), RecordType::ANY);
265 assert!("INVALID".parse::<RecordType>().is_err());
266 }
267
268 #[test]
269 fn test_record_type_display() {
270 assert_eq!(RecordType::A.to_string(), "A");
271 assert_eq!(RecordType::AAAA.to_string(), "AAAA");
272 assert_eq!(RecordType::MX.to_string(), "MX");
273 assert_eq!(RecordType::SOA.to_string(), "SOA");
274 }
275
276 #[test]
277 fn test_dns_record_format_short() {
278 let record = DnsRecord {
279 name: "example.com".to_string(),
280 record_type: RecordType::A,
281 ttl: 300,
282 data: RecordData::A {
283 address: "1.2.3.4".to_string(),
284 },
285 };
286 assert_eq!(record.format_short(), "1.2.3.4");
287 }
288
289 #[test]
290 fn test_dns_record_format_full() {
291 let record = DnsRecord {
292 name: "example.com".to_string(),
293 record_type: RecordType::A,
294 ttl: 300,
295 data: RecordData::A {
296 address: "1.2.3.4".to_string(),
297 },
298 };
299 assert_eq!(record.format_full(), "example.com\t300\tIN\tA\t1.2.3.4");
300 }
301
302 #[test]
303 fn test_record_data_display() {
304 let mx = RecordData::MX {
305 preference: 10,
306 exchange: "mail.example.com".to_string(),
307 };
308 assert_eq!(format!("{}", mx), "10 mail.example.com");
309
310 let txt = RecordData::TXT {
311 text: "v=spf1 include:example.com".to_string(),
312 };
313 assert_eq!(format!("{}", txt), "\"v=spf1 include:example.com\"");
314
315 let srv = RecordData::SRV {
316 priority: 10,
317 weight: 5,
318 port: 443,
319 target: "server.example.com".to_string(),
320 };
321 assert_eq!(format!("{}", srv), "10 5 443 server.example.com");
322 }
323
324 #[test]
325 fn test_record_serialization_roundtrip() {
326 let record = DnsRecord {
327 name: "example.com".to_string(),
328 record_type: RecordType::A,
329 ttl: 300,
330 data: RecordData::A {
331 address: "1.2.3.4".to_string(),
332 },
333 };
334 let json = serde_json::to_string(&record).unwrap();
335 assert!(json.contains("\"A\""));
336 assert!(json.contains("1.2.3.4"));
337 }
338
339 #[test]
340 fn test_soa_display() {
341 let soa = RecordData::SOA {
342 mname: "ns1.example.com".to_string(),
343 rname: "admin.example.com".to_string(),
344 serial: 2024010101,
345 refresh: 3600,
346 retry: 900,
347 expire: 604800,
348 minimum: 86400,
349 };
350 let display = format!("{}", soa);
351 assert!(display.contains("ns1.example.com"));
352 assert!(display.contains("2024010101"));
353 }
354
355 #[test]
356 fn test_naptr_display() {
357 let naptr = RecordData::NAPTR {
359 order: 100,
360 preference: 50,
361 flags: "s".to_string(),
362 services: "http+N2L+N2C+N2R".to_string(),
363 regexp: String::new(),
364 replacement: "www.example.com.".to_string(),
365 };
366 assert_eq!(
367 format!("{}", naptr),
368 "100 50 \"s\" \"http+N2L+N2C+N2R\" \"\" www.example.com."
369 );
370 }
371}