Skip to main content

dinio_sdk/ops/
rdata.rs

1//! Comparing user-typed record data with Core's canonical form (`../core/src/dns/rdata.ts`),
2//! so a value can be addressed by its data. Best effort: types without a known canonical
3//! form compare with collapsed whitespace; callers fall back to the value's uuid.
4
5use std::net::Ipv6Addr;
6
7/// Whether `input` (as a user would type it) denotes the stored canonical `stored` data.
8///
9/// # Examples
10///
11/// ```
12/// use dinio_sdk::ops::same_data;
13///
14/// assert!(same_data("AAAA", "2001:DB8:0:0::1", "2001:db8::1"));
15/// assert!(same_data("MX", "10 Mail.Example.com", "10 mail.example.com."));
16/// assert!(same_data("TXT", "v=spf1 -all", "\"v=spf1 -all\""));
17/// ```
18pub 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        // Last token is a name, the rest numbers.
27        "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        // Core quotes unquoted TXT as one string.
35        "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
44/// Lowercase with exactly one trailing dot (`.` stays the root).
45fn 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}