cdns_rs/
cfg_host_parser.rs

1/*-
2 * cdns-rs - a simple sync/async DNS query library
3 * 
4 * Copyright (C) 2020  Aleksandr Morozov
5 * 
6 * Copyright (C) 2025 Aleksandr Morozov
7 * 
8 * The syslog-rs crate can be redistributed and/or modified
9 * under the terms of either of the following licenses:
10 *
11 *   1. the Mozilla Public License Version 2.0 (the “MPL”) OR
12 *                     
13 *   2. EUROPEAN UNION PUBLIC LICENCE v. 1.2 EUPL © the European Union 2007, 2016
14 */
15
16/// This file contains the config file parsers.
17
18use std::borrow::Borrow;
19use std::collections::HashSet;
20use std::hash::{Hash, Hasher};
21use std::net::IpAddr;
22
23use crate::common::HOST_CFG_PATH;
24use crate::{error::*, internal_error_map, write_error, QType};
25use crate::tokenizer::*;
26
27
28/// An /etc/hosts file parser
29
30
31#[derive(Clone, Debug)]
32pub struct HostnameEntry
33{
34    ip: IpAddr,
35    hostnames: Vec<String>,
36}
37
38impl Eq for HostnameEntry {}
39
40impl PartialEq for HostnameEntry 
41{
42    fn eq(&self, other: &HostnameEntry) -> bool 
43    {
44        return self.ip == other.ip;
45    }
46}
47
48impl Borrow<IpAddr> for HostnameEntry
49{
50    fn borrow(&self) -> &IpAddr 
51    {
52        return &self.ip;    
53    }
54}
55
56impl Hash for HostnameEntry 
57{
58    fn hash<H: Hasher>(&self, state: &mut H) 
59    {
60        self.ip.hash(state);
61    }
62}
63
64
65impl HostnameEntry
66{
67    /// Returns the IP address
68    pub 
69    fn get_ip(&self) -> &IpAddr
70    {
71        return &self.ip;
72    }
73
74    /// Returns the slice with the hostnames
75    pub 
76    fn get_hostnames(&self) -> &[String]
77    {
78        return self.hostnames.as_slice();
79    }
80
81    /// Returns the iterator of the hostnames
82    pub 
83    fn get_hostnames_iter(&self) -> std::slice::Iter<'_, String>
84    {
85        return self.hostnames.iter();
86    }
87}
88
89#[derive(Clone, Debug)]
90pub struct HostConfig
91{
92    hostnames: HashSet<HostnameEntry>
93}
94
95impl Default for HostConfig
96{
97    fn default() -> Self 
98    {
99        return
100            Self 
101            { 
102                hostnames: Default::default(), 
103            };
104    }
105}
106
107impl HostConfig
108{
109    pub 
110    fn is_empty(&self) -> bool
111    {
112        return self.hostnames.is_empty();
113    }
114
115    pub 
116    fn search_by_ip(&self, ip: &IpAddr) -> Option<&HostnameEntry>
117    {
118        return self.hostnames.get(ip);
119    }
120
121    // Expensive operation. Walks through the list in On(Ologn) or On^2
122    pub 
123    fn search_by_fqdn(&self, qtype: &QType, name: &str) -> Option<&HostnameEntry>
124    {
125        for host in self.hostnames.iter()
126        {
127            for fqdn in host.hostnames.iter()
128            {
129                // check that fqdn match and IP type is same as requested
130                if name == fqdn.as_str() && qtype.ipaddr_match(&host.ip)
131                {
132                    return Some(host);
133                }
134            }
135        }
136
137        return None;
138    }
139
140    pub 
141    fn parse_host_file_internal(file_content: String) -> CDnsResult<Self>
142    {
143        let mut tk = ConfTokenizer::from_str(&file_content)?;
144        let mut he_list: HashSet<HostnameEntry> = HashSet::new();
145
146        loop
147        {
148            let field_ip = tk.read_next()?;
149    
150            if field_ip.is_none() == true
151            {
152                // reached EOF
153                break;
154            }
155
156            //println!("ip: {}", field_ip.as_ref().unwrap());
157    
158            let ip: IpAddr = 
159                match field_ip.unwrap().parse()
160                {
161                    Ok(r) => r,
162                    Err(_e) => 
163                    {
164                        // skip
165                        tk.skip_upto_eol();
166                        continue;
167                    }
168                };
169    
170            let hostnames = tk.read_upto_eol()?;
171
172            if hostnames.len() > 0
173            {
174                let he = HostnameEntry{ ip: ip, hostnames: hostnames };
175                he_list.insert(he);
176            }
177            else
178            {
179                write_error!(
180                    internal_error_map!(CDnsErrorType::ConfigError, 
181                        "in file: '{}' IP is not defined with domain name: '{}'\n", HOST_CFG_PATH, ip)
182                );
183            }
184        }
185
186        if he_list.len() == 0
187        {
188            write_error!(
189                internal_error_map!(CDnsErrorType::ConfigError, "file: '{}' file is empty or damaged!\n", 
190                    HOST_CFG_PATH)
191            );
192        }
193
194        return Ok(
195            Self
196            {
197                hostnames: he_list
198            }
199        );
200    }    
201}
202
203#[cfg(test)]
204mod tests
205{
206    use std::net::IpAddr;
207
208    use crate::cfg_host_parser::HostConfig;
209    
210    #[test]
211    fn test_parse_host_file_0()
212    {
213        let hosts1: Vec<&'static str> = vec!["debian-laptop"];
214        let hosts2: Vec<&'static str> = vec!["localhost", "ip6-localhost", "ip6-loopback"];
215        let hosts3: Vec<&'static str> = vec!["ip6-allnodes"];
216        let hosts4: Vec<&'static str> = vec!["ip6-allrouters"];
217
218        let ip1: IpAddr = "127.0.1.1".parse().unwrap();
219        let ip2: IpAddr = "::1".parse().unwrap();
220        let ip3: IpAddr = "ff02::1".parse().unwrap();
221        let ip4: IpAddr = "ff02::2".parse().unwrap();
222
223        let ip_list = 
224            vec![
225                (ip1, hosts1), 
226                (ip2, hosts2), 
227                (ip3, hosts3),
228                (ip4, hosts4)
229            ];
230
231        let test =
232        "127.0. 0.1	localhost
233        127.0.1.1	debian-laptop
234        
235        # The following lines are desirable for IPv6 capable hosts
236        ::1     localhost ip6-localhost ip6-loopback
237        ff02::1 ip6-allnodes
238        ff02::2 ip6-allrouters".to_string();
239
240        let p = HostConfig::parse_host_file_internal(test);
241        assert_eq!(p.is_ok(), true, "{}", p.err().unwrap());
242
243        let p = p.unwrap();
244
245        for (ip, host) in ip_list
246        {
247            let res = p.hostnames.get(&ip);
248            assert_eq!(res.is_some(), true);
249
250            let res = res.unwrap();
251
252            assert_eq!(res.hostnames, host);
253        }
254
255        return;
256    }
257
258    #[test]
259    fn test_parse_host_file()
260    {
261
262        let ip0:IpAddr = "127.0.0.1".parse().unwrap();
263        let ip1:IpAddr = "127.0.1.1".parse().unwrap();
264        let ip2:IpAddr = "::1".parse().unwrap();
265        let ip3:IpAddr = "ff02::1".parse().unwrap();
266        let ip4:IpAddr = "ff02::2".parse().unwrap();
267
268        let hosts0: Vec<&'static str> = vec!["localhost"];
269        let hosts1: Vec<&'static str> = vec!["debian-laptop"];
270        let hosts2: Vec<&'static str> = vec!["localhost", "ip6-localhost", "ip6-loopback"];
271        let hosts3: Vec<&'static str> = vec!["ip6-allnodes"];
272        let hosts4: Vec<&'static str> = vec!["ip6-allrouters"];
273
274        let ip_list = 
275            vec![
276                (ip0, hosts0),
277                (ip1, hosts1), 
278                (ip2, hosts2), 
279                (ip3, hosts3),
280                (ip4, hosts4)
281            ];
282
283        let test =
284        "127.0.0.1	localhost
285        127.0.1.1	debian-laptop
286        
287        # The following lines are desirable for IPv6 capable hosts
288        ::1     localhost ip6-localhost ip6-loopback
289        ff02::1 ip6-allnodes
290        ff02::2 ip6-allrouters".to_string();
291
292
293        let p = HostConfig::parse_host_file_internal(test);
294        assert_eq!(p.is_ok(), true, "{}", p.err().unwrap());
295
296        let p = p.unwrap();
297
298        for (ip, host) in ip_list
299        {
300            let res = p.hostnames.get(&ip);
301            assert_eq!(res.is_some(), true);
302
303            let res = res.unwrap();
304
305            assert_eq!(res.hostnames, host);
306        }
307    }
308
309    #[test]
310    fn test_parse_host_file_2()
311    {
312
313        let ip0:IpAddr = "127.0.0.1".parse().unwrap();
314        let ip1:IpAddr = "127.0.1.1".parse().unwrap();
315        let ip2:IpAddr = "::1".parse().unwrap();
316        let ip3:IpAddr = "ff02::1".parse().unwrap();
317        let ip4:IpAddr = "ff02::2".parse().unwrap();
318
319        let hosts0: Vec<&'static str> = vec!["localhost", "localdomain", "domain.local"];
320        let hosts1: Vec<&'static str> = vec!["debian-laptop", "test123.domain.tld"];
321        let hosts2: Vec<&'static str> = vec!["localhost", "ip6-localhost", "ip6-loopback"];
322        let hosts3: Vec<&'static str> = vec!["ip6-allnodes"];
323        let hosts4: Vec<&'static str> = vec!["ip6-allrouters"];
324
325
326        let ip_list = 
327            vec![
328                (ip0, hosts0),
329                (ip1, hosts1), 
330                (ip2, hosts2), 
331                (ip3, hosts3),
332                (ip4, hosts4)
333            ];
334
335        let test =
336        "127.0.0.1	localhost localdomain domain.local
337        127.0.1.1	debian-laptop test123.domain.tld
338        
339        # The following lines are desirable for IPv6 capable hosts
340        #
341        #
342        ::1     localhost ip6-localhost ip6-loopback
343        ff02::1 ip6-allnodes
344        ff02::2 ip6-allrouters
345        ".to_string();
346
347        let p = HostConfig::parse_host_file_internal(test);
348        assert_eq!(p.is_ok(), true, "{}", p.err().unwrap());
349
350        let p = p.unwrap();
351
352        for (ip, host) in ip_list
353        {
354            let res = p.hostnames.get(&ip);
355            assert_eq!(res.is_some(), true);
356
357            let res = res.unwrap();
358
359            assert_eq!(res.hostnames, host);
360        }
361    }
362
363}