1use std::net::Ipv6Addr;
6
7pub fn same_data(record_type: &str, input: &str, stored: &str) -> bool {
19 let input = input.trim();
20 match record_type.to_ascii_uppercase().as_str() {
21 "AAAA" => match (input.parse::<Ipv6Addr>(), stored.parse::<Ipv6Addr>()) {
22 (Ok(a), Ok(b)) => a == b,
23 _ => input == stored,
24 },
25 "CNAME" | "PTR" | "NS" => fqdn(input) == fqdn(stored),
26 "MX" | "SRV" => {
28 let (a, b) = (tokens(input), tokens(stored));
29 a.len() == b.len()
30 && !a.is_empty()
31 && a[..a.len() - 1] == b[..b.len() - 1]
32 && fqdn(a[a.len() - 1]) == fqdn(b[b.len() - 1])
33 }
34 "TXT" if !input.starts_with('"') => format!("\"{input}\"") == stored,
36 _ => tokens(input) == tokens(stored),
37 }
38}
39
40fn tokens(text: &str) -> Vec<&str> {
41 text.split_whitespace().collect()
42}
43
44fn fqdn(name: &str) -> String {
46 let lower = name.trim().to_lowercase();
47 if lower.ends_with('.') {
48 lower
49 } else {
50 format!("{lower}.")
51 }
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57
58 #[test]
59 fn matches_canonical_forms() {
60 assert!(same_data("A", " 192.0.2.1 ", "192.0.2.1"));
61 assert!(!same_data("A", "192.0.2.2", "192.0.2.1"));
62 assert!(same_data("aaaa", "2001:DB8:0:0::1", "2001:db8::1"));
63 assert!(same_data(
64 "CNAME",
65 "Target.Example.com",
66 "target.example.com."
67 ));
68 assert!(same_data(
69 "MX",
70 "10 mail.example.com",
71 "10 mail.example.com."
72 ));
73 assert!(!same_data(
74 "MX",
75 "20 mail.example.com",
76 "10 mail.example.com."
77 ));
78 assert!(same_data("MX", "0 .", "0 ."));
79 assert!(same_data(
80 "SRV",
81 "1 2 5060 sip.example.com",
82 "1 2 5060 sip.example.com."
83 ));
84 assert!(same_data("TXT", "v=spf1 -all", "\"v=spf1 -all\""));
85 assert!(same_data("TXT", "\"a\" \"b\"", "\"a\" \"b\""));
86 assert!(same_data(
87 "CAA",
88 "0 issue \"letsencrypt.org\"",
89 "0 issue \"letsencrypt.org\""
90 ));
91 }
92}