dns_types/hosts/
types.rs

1use std::collections::HashMap;
2use std::net::{Ipv4Addr, Ipv6Addr};
3
4use crate::protocol::types::*;
5use crate::zones::types::*;
6
7/// TTL used when converting into A / AAAA records.
8pub const TTL: u32 = 5;
9
10/// A collection of A records.
11#[derive(Debug, Clone, Eq, PartialEq)]
12#[cfg_attr(any(feature = "test-util", test), derive(arbitrary::Arbitrary))]
13pub struct Hosts {
14    pub v4: HashMap<DomainName, Ipv4Addr>,
15    pub v6: HashMap<DomainName, Ipv6Addr>,
16}
17
18impl Hosts {
19    pub fn new() -> Self {
20        Self {
21            v4: HashMap::new(),
22            v6: HashMap::new(),
23        }
24    }
25
26    /// Merge another hosts file into this one.  If the same name has
27    /// records in both files, the new file will win.
28    pub fn merge(&mut self, other: Hosts) {
29        for (name, address) in other.v4 {
30            self.v4.insert(name, address);
31        }
32        for (name, address) in other.v6 {
33            self.v6.insert(name, address);
34        }
35    }
36
37    /// Convert a zone into a hosts file, discarding any non-A and
38    /// non-AAAA records.
39    pub fn from_zone_lossy(zone: &Zone) -> Self {
40        let mut v4 = HashMap::new();
41        let mut v6 = HashMap::new();
42        for (name, zrs) in zone.all_records() {
43            for zr in zrs {
44                let rr = zr.to_rr(name);
45                match rr.rtype_with_data {
46                    RecordTypeWithData::A { address } => {
47                        v4.insert(rr.name, address);
48                    }
49                    RecordTypeWithData::AAAA { address } => {
50                        v6.insert(rr.name, address);
51                    }
52                    _ => (),
53                }
54            }
55        }
56
57        Self { v4, v6 }
58    }
59}
60
61impl Default for Hosts {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67impl From<Hosts> for Zone {
68    fn from(hosts: Hosts) -> Zone {
69        let mut zone = Self::default();
70        for (name, address) in hosts.v4 {
71            zone.insert(&name, RecordTypeWithData::A { address }, TTL);
72        }
73        for (name, address) in hosts.v6 {
74            zone.insert(&name, RecordTypeWithData::AAAA { address }, TTL);
75        }
76        zone
77    }
78}
79
80impl TryFrom<Zone> for Hosts {
81    type Error = TryFromZoneError;
82
83    /// # Errors
84    ///
85    /// If the zone has wildcard domain names or non-A / non-AAAA
86    /// record types.
87    fn try_from(zone: Zone) -> Result<Self, Self::Error> {
88        if !zone.all_wildcard_records().is_empty() {
89            return Err(TryFromZoneError::HasWildcardRecords);
90        }
91
92        let mut v4 = HashMap::new();
93        let mut v6 = HashMap::new();
94        for (name, zrs) in zone.all_records() {
95            for zr in zrs {
96                let rr = zr.to_rr(name);
97                match rr.rtype_with_data {
98                    RecordTypeWithData::A { address } => {
99                        v4.insert(rr.name, address);
100                    }
101                    RecordTypeWithData::AAAA { address } => {
102                        v6.insert(rr.name, address);
103                    }
104                    _ => return Err(TryFromZoneError::HasRecordTypesOtherThanA),
105                }
106            }
107        }
108
109        Ok(Self { v4, v6 })
110    }
111}
112
113/// Errors that can arise when converting a `Zone` into a `Hosts`.
114#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
115pub enum TryFromZoneError {
116    HasWildcardRecords,
117    HasRecordTypesOtherThanA,
118}
119
120#[cfg(test)]
121mod tests {
122    use crate::protocol::types::test_util::*;
123
124    use super::test_util::*;
125    use super::*;
126
127    #[test]
128    fn hosts_zone_roundtrip() {
129        for _ in 0..100 {
130            let expected = arbitrary_hosts();
131            if let Ok(actual) = Hosts::try_from(Zone::from(expected.clone())) {
132                assert_eq!(expected, actual);
133            } else {
134                panic!("expected round-trip");
135            }
136        }
137    }
138
139    #[test]
140    fn hosts_merge_zone_merge_equiv_when_disjoint() {
141        for _ in 0..100 {
142            let hosts1 = arbitrary_hosts_with_apex(&domain("hosts1."));
143            let hosts2 = arbitrary_hosts_with_apex(&domain("hosts2."));
144
145            let mut combined_hosts = hosts1.clone();
146            combined_hosts.merge(hosts2.clone());
147
148            let combined_zone_direct = Zone::from(combined_hosts.clone());
149            let mut combined_zone_indirect = Zone::from(hosts1);
150            combined_zone_indirect.merge(hosts2.into()).unwrap();
151
152            assert_eq!(combined_zone_direct, combined_zone_indirect);
153            assert_eq!(Ok(combined_hosts), combined_zone_direct.try_into());
154        }
155    }
156
157    fn arbitrary_hosts_with_apex(apex: &DomainName) -> Hosts {
158        let arbitrary = arbitrary_hosts();
159
160        let mut out = Hosts::new();
161        for (k, v) in arbitrary.v4 {
162            out.v4.insert(k.make_subdomain_of(apex).unwrap(), v);
163        }
164        for (k, v) in arbitrary.v6 {
165            out.v6.insert(k.make_subdomain_of(apex).unwrap(), v);
166        }
167        out
168    }
169}
170
171#[cfg(any(feature = "test-util", test))]
172#[allow(clippy::missing_panics_doc)]
173pub mod test_util {
174    use super::*;
175
176    use arbitrary::{Arbitrary, Unstructured};
177    use rand::Rng;
178
179    pub fn arbitrary_hosts() -> Hosts {
180        let mut rng = rand::rng();
181        for size in [128, 256, 512, 1024, 2048, 4096] {
182            let mut buf = Vec::new();
183            for _ in 0..size {
184                buf.push(rng.random());
185            }
186
187            if let Ok(rr) = Hosts::arbitrary(&mut Unstructured::new(&buf)) {
188                return rr;
189            }
190        }
191
192        panic!("could not generate arbitrary value!");
193    }
194}