dns_types/zones/
serialise.rs

1use bytes::Bytes;
2use std::collections::HashSet;
3use std::fmt::Write as _;
4
5use crate::protocol::types::*;
6use crate::zones::types::*;
7
8impl Zone {
9    pub fn serialise(&self) -> String {
10        let mut out = String::new();
11
12        if let Some(soa) = self.get_soa() {
13            let show_origin = !self.get_apex().is_root();
14            let serialised_apex = serialise_octets(
15                &self
16                    .get_apex()
17                    .to_dotted_string()
18                    .bytes()
19                    .collect::<Bytes>(),
20                false,
21            );
22
23            if show_origin {
24                _ = writeln!(&mut out, "$ORIGIN {serialised_apex}");
25                out.push('\n');
26            }
27
28            _ = writeln!(
29                &mut out,
30                "{} IN SOA {}",
31                if show_origin { "@" } else { &serialised_apex },
32                self.serialise_rdata(&soa.to_rdata()),
33            );
34            out.push('\n');
35        }
36
37        let all_records = self.all_records();
38        let all_wildcard_records = self.all_wildcard_records();
39
40        let sorted_domains = {
41            let mut set = HashSet::new();
42            for name in all_records.keys() {
43                set.insert(*name);
44            }
45            for name in all_wildcard_records.keys() {
46                set.insert(*name);
47            }
48            let mut vec = set.into_iter().collect::<Vec<&DomainName>>();
49            vec.sort();
50            vec
51        };
52
53        for domain in sorted_domains {
54            if let Some(zrs) = all_records.get(domain) {
55                let has_wildcards = all_wildcard_records.contains_key(domain);
56                for zr in zrs {
57                    if zr.rtype_with_data.rtype() == RecordType::SOA {
58                        // already handled above, and it's invalid for
59                        // a zone to have multiple SOA records
60                        continue;
61                    }
62
63                    _ = writeln!(
64                        &mut out,
65                        "{}{} {} IN {} {}",
66                        self.serialise_domain(domain),
67                        if has_wildcards { "  " } else { "" },
68                        zr.ttl,
69                        zr.rtype_with_data.rtype(),
70                        self.serialise_rdata(&zr.rtype_with_data)
71                    );
72                }
73            }
74            if let Some(zrs) = all_wildcard_records.get(domain) {
75                for zr in zrs {
76                    _ = writeln!(
77                        &mut out,
78                        "*.{} {} IN {} {}",
79                        self.serialise_domain(domain),
80                        zr.ttl,
81                        zr.rtype_with_data.rtype(),
82                        self.serialise_rdata(&zr.rtype_with_data)
83                    );
84                }
85            }
86            out.push('\n');
87        }
88
89        out
90    }
91
92    /// Serialise a domain name: dotted string format, with the apex
93    /// chopped off if this is an authoritative zone (unless the apex
94    /// is the root domain, because that's only a single character
95    /// long so we may as well show it).
96    fn serialise_domain(&self, name: &DomainName) -> String {
97        let domain_str = {
98            let apex = self.get_apex();
99            if apex.is_root() || !self.is_authoritative() || !name.is_subdomain_of(apex) {
100                name.to_dotted_string()
101            } else if name == apex {
102                "@".to_string()
103            } else {
104                let labels_to_keep = name.labels.len() - apex.labels.len();
105                DomainName {
106                    labels: Vec::from(&name.labels[..labels_to_keep]),
107                    len: name.len - apex.len,
108                }
109                .to_dotted_string()
110            }
111        };
112
113        serialise_octets(&domain_str.bytes().collect::<Bytes>(), false)
114    }
115
116    /// Serialise the RDATA, with domains displayed relative to the apex (if
117    /// authoritative).
118    pub fn serialise_rdata(&self, rtype_with_data: &RecordTypeWithData) -> String {
119        match rtype_with_data {
120            RecordTypeWithData::A { address } => format!("{address}"),
121            RecordTypeWithData::NS { nsdname } => self.serialise_domain(nsdname),
122            RecordTypeWithData::MD { madname } => self.serialise_domain(madname),
123            RecordTypeWithData::MF { madname } => self.serialise_domain(madname),
124            RecordTypeWithData::CNAME { cname } => self.serialise_domain(cname),
125            RecordTypeWithData::SOA {
126                mname,
127                rname,
128                serial,
129                refresh,
130                retry,
131                expire,
132                minimum,
133            } => format!(
134                "{} {} {serial} {refresh} {retry} {expire} {minimum}",
135                self.serialise_domain(mname),
136                self.serialise_domain(rname),
137            ),
138            RecordTypeWithData::MB { madname } => self.serialise_domain(madname),
139            RecordTypeWithData::MG { mdmname } => self.serialise_domain(mdmname),
140            RecordTypeWithData::MR { newname } => self.serialise_domain(newname),
141            RecordTypeWithData::NULL { octets } => serialise_octets(octets, true),
142            RecordTypeWithData::WKS { octets } => serialise_octets(octets, true),
143            RecordTypeWithData::PTR { ptrdname } => self.serialise_domain(ptrdname),
144            RecordTypeWithData::HINFO { octets } => serialise_octets(octets, true),
145            RecordTypeWithData::MINFO { rmailbx, emailbx } => format!(
146                "{} {}",
147                self.serialise_domain(rmailbx),
148                self.serialise_domain(emailbx)
149            ),
150            RecordTypeWithData::MX {
151                preference,
152                exchange,
153            } => format!("{preference} {}", self.serialise_domain(exchange)),
154            RecordTypeWithData::TXT { octets } => serialise_octets(octets, true),
155            RecordTypeWithData::AAAA { address } => format!("{address}"),
156            RecordTypeWithData::SRV {
157                priority,
158                weight,
159                port,
160                target,
161            } => format!(
162                "{priority} {weight} {port} {}",
163                self.serialise_domain(target)
164            ),
165            RecordTypeWithData::Unknown { octets, .. } => serialise_octets(octets, true),
166        }
167    }
168}
169
170/// Serialise a string of octets to a quoted or unquoted string with
171/// the appropriate escaping.
172fn serialise_octets(octets: &[u8], quoted: bool) -> String {
173    let mut out = String::with_capacity(2 + octets.len());
174
175    if quoted {
176        out.push('"');
177    }
178
179    for octet in octets {
180        if *octet == b'"' || *octet == b'\\' || *octet == b';' || *octet == b'(' || *octet == b')' {
181            out.push('\\');
182            out.push(*octet as char);
183        } else if *octet < 32 || *octet > 126 || (*octet == 32 && !quoted) {
184            out.push('\\');
185            let digit3 = *octet % 10;
186            let digit2 = (*octet / 10) % 10;
187            let digit1 = (*octet / 100) % 10;
188            out.push((digit1 + 48) as char);
189            out.push((digit2 + 48) as char);
190            out.push((digit3 + 48) as char);
191        } else {
192            out.push(*octet as char);
193        }
194    }
195
196    if quoted {
197        out.push('"');
198    }
199
200    out
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn serialise_octets_special() {
209        assert_eq!("\\012", serialise_octets(&[12], false));
210        assert_eq!("\\234", serialise_octets(&[234], false));
211
212        assert_eq!("\\\\", serialise_octets(b"\\", false));
213        assert_eq!("\\\"", serialise_octets(b"\"", false));
214    }
215
216    #[test]
217    fn serialise_octets_space() {
218        assert_eq!("\\032", serialise_octets(b" ", false));
219        assert_eq!("\" \"", serialise_octets(b" ", true));
220    }
221}